Files
2026-01-14 14:24:58 +08:00

823 lines
17 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="main-view">
<div class="container">
<div class="logo">
<image src="/static/logo.png" alt="YXD" />
</div>
<h1>登录 YXD 智能应用</h1>
<div class="login-form">
<div class="form-group">
<label for="username">账号</label>
<input id="username" v-model="username" type="text" placeholder="请输入账号/邮箱/手机号"
:class="{ 'error': usernameError }" @input="clearError('username')" />
<div v-if="usernameError" class="error-message">{{ usernameError }}</div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="password-input">
<input id="password" v-model="password" :type="showPassword ? 'text' : 'password'"
placeholder="请输入密码" :class="{ 'error': passwordError }" @input="clearError('password')"
@keyup.enter="handleLogin" />
<button type="button" class="toggle-password" @click="showPassword = !showPassword">
<span v-if="showPassword">🙈</span>
<span v-else>👁</span>
</button>
</div>
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
</div>
<div class="form-options">
<!-- <label class="remember-me">
<checkbox-group @change="toggleRememberMe">
<checkbox :checked="rememberMe"></checkbox>
</checkbox-group>
<span>记住账号</span>
</label> -->
<label class="remember-me" @click="toggleRememberMe">
<!-- 自定义勾选框 -->
<div class="custom-checkbox-wrapper">
<div class="custom-checkbox" :class="{ 'checked': rememberMe }">
<span v-if="rememberMe"></span>
</div>
</div>
<span>记住账号</span>
</label>
<!-- <a href="#" class="forgot-password">忘记密码</a> -->
</div>
<button class="login-btn" @click="handleLogin" :disabled="isLoggingIn">
<span v-if="isLoggingIn"></span>
<span v-else>🔑</span>
{{ isLoggingIn ? '登录中...' : '立即登录' }}
</button>
<div v-if="loginError" class="login-error">
<span></span>
{{ loginError }}
</div>
<!-- 登录成功后的网址显示 -->
<div v-if="redirectUrl" class="redirect-section">
<div class="success-message">
<span></span>
登录成功正在为您跳转...
</div>
<!-- <div class="url-display">
<input :value="redirectUrl" readonly />
<button class="copy-btn" @click="copyToClipboard" :title="copySuccess ? '已复制' : '复制链接'">
<span v-if="copySuccess"></span>
<span v-else>📋</span>
</button>
</div>
<button class="access-btn" @click="openExternalLink">
<span>🔗</span> 立即访问
</button> -->
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue';
// 表单数据
const username = ref('');
const password = ref('');
const rememberMe = ref(false);
const showPassword = ref(false);
// 状态
const isLoggingIn = ref(false);
const redirectUrl = ref('');
const copySuccess = ref(false);
// 错误信息
const usernameError = ref('');
const passwordError = ref('');
const loginError = ref('');
// 切换记住账号状态
const toggleRememberMe = () => {
rememberMe.value = !rememberMe.value;
console.log("触发toggleRememberMe");
};
// 模拟登录API - 实际项目中替换为真实API
const mockLoginApi = (user, pwd) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟登录验证
if (user && pwd) {
// 模拟返回的跳转URL
resolve({
success: true,
redirectUrl: 'https://ws.yuxindazhineng.com/',
userInfo: {
username: user,
name: '用户' + user.substring(0, 3)
}
});
} else {
reject({
success: false,
message: '账号或密码错误'
});
}
}, 1500);
});
};
// 表单验证
const validateForm = () => {
let isValid = true;
usernameError.value = '';
passwordError.value = '';
if (!username.value.trim()) {
usernameError.value = '请输入账号';
isValid = false;
}
if (!password.value) {
passwordError.value = '请输入密码';
isValid = false;
} else if (password.value.length < 6) {
passwordError.value = '密码至少6位字符';
isValid = false;
}
return isValid;
};
// 清除错误信息
const clearError = (field) => {
if (field === 'username') {
usernameError.value = '';
} else if (field === 'password') {
passwordError.value = '';
}
loginError.value = '';
};
// 处理登录
const handleLogin = async () => {
if (!validateForm()) {
return;
}
isLoggingIn.value = true;
loginError.value = '';
try {
// 实际项目中替换为真实API调用
const response = await mockLoginApi(username.value, password.value);
if (response.success) {
// 存储用户信息(实际项目中应该使用更安全的方式)
if (rememberMe.value) {
// 使用 uni.setStorageSync 代替 localStorage.setItem
uni.setStorageSync('yxd_username', username.value);
} else {
// 如果不记住账号,清除存储
uni.removeStorageSync('yxd_username');
}
// 获取返回的跳转URL
redirectUrl.value = response.redirectUrl;
// 自动跳转(可选)
setTimeout(() => {
openExternalLink();
}, 2000);
}
} catch (error) {
loginError.value = error.message || '登录失败,请检查账号密码';
} finally {
isLoggingIn.value = false;
}
};
// 打开外部链接的通用方法
function openExternalLink() {
if (!redirectUrl.value) return;
// #ifdef H5
window.open(redirectUrl.value, '_blank')
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(redirectUrl.value)
// #endif
// #ifdef MP-WEIXIN
uni.setClipboardData({
data: redirectUrl.value,
success: () => {
uni.showModal({
content: '链接已复制,请在浏览器中打开',
showCancel: false
})
}
})
// #endif
}
// 复制到剪贴板
const copyToClipboard = () => {
if (!redirectUrl.value) return;
// #ifdef H5
navigator.clipboard.writeText(redirectUrl.value).then(() => {
copySuccess.value = true;
setTimeout(() => {
copySuccess.value = false;
}, 2000);
});
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
uni.setClipboardData({
data: redirectUrl.value,
success: () => {
copySuccess.value = true;
setTimeout(() => {
copySuccess.value = false;
}, 2000);
}
});
// #endif
};
// 在组件挂载后执行的操作
onMounted(() => {
// 检查是否记住账号 - 修改这里
const savedUsername = uni.getStorageSync('yxd_username');
if (savedUsername) {
username.value = savedUsername;
rememberMe.value = true;
}
console.log('登录组件已挂载');
});
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
/* 移除全局body样式改为在main-view中设置背景 */
.main-view {
z-index: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #0a0e17 0%, #131826 100%);
color: #e8f4f8;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 500px;
background: linear-gradient(135deg, rgba(23, 32, 56, 0.9), rgba(19, 28, 46, 0.95));
border-radius: 20px;
overflow: hidden;
box-shadow:
0 20px 50px rgba(0, 10, 30, 0.5),
0 0 0 1px rgba(59, 130, 246, 0.15),
inset 0 0 30px rgba(0, 0, 0, 0.5);
text-align: center;
align-items: center;
justify-content: center;
padding: 45px 35px;
margin: 10px;
backdrop-filter: blur(10px);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.logo {
width: 110px;
height: 110px;
margin: 0 auto 30px;
background: linear-gradient(135deg, rgba(23, 32, 56, 0.8), rgba(59, 130, 246, 0.2));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(59, 130, 246, 0.3),
inset 0 0 20px rgba(59, 130, 246, 0.2);
overflow: hidden;
padding: 12px;
position: relative;
}
.logo::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05));
z-index: 1;
}
.logo image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
position: relative;
z-index: 2;
filter: drop-shadow(0 0 10px rgba(59, 130, 246, 0.5));
}
h1 {
font-size: 2.1rem;
font-weight: 700;
color: #60a5fa;
margin-bottom: 18px;
text-shadow: 0 0 15px rgba(96, 165, 250, 0.5);
letter-spacing: -0.3px;
}
.description {
color: #93c5fd;
font-size: 0.95rem;
margin: 0 0 30px 0;
line-height: 1.6;
font-weight: 400;
opacity: 0.9;
}
.login-form {
width: 100%;
}
.form-group {
margin-bottom: 22px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #93c5fd;
font-size: 14px;
}
.form-group uni-input {
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
width: 100%;
height: 30px;
padding-left: 8px;
}
.form-group uni-input .uni-input-input {
width: 100%;
padding: 15px 20px;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
font-size: 15px;
transition: all 0.3s ease;
background: rgba(15, 23, 42, 0.8);
box-shadow:
inset 0 2px 10px rgba(0, 0, 0, 0.3),
0 1px 0 rgba(255, 255, 255, 0.05);
color: #e2e8f0;
}
/* 修复输入框无法选中的问题 */
.form-group uni-input .uni-input-input {
pointer-events: auto !important;
user-select: auto !important;
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
}
.form-group uni-input:focus {
outline: none;
border-color: #3b82f6;
background: rgba(15, 23, 42, 0.9);
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.2),
inset 0 2px 10px rgba(0, 0, 0, 0.4);
}
.form-group uni-input::placeholder {
color: #64748b;
}
.form-group uni-input.error {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.05);
}
.error-message {
color: #fca5a5;
font-size: 13px;
margin-top: 8px;
font-weight: 500;
}
.password-input {
position: relative;
}
.toggle-password {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
background: transparent;
color: #93c5fd;
cursor: pointer;
font-size: 15px;
transition: all 0.2s ease;
z-index: 2;
}
/* .toggle-password:hover {
color: #60a5fa;
border-color: #3b82f6;
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
} */
.toggle-password span {
font-size: 16px;
display: inline-block;
vertical-align: middle;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28px;
font-size: 14px;
}
.remember-me {
display: flex;
align-items: center;
gap: 10px;
color: #93c5fd;
cursor: pointer;
font-weight: 500;
}
.custom-checkbox {
width: 18px;
height: 18px;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 4px;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.custom-checkbox.checked {
/* background: #3b82f6; */
border-color: #3b82f6;
}
.custom-checkbox.checked i {
color: white;
font-size: 12px;
}
/* 对✓字符加粗效果有限 */
/* .custom-checkbox span {
font-size: 12px;
font-weight: bold;
line-height: 1;
} */
/* checkbox样式 */
/* .remember-me uni-checkbox {
width: 18px;
height: 18px;
margin: 0;
padding: 0;
background: transparent;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.remember-me uni-checkbox:checked {
background: #3b82f6;
border-color: #3b82f6;
}
.remember-me uni-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
.remember-me uni-checkbox:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
} */
.forgot-password {
color: #60a5fa;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
}
.forgot-password:hover {
color: #3b82f6;
text-decoration: underline;
}
.login-btn {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
border: none;
width: 100%;
padding: 18px;
border-radius: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow:
0 8px 25px rgba(59, 130, 246, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.login-btn span {
font-size: 16px;
display: inline-block;
vertical-align: middle;
}
.login-btn:disabled span {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s;
}
.login-btn:hover:not(:disabled)::before {
left: 100%;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow:
0 12px 30px rgba(59, 130, 246, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.2);
background: linear-gradient(135deg, #60a5fa, #2563eb);
}
.login-btn:active:not(:disabled) {
transform: translateY(0);
box-shadow:
0 4px 15px rgba(59, 130, 246, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
.login-error {
margin-top: 18px;
padding: 14px;
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05));
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
color: #fca5a5;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.1);
}
.login-error span {
font-size: 16px;
}
.redirect-section {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid rgba(59, 130, 246, 0.2);
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success-message {
color: #4ade80;
font-weight: 600;
margin-bottom: 18px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 15px;
text-shadow: 0 0 10px rgba(74, 222, 128, 0.3);
}
.success-message span {
font-size: 16px;
}
.url-display {
background: linear-gradient(135deg, rgba(15, 23, 42, 0.8), rgba(30, 41, 59, 0.8));
padding: 15px;
border-radius: 12px;
margin-bottom: 18px;
border: 1px solid rgba(59, 130, 246, 0.3);
display: flex;
align-items: center;
gap: 12px;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.3);
}
.url-display input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
color: #60a5fa;
outline: none;
font-family: 'Monaco', 'Consolas', monospace;
font-weight: 500;
pointer-events: auto !important;
user-select: auto !important;
}
.copy-btn {
background: rgba(30, 41, 59, 0.8);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 8px;
padding: 9px 14px;
color: #93c5fd;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn span {
font-size: 16px;
}
.copy-btn:hover {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
border-color: #3b82f6;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.3);
}
.access-btn {
background: linear-gradient(135deg, #0ea5e9, #0369a1);
color: white;
border: none;
width: 100%;
padding: 16px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow:
0 8px 25px rgba(14, 165, 233, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
letter-spacing: 0.5px;
}
.access-btn span {
font-size: 16px;
}
.access-btn:hover {
transform: translateY(-2px);
box-shadow:
0 12px 30px rgba(14, 165, 233, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.2);
background: linear-gradient(135deg, #38bdf8, #0284c7);
}
.access-btn:active {
transform: translateY(0);
box-shadow:
0 4px 15px rgba(14, 165, 233, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
@media (max-width: 600px) {
.container {
padding: 35px 25px;
}
h1 {
font-size: 1.9rem;
}
.form-options {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.logo {
width: 95px;
height: 95px;
}
.form-group uni-input input {
padding: 14px 16px;
}
}
</style>