feat: 新增组件

This commit is contained in:
DEV_DSW
2025-09-23 16:59:32 +08:00
parent 3c4f14be7f
commit c77c5350aa
14 changed files with 739 additions and 155 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View 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
View 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>

View 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
View 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>