feat: 新增组件
This commit is contained in:
28
src/App.vue
28
src/App.vue
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import HelloWorld from "./components/HelloWorld.vue";
|
||||
import { useMainStore } from "./store";
|
||||
|
||||
const store = useMainStore();
|
||||
@@ -11,32 +10,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HelloWorld />
|
||||
|
||||
<!-- Pinia 状态管理示例 -->
|
||||
<div style="margin: 20px; padding: 20px; border: 1px solid #ccc">
|
||||
<h3>计数器: {{ store.count }}</h3>
|
||||
<button @click="store.increment()">+1</button>
|
||||
<button @click="store.decrement()">-1</button>
|
||||
</div>
|
||||
|
||||
<!-- 路由视图 -->
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import TheWelcome from '../components/TheWelcome.vue'
|
||||
import { onMounted } from "vue";
|
||||
onMounted(async () => {
|
||||
let res = await window.myApi.handleSend("liaoruiruirurirui");
|
||||
console.log(res);
|
||||
});
|
||||
const toMin = () => {
|
||||
window.myApi.windowMin();
|
||||
};
|
||||
const toBig = () => {
|
||||
window.myApi.windowMax();
|
||||
};
|
||||
const toClose = () => {
|
||||
window.myApi.windowClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="hearder">
|
||||
<span @click="toMin">最小化</span>
|
||||
<span @click="toBig">最大化</span>
|
||||
<span @click="toClose">关闭</span>
|
||||
</div>
|
||||
<div class="main">主要内容</div>
|
||||
<!-- <TheWelcome /> -->
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hearder {
|
||||
-webkit-app-region: drag;
|
||||
background-color: #ccc;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.hearder span {
|
||||
margin: 0 16px;
|
||||
border: 1px solid rgb(35, 34, 34);
|
||||
cursor: pointer;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.main {
|
||||
height: calc(100vh - 40px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import pinia from "./store";
|
||||
import "./style.css";
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../App.vue')
|
||||
}
|
||||
]
|
||||
path: "/",
|
||||
name: "login",
|
||||
component: () => import("../views/login/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
name: "home",
|
||||
component: () => import("../views/home/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "about",
|
||||
component: () => import("../views/about/index.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,79 +1,25 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
7
src/views/about/index.vue
Normal file
7
src/views/about/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
107
src/views/home/index.vue
Normal file
107
src/views/home/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<main class="h-screen flex flex-col">
|
||||
<!-- 标题栏 -->
|
||||
<div class="header bg-gray-300 h-10 w-full flex justify-end items-center" style="-webkit-app-region: drag;">
|
||||
<span @click="toMin"
|
||||
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-gray-400 transition-colors"
|
||||
style="-webkit-app-region: no-drag;">最小化</span>
|
||||
<span @click="toBig"
|
||||
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-gray-400 transition-colors"
|
||||
style="-webkit-app-region: no-drag;">最大化</span>
|
||||
<span @click="toClose"
|
||||
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-red-400 transition-colors"
|
||||
style="-webkit-app-region: no-drag;">关闭</span>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="flex-1 flex flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
|
||||
<h1 class="text-4xl font-bold text-gray-800 mb-8">Tailwind CSS 示例</h1>
|
||||
|
||||
<!-- 卡片示例 -->
|
||||
<div class="max-w-md w-full bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 mb-4">卡片组件</h2>
|
||||
<p class="text-gray-600 mb-4">这是一个使用 Tailwind CSS 样式的卡片组件示例。</p>
|
||||
<button class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded transition-colors">
|
||||
点击按钮
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 网格布局示例 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 w-full max-w-4xl">
|
||||
<div class="bg-red-100 p-4 rounded-lg text-center">
|
||||
<div class="text-red-600 text-2xl mb-2">🎨</div>
|
||||
<h3 class="font-semibold text-red-800">设计</h3>
|
||||
<p class="text-red-600 text-sm">美观的界面设计</p>
|
||||
</div>
|
||||
<div class="bg-green-100 p-4 rounded-lg text-center">
|
||||
<div class="text-green-600 text-2xl mb-2">⚡</div>
|
||||
<h3 class="font-semibold text-green-800">性能</h3>
|
||||
<p class="text-green-600 text-sm">快速响应体验</p>
|
||||
</div>
|
||||
<div class="bg-purple-100 p-4 rounded-lg text-center">
|
||||
<div class="text-purple-600 text-2xl mb-2">🔧</div>
|
||||
<h3 class="font-semibold text-purple-800">功能</h3>
|
||||
<p class="text-purple-600 text-sm">丰富的功能特性</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 响应式按钮组 -->
|
||||
<div class="flex flex-wrap gap-2 mt-6">
|
||||
<button
|
||||
class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
主要按钮
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
次要按钮
|
||||
</button>
|
||||
<button
|
||||
class="border border-indigo-500 text-indigo-500 hover:bg-indigo-50 px-4 py-2 rounded-md text-sm font-medium transition-colors">
|
||||
边框按钮
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
|
||||
// 检查是否在Electron环境中
|
||||
const isElectron = typeof window !== 'undefined' && window.myApi;
|
||||
|
||||
onMounted(async () => {
|
||||
if (isElectron) {
|
||||
let res = await window.myApi.handleSend("liaoruiruirurirui");
|
||||
console.log(res);
|
||||
} else {
|
||||
console.log('在浏览器环境中运行,Electron API不可用');
|
||||
}
|
||||
});
|
||||
|
||||
const toMin = () => {
|
||||
if (isElectron) {
|
||||
window.myApi.windowMin();
|
||||
} else {
|
||||
console.log('最小化功能仅在Electron环境中可用');
|
||||
}
|
||||
};
|
||||
|
||||
const toBig = () => {
|
||||
if (isElectron) {
|
||||
window.myApi.windowMax();
|
||||
} else {
|
||||
console.log('最大化功能仅在Electron环境中可用');
|
||||
}
|
||||
};
|
||||
|
||||
const toClose = () => {
|
||||
if (isElectron) {
|
||||
window.myApi.windowClose();
|
||||
} else {
|
||||
console.log('关闭功能仅在Electron环境中可用');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
112
src/views/login/LOGIN-README.md
Normal file
112
src/views/login/LOGIN-README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
**产品需求文档:综合登录系统**
|
||||
|
||||
**1. 文档概述**
|
||||
|
||||
- **产品名称:** 统一认证与登录系统
|
||||
- **功能模块:** 登录页 (Login Page)
|
||||
- **文档目的:** 定义登录功能的详细需求,涵盖账号密码登录、验证码校验、二维码登录及相关的用户体验细节,确保开发实现准确无误。
|
||||
- **目标用户:** 所有需要访问系统权限的注册用户(包括新用户、老用户、遗忘密码用户)。
|
||||
- **核心目标:**
|
||||
1. **安全可靠:** 保障用户账号安全,防御机器攻击。
|
||||
2. **便捷高效:** 提供多种登录方式,减少用户操作成本。
|
||||
3. **清晰引导:** 界面文案清晰,对各类情况给予明确反馈和引导。
|
||||
|
||||
**2. 功能需求详述**
|
||||
|
||||
**2.1. 账号密码登录(主流程)**
|
||||
|
||||
- **输入字段:**
|
||||
|
||||
- **用户名/邮箱/手机号:**
|
||||
- 类型:单行输入框。
|
||||
- 提示文案(Placeholder): “请输入用户名/邮箱/手机号”。
|
||||
- 规则:支持三者之一即可,后端需做兼容判断。前端可做初步格式校验(如邮箱含'@',手机号为 11 位数字),但最终有效性由后端验证。
|
||||
- **密码:**
|
||||
- 类型:密码输入框(内容默认隐藏)。
|
||||
- 提示文案:“请输入密码”。
|
||||
- 功能:右侧需提供“眼睛”图标,点击可切换明文/密文显示。
|
||||
- **图形验证码:**
|
||||
- 显示逻辑:非首次输入时触发。为了提高体验,可在用户密码输入框失焦(blur)后、或连续输错 1 次密码后,再出现验证码输入框。
|
||||
- 组件:一个输入框 + 一个实时图形验证码图片。
|
||||
- 图片:需提供“刷新”图标,点击可无刷新切换新的验证码。图片应清晰可辨,但具备一定的抗机器识别能力。
|
||||
- 提示文案:“请输入验证码”。
|
||||
|
||||
- **辅助功能:**
|
||||
|
||||
- **记住我:**
|
||||
- 组件:复选框(Checkbox)。
|
||||
- 功能:勾选后,下次访问登录页时自动填充用户名(切勿记住密码)。
|
||||
- 默认状态:不勾选。
|
||||
- **忘记密码:**
|
||||
- 组件:文字链接。
|
||||
- 位置:位于密码框右侧或下方。
|
||||
- 功能:点击后跳转至密码找回流程页。
|
||||
|
||||
- **主要操作按钮:**
|
||||
- **“登录”按钮:**
|
||||
- 状态:
|
||||
- 默认态:可点击。
|
||||
- 加载态:用户点击后,按钮变为禁用状态并显示“登录中...”加载动画,防止重复提交。
|
||||
- 功能:触发登录逻辑。
|
||||
|
||||
**2.2. 二维码登录(便捷流程)**
|
||||
|
||||
- **切换入口:**
|
||||
|
||||
- 在登录框顶部或侧边提供 Tab 页或文字链接,如“账号密码登录” | “二维码登录”,用于切换两种登录方式。
|
||||
|
||||
- **二维码显示区域:**
|
||||
|
||||
- 初始状态:页面应动态生成一个唯一的二维码(QR Code),并配有文案提示:“请使用【APP 名称】扫描二维码登录”。
|
||||
- 自动刷新:该二维码应有时效性(例如 2 分钟)。倒计时在二维码旁可视化显示(如:01:45)。过期后自动刷新生成新的二维码。
|
||||
- 手动刷新:提供“刷新”按钮,用户可手动点击更换二维码。
|
||||
|
||||
- **登录状态轮询:**
|
||||
- 一旦二维码生成,前端即开始向后台轮询(Polling)其状态(如每 2 秒一次)。
|
||||
- 状态反馈:
|
||||
- 待扫描:显示初始状态。
|
||||
- 已扫描,待确认:二维码仍显示,下方文案变为“请在手机上确认登录”。
|
||||
- 登录成功:轮询停止,前端跳转至登录后页面(或首页)。
|
||||
- 过期/失败:二维码区域变灰,显示“二维码已失效”,并自动刷新生成新的二维码。
|
||||
|
||||
**2.3. 异常与交互流程**
|
||||
|
||||
- **输入校验(前端 + 后端):**
|
||||
|
||||
- 字段为空:点击登录后,对未填写的必填字段进行红色高亮提示,并显示文案“XXX 不能为空”。
|
||||
- 格式错误:在输入框失焦(blur)时即可进行初步校验(如邮箱格式错误),即时提示“邮箱格式不正确”。
|
||||
- 验证码错误:提示“验证码错误”,并自动刷新验证码图片。
|
||||
- 账号或密码错误:切勿明确提示是“账号错误”还是“密码错误”,统一提示:“用户名或密码错误”,以防黑客枚举用户名。同时,刷新验证码。
|
||||
|
||||
- **安全限制:**
|
||||
|
||||
- 连续输错密码 N 次(如 5 次)后,除图形验证码外,应触发更严格的验证(如滑块验证、短信验证码)或直接锁定账号一段时间(如 15 分钟),并明确提示用户:“密码错误次数过多,请 15 分钟后再试”。
|
||||
|
||||
- **加载与反馈:**
|
||||
- 任何网络请求都必须有加载状态(如按钮 Loading),防止用户重复操作。
|
||||
- 所有错误提示都应清晰、友好,用红色字体在表单项附近或页面顶部固定区域显示。
|
||||
|
||||
**3. 用户体验(UX)重点**
|
||||
|
||||
1. **默认焦点:** 页面加载后,自动将光标聚焦到“用户名”输入框。
|
||||
2. **键盘操作:** 在最后一项输入框(验证码或密码)按`Enter`键应触发登录操作。
|
||||
3. **密码可见性:** “眼睛”图标是行业标准,显著降低用户输错密码的概率。
|
||||
4. **智能验证码:** 不要一开始就展示验证码,而是在系统怀疑有风险(如输错一次)时再出现,优化善良用户的体验。
|
||||
5. **状态反馈:** 二维码登录的每种状态(待扫描、已确认、已过期)都必须有明确的视觉和文案反馈,让用户知其所以然。
|
||||
6. **链路闭环:** “忘记密码”和“注册”入口必须清晰可见,为登录失败的用户提供清晰的出路。
|
||||
|
||||
**4. 非功能性需求**
|
||||
|
||||
1. **性能:** 页面加载速度快,登录接口响应时间应小于 500ms。
|
||||
2. **安全性:**
|
||||
- 密码传输必须使用 HTTPS 并加密(如 SHA256 加盐)。
|
||||
- 防暴力破解:后端需实施限流策略。
|
||||
- Token 管理:登录成功后颁发的 Token 需有有效期并支持刷新机制。
|
||||
3. **兼容性:** 支持主流浏览器(Chrome, Firefox, Safari, Edge)及移动端浏览器。
|
||||
|
||||
**5. 输出物建议**
|
||||
|
||||
1. **PRD 文档:** 即本文档,用于详细阐述逻辑和规则。
|
||||
2. **线框图(Wireframe):** 绘制登录页的布局,标注各元素和交互点。
|
||||
3. **高保真原型(High-Fidelity Mockup):** 由 UI 设计师输出,明确视觉样式、颜色、字体等。
|
||||
4. **交互原型(Interactive Prototype):** 使用 Figma、Axure 等工具制作可点击的原型,演示登录成功、失败、切换等所有流程。
|
||||
295
src/views/login/index.vue
Normal file
295
src/views/login/index.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-blue-50 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- 登录卡片 -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||
<!-- 头部 -->
|
||||
<div class="text-center mb-8">
|
||||
<div
|
||||
class="w-16 h-16 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center">
|
||||
<div class="w-4 h-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">欢迎来到 Fellou</h1>
|
||||
<p class="text-gray-500 text-sm">您的隐私对我们很重要,我们确保您的数据安全可靠</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录方式切换 -->
|
||||
<div class="flex mb-6">
|
||||
<button @click="loginType = 'code'" :class="[
|
||||
'flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors',
|
||||
loginType === 'code'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-500 border-transparent hover:text-gray-700'
|
||||
]">
|
||||
验证码登录
|
||||
</button>
|
||||
<button @click="loginType = 'password'" :class="[
|
||||
'flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors',
|
||||
loginType === 'password'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-500 border-transparent hover:text-gray-700'
|
||||
]">
|
||||
密码登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin"
|
||||
class="space-y-4">
|
||||
<!-- 邮箱输入框 -->
|
||||
<el-form-item prop="email" class="mb-4">
|
||||
<el-input v-model="loginForm.email" type="email" placeholder="请输入邮箱" size="large" class="w-full"
|
||||
:prefix-icon="User" @blur="validateField('email')" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 验证码登录 -->
|
||||
<div v-if="loginType === 'code'" class="space-y-4">
|
||||
<el-form-item prop="verifyCode" class="mb-4">
|
||||
<div class="flex space-x-2">
|
||||
<el-input v-model="loginForm.verifyCode" placeholder="请输入4位验证码" size="large" class="flex-1"
|
||||
:prefix-icon="Shield" maxlength="4" @blur="validateField('verifyCode')" />
|
||||
<el-button type="primary" size="large" :disabled="!isEmailValid || countdown > 0"
|
||||
@click="sendVerifyCode" class="px-6">
|
||||
{{ countdown > 0 ? `${countdown}s` : '发送验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 密码登录 -->
|
||||
<div v-if="loginType === 'password'">
|
||||
<el-form-item prop="password" class="mb-4">
|
||||
<el-input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" placeholder="请输入密码"
|
||||
size="large" class="w-full" :prefix-icon="Lock" @blur="validateField('password')">
|
||||
<template #suffix>
|
||||
<el-icon @click="showPassword = !showPassword"
|
||||
class="cursor-pointer text-gray-400 hover:text-gray-600">
|
||||
<View v-if="showPassword" />
|
||||
<Hide v-else />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 忘记密码 -->
|
||||
<div class="text-right mb-4">
|
||||
<el-link type="primary" :underline="false" @click="handleForgotPassword">
|
||||
忘记密码?
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 协议同意 -->
|
||||
<el-form-item prop="agreed" class="mb-6">
|
||||
<el-checkbox v-model="loginForm.agreed" @change="validateField('agreed')">
|
||||
<span class="text-sm text-gray-600">
|
||||
我已阅读并同意 Fellou
|
||||
<el-link type="primary" :underline="false" @click="handlePrivacyPolicy">隐私政策</el-link>
|
||||
和
|
||||
<el-link type="primary" :underline="false" @click="handleServiceTerms">平台服务条款</el-link>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="handleLogin"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-blue-600 border-0 hover:from-purple-700 hover:to-blue-700">
|
||||
{{ loading ? '登录中...' : '注册 / 登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Shield, View, Hide } from '@element-plus/icons-vue'
|
||||
|
||||
// 登录类型
|
||||
const loginType = ref('code')
|
||||
|
||||
// 表单引用
|
||||
const loginFormRef = ref()
|
||||
|
||||
// 表单数据
|
||||
const loginForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
verifyCode: '',
|
||||
agreed: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules = reactive({
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
],
|
||||
verifyCode: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 4, message: '验证码必须为4位', trigger: 'blur' }
|
||||
],
|
||||
agreed: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请同意隐私政策和服务条款'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const countdown = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const isEmailValid = computed(() => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(loginForm.email)
|
||||
})
|
||||
|
||||
// 验证码倒计时
|
||||
const startCountdown = () => {
|
||||
countdown.value = 60
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const sendVerifyCode = async () => {
|
||||
if (!isEmailValid.value) {
|
||||
ElMessage.error('请先输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里调用发送验证码的API
|
||||
ElMessage.success('验证码已发送到您的邮箱')
|
||||
startCountdown()
|
||||
} catch (error) {
|
||||
ElMessage.error('验证码发送失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证单个字段
|
||||
const validateField = (field) => {
|
||||
loginFormRef.value?.validateField(field)
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
const valid = await loginFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 根据登录类型构建请求数据
|
||||
const loginData = {
|
||||
email: loginForm.email,
|
||||
agreed: loginForm.agreed
|
||||
}
|
||||
|
||||
if (loginType.value === 'password') {
|
||||
loginData.password = loginForm.password
|
||||
} else {
|
||||
loginData.verifyCode = loginForm.verifyCode
|
||||
}
|
||||
|
||||
// 这里调用登录API
|
||||
console.log('登录数据:', loginData)
|
||||
|
||||
// 模拟登录请求
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
|
||||
// 登录成功后的跳转逻辑
|
||||
// router.push('/home')
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
ElMessage.error('登录失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码
|
||||
const handleForgotPassword = () => {
|
||||
ElMessage.info('忘记密码功能开发中')
|
||||
}
|
||||
|
||||
// 隐私政策
|
||||
const handlePrivacyPolicy = () => {
|
||||
ElMessage.info('隐私政策页面开发中')
|
||||
}
|
||||
|
||||
// 服务条款
|
||||
const handleServiceTerms = () => {
|
||||
ElMessage.info('服务条款页面开发中')
|
||||
}
|
||||
|
||||
// 页面加载时自动聚焦到邮箱输入框
|
||||
onMounted(() => {
|
||||
// 可以在这里添加自动聚焦逻辑
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Element Plus 样式覆盖 */
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 1px #e5e7eb;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper:hover) {
|
||||
box-shadow: 0 0 0 1px #d1d5db;
|
||||
}
|
||||
|
||||
:deep(.el-input.is-focus .el-input__wrapper) {
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 自定义渐变按钮样式 */
|
||||
.el-button--primary.bg-gradient-to-r {
|
||||
background: linear-gradient(to right, #9333ea, #2563eb) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.el-button--primary.bg-gradient-to-r:hover {
|
||||
background: linear-gradient(to right, #7c3aed, #1d4ed8) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user