Files
dysync/app/src/pages/login/LoginBox.vue
T

354 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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>
<ThemeProvider :color="{
middle: { 'bg-base': '#fff' },
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
}">
<!-- 登录卡片 - 移动端全屏 + PC端保留原宽度 -->
<div class="login-box rounded-2xl shadow-lg p-6 md:p-8 lg:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1 mx-auto" style="box-sizing: border-box; max-width: 90vw;">
<!-- 顶部Logo区域 - 边框圈精准贴合Logo内容 -->
<div class="flex flex-col items-center mb-6 md:mb-8">
<div class="w-30 h-30 md:w-36 md:h-36 bg-primary/10 rounded-full flex items-center justify-center mb-4 md:mb-5">
<div class="w-24 h-24 md:w-28 h-28 rounded-full flex items-center justify-center">
<img src="/logo.png" alt="抖小云logo" class="w-18 h-18 md:w-20 h-20 object-contain" />
</div>
</div>
<h2 class="third-title text-xl md:text-2xl font-bold text-gray-800 tracking-tight">抖小云</h2>
</div>
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
<!-- 用户名输入框 -->
<a-form-item :required="true" name="username" class="mb-4 md:mb-5" :validate-status="usernameStatus">
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" style="padding: 0 12px;">
<template #prefix>
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</template>
</a-input>
</a-form-item>
<!-- 密码输入框 -->
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'" style="padding: 0 12px;">
<template #prefix>
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</template>
<!-- 密码显示/隐藏图标 -->
<template #suffix>
<div class="w-8 h-8 flex items-center justify-center" @click="showPassword = !showPassword">
<EyeOutlined v-if="showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
<EyeInvisibleOutlined v-else class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
</div>
</template>
</a-input>
</a-form-item>
<!-- 记住密码 -->
<div class="flex items-center justify-between mb-5 md:mb-6 px-1">
<a-form-item class="mb-0">
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
<span class="text-xs md:text-sm">记住密码</span>
</a-checkbox>
</a-form-item>
<a-form-item class="mb-0">
<div class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer" @click="handleForgotPassword">
<span class="text-xs md:text-sm">忘记密码</span>
</div>
</a-form-item>
</div>
<!-- 登录按钮 -->
<a-button htmlType="submit" class="h-[48px] md:h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading" style="touch-action: manipulation; -webkit-tap-highlight-color: transparent;">
<span v-if="!loading">登录</span>
<span v-else>登录中...</span>
</a-button>
</a-form>
</div>
</ThemeProvider>
<!-- 忘记密码弹窗 -->
<a-modal v-model:visible="forgotModalVisible" title="密码重置教程" width="50%" :max-width="500" centered :mask-closable="false" :footer="null" class="forgot-modal">
<div class="p-2">
<!-- 步骤说明 -->
<div class="space-y-4 mb-6">
<div class="flex items-start">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">1</div>
<div class="flex-1 text-gray-700 text-sm leading-relaxed">在 db 目录下新建一个文本文件,命名为 <span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">pwd.txt</span></div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">2</div>
<div class="flex-1 text-gray-700 text-sm leading-relaxed">打开文件写入你要设置的新密码,留空则自动重置为默认密码:<span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">douyin2026</span></div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">3</div>
<div class="flex-1 text-gray-700 text-sm leading-relaxed">重启 docker服务或者飞牛安装的抖小云应用 ,使用新密码登录即可</div>
</div>
</div>
<!-- 知道了按钮 -->
<div class="flex justify-center">
<a-button type="primary" class="h-10 px-8 rounded-lg" @click="forgotModalVisible = false">
我知道了
</a-button>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted, computed } from 'vue';
import { useAccountStore } from '@/store';
import { ThemeProvider } from 'stepin';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
// 控制密码显示/隐藏的状态
const showPassword = ref(false);
// 记住密码状态
const rememberPassword = ref(false);
// 加载状态
const loading = ref(false);
// 忘记密码弹窗显示状态
const forgotModalVisible = ref(false);
// 表单状态(用于输入框验证反馈)
const form = reactive({
username: '',
password: '',
});
const usernameStatus = computed(() => (form.username ? 'success' : ''));
const passwordStatus = computed(() => (form.password ? 'success' : ''));
export interface LoginFormProps {
username: string;
password: string;
}
const emit = defineEmits<{
(e: 'success', fields: LoginFormProps): void;
(e: 'failure', reason: string, fields: LoginFormProps): void;
(e: 'register'): void;
(e: 'forgot-password'): void;
}>();
const accountStore = useAccountStore();
// 页面加载时读取本地存储的记住密码信息
onMounted(() => {
const savedUser = localStorage.getItem('rememberedUser');
if (savedUser) {
try {
const { username, password } = JSON.parse(savedUser);
form.username = username;
form.password = password;
rememberPassword.value = true;
} catch (e) {
console.error('读取保存的用户信息失败', e);
localStorage.removeItem('rememberedUser');
}
}
});
// 登录处理
async function login(params: LoginFormProps) {
// 简单表单验证
if (!params.username) {
message.warning('请输入用户名');
return;
}
if (!params.password) {
message.warning('请输入密码');
return;
}
loading.value = true;
// 根据记住密码状态保存/清除用户信息
if (rememberPassword.value) {
localStorage.setItem(
'rememberedUser',
JSON.stringify({
username: params.username,
password: params.password,
})
);
} else {
localStorage.removeItem('rememberedUser');
}
try {
const res = await accountStore.login(params.username, params.password);
emit('success', params);
console.log(res);
message.success('登录成功!');
} catch (e: any) {
emit('failure', e.message || '登录失败', params);
message.error(e.data?.erro || '登录失败,请重试');
} finally {
loading.value = false;
}
}
// 忘记密码处理 - 打开弹窗
function handleForgotPassword() {
forgotModalVisible.value = true;
emit('forgot-password');
}
// 注册处理
function handleRegister() {
emit('register');
}
</script>
<style scoped>
/* 全局布局适配 - 替代原外层容器的样式 */
:root {
-webkit-text-size-adjust: 100%; /* 禁止iOS文本缩放 */
-webkit-font-smoothing: antialiased; /* 优化字体渲染 */
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* 全局居中布局 */
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
}
/* 登录卡片核心样式 */
.login-box {
box-sizing: border-box;
touch-action: manipulation; /* 优化触摸性能 */
/* 移动端全屏,PC端保留原max-w-md(默认28rem/448px)+ 90vw限制 */
}
/* 移动端适配(<768px) */
@media (max-width: 767.98px) {
body {
padding: 0;
}
.login-box {
max-width: 100vw !important; /* 移动端全屏 */
min-height: 100vh;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 20px 16px !important;
padding-top: 20vh !important; /* 移动端垂直居中 */
}
.third-title {
font-size: 1.25rem !important;
}
::v-deep(.ant-input) {
font-size: 14px; /* 移动端字体适配 */
}
}
/* 小屏手机额外适配 */
@media (max-width: 480px) {
.login-box {
padding-top: 15vh !important;
}
}
/* PC端样式(保留原有宽度,仅调小最大宽度) */
@media (min-width: 768px) {
body {
padding: 8px;
}
.login-box {
/* 原宽度是 min(448px, 90vw),现调整为更小的数值,比如 380px(可根据需求修改) */
max-width: min(340px, 90vw) !important;
min-height: auto !important;
border: 1px solid #e5e7eb !important;
border-radius: 1rem !important;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important;
padding-top: 0 !important;
}
}
/* 输入框聚焦动画优化 */
::v-deep(.ant-input) {
box-sizing: border-box;
}
::v-deep(.ant-input:focus) {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important;
border-color: #4f46e5 !important;
outline: none; /* 移除默认轮廓 */
}
/* 复选框样式优化 */
::v-deep(.ant-checkbox-checked .ant-checkbox-inner) {
background-color: #4f46e5 !important;
border-color: #4f46e5 !important;
}
::v-deep(.ant-checkbox:hover .ant-checkbox-inner) {
border-color: #4f46e5 !important;
}
::v-deep(.ant-checkbox) {
transform: scale(0.9); /* 移动端复选框适当缩小 */
}
/* 按钮样式优化 */
::v-deep(.ant-btn-primary) {
background-color: #4f46e5 !important;
border-color: #4f46e5 !important;
box-sizing: border-box;
}
::v-deep(.ant-btn-primary:hover) {
background-color: #4338ca !important;
border-color: #4338ca !important;
}
::v-deep(.ant-btn-primary:focus) {
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important;
outline: none;
}
/* 加载状态动画优化 */
::v-deep(.ant-btn-loading .ant-btn-loading-icon) {
margin-right: 8px !important;
}
/* 弹窗样式美化 */
::v-deep(.forgot-modal .ant-modal-header) {
border-radius: 12px 12px 0 0;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
::v-deep(.forgot-modal .ant-modal-title) {
color: #1f2937;
font-weight: 600;
font-size: 16px;
}
::v-deep(.forgot-modal .ant-modal-body) {
padding: 16px 20px;
}
::v-deep(.forgot-modal .ant-modal-content) {
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
overflow: hidden;
}
/* 防止点击闪烁和触摸高亮 */
* {
-webkit-tap-highlight-color: transparent;
tap-highlight-color: transparent;
}
/* 平滑滚动 */
html {
scroll-behavior: smooth;
overflow-x: hidden; /* 防止移动端横向滚动 */
}
</style>