save
This commit is contained in:
95
src/apis/auth.ts
Normal file
95
src/apis/auth.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import http from '@/utils/http.ts'
|
||||
// 用户登录接口
|
||||
export interface LoginParams {
|
||||
email?: string
|
||||
phone?: string
|
||||
password?: string
|
||||
code?: string
|
||||
type: 'email' | 'phone' | 'wechat'
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: {
|
||||
id: string
|
||||
email?: string
|
||||
phone?: string
|
||||
nickname?: string
|
||||
avatar?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 用户注册接口
|
||||
export interface RegisterParams {
|
||||
email?: string
|
||||
phone: string
|
||||
password: string
|
||||
code: string
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
token: string
|
||||
user: {
|
||||
id: string
|
||||
email?: string
|
||||
phone: string
|
||||
nickname?: string
|
||||
avatar?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 发送验证码接口
|
||||
export interface SendCodeParams {
|
||||
email?: string
|
||||
phone: string
|
||||
type: 'login' | 'register' | 'reset_password'
|
||||
}
|
||||
|
||||
// 重置密码接口
|
||||
export interface ResetPasswordParams {
|
||||
email?: string
|
||||
phone: string
|
||||
code: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
// 微信登录接口
|
||||
export interface WechatLoginParams {
|
||||
code: string
|
||||
state?: string
|
||||
}
|
||||
|
||||
// API 函数定义
|
||||
export function login(params: LoginParams) {
|
||||
return http<LoginResponse>('auth/login', params, null, 'post')
|
||||
}
|
||||
|
||||
export function register(params: RegisterParams) {
|
||||
return http<RegisterResponse>('auth/register', params, null, 'post')
|
||||
}
|
||||
|
||||
export function sendCode(params: SendCodeParams) {
|
||||
return http<boolean>('auth/sendCode', params, null, 'post')
|
||||
}
|
||||
|
||||
export function resetPassword(params: ResetPasswordParams) {
|
||||
return http<boolean>('auth/resetPassword', params, null, 'post')
|
||||
}
|
||||
|
||||
export function wechatLogin(params: WechatLoginParams) {
|
||||
return http<LoginResponse>('auth/wechatLogin', params, null, 'post')
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return http<boolean>('auth/logout', null, null, 'post')
|
||||
}
|
||||
|
||||
export function refreshToken() {
|
||||
return http<{ token: string }>('auth/refreshToken', null, null, 'post')
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return http<LoginResponse['user']>('auth/userInfo', null, null, 'get')
|
||||
}
|
||||
@@ -59,3 +59,6 @@ export function uploadImportData(data,onUploadProgress) {
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
|
||||
// 导出认证相关API
|
||||
export * from './auth'
|
||||
|
||||
@@ -219,6 +219,9 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cp{
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -21,6 +21,11 @@ const props = defineProps({
|
||||
default: false,
|
||||
},
|
||||
maxLength: Number,
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal',
|
||||
validator: (value: string) => ['normal', 'large'].includes(value)
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'validation']);
|
||||
@@ -96,7 +101,7 @@ const vFocus = {
|
||||
<template>
|
||||
<div class="base-input2"
|
||||
ref="inputEl"
|
||||
:class="{ 'is-disabled': disabled, 'has-error': errorMsg,focus }">
|
||||
:class="{ 'is-disabled': disabled, 'has-error': errorMsg,focus, [`base-input2--${size}`]: true }">
|
||||
<slot name="subfix"></slot>
|
||||
<input
|
||||
v-bind="attrs"
|
||||
@@ -134,6 +139,26 @@ const vFocus = {
|
||||
align-items: center;
|
||||
background: var(--color-input-bg);
|
||||
|
||||
// normal size (default)
|
||||
&--normal {
|
||||
padding: .2rem .3rem;
|
||||
|
||||
.inner {
|
||||
height: 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// large size
|
||||
&--large {
|
||||
padding: .6rem .8rem;
|
||||
|
||||
.inner {
|
||||
height: 2rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
52
src/config/auth.ts
Normal file
52
src/config/auth.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// 微信登录配置
|
||||
export const WECHAT_CONFIG = {
|
||||
// 微信开放平台AppID(需要在微信开放平台申请)
|
||||
appId: 'your_wechat_app_id',
|
||||
|
||||
// 微信授权回调地址
|
||||
redirectUri: `${window.location.origin}/wechat/callback`,
|
||||
|
||||
// 授权作用域
|
||||
scope: 'snsapi_userinfo',
|
||||
|
||||
// 授权状态参数
|
||||
state: 'wechat_login'
|
||||
}
|
||||
|
||||
// 获取微信授权URL
|
||||
export function getWechatAuthUrl(state?: string): string {
|
||||
const {appId, redirectUri, scope} = WECHAT_CONFIG
|
||||
const authState = state || Math.random().toString(36).substr(2, 15)
|
||||
|
||||
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${authState}#wechat_redirect`
|
||||
}
|
||||
|
||||
// 手机号验证配置
|
||||
export const PHONE_CONFIG = {
|
||||
// 验证码长度
|
||||
codeLength: 6,
|
||||
|
||||
// 验证码发送间隔(秒)
|
||||
sendInterval: 60,
|
||||
|
||||
// 手机号正则表达式(中国大陆)
|
||||
phoneRegex: /^1[3-9]\d{9}$/
|
||||
}
|
||||
|
||||
// 邮箱配置
|
||||
export const EMAIL_CONFIG = {
|
||||
// 邮箱正则表达式
|
||||
emailRegex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
|
||||
// 邮箱验证码长度
|
||||
codeLength: 6
|
||||
}
|
||||
|
||||
// 密码配置
|
||||
export const PASSWORD_CONFIG = {
|
||||
// 密码最小长度
|
||||
minLength: 6,
|
||||
|
||||
// 密码最大长度
|
||||
maxLength: 20
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import './types/global.d.ts'
|
||||
import loadingDirective from './directives/loading.tsx'
|
||||
import { useAuthStore } from './stores/auth.ts'
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
@@ -22,7 +23,11 @@ app.directive('opacity', (el, binding) => {
|
||||
})
|
||||
app.directive('loading', loadingDirective)
|
||||
|
||||
app.mount('#app')
|
||||
// 初始化认证状态
|
||||
const authStore = useAuthStore()
|
||||
authStore.initAuth().then(() => {
|
||||
app.mount('#app')
|
||||
})
|
||||
|
||||
// 注册Service Worker(pwa支持)
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import Textarea from "@/components/base/Textarea.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { get, set } from "idb-keyval";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useAuthStore } from "@/stores/auth.ts";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
@@ -40,6 +41,7 @@ const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const store = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
@@ -773,6 +775,29 @@ function importOldData() {
|
||||
|
||||
<div v-if="tabIndex === 6" class="center flex-col">
|
||||
<h1>Type Words</h1>
|
||||
|
||||
<!-- 用户信息部分 -->
|
||||
<div v-if="authStore.isLoggedIn && authStore.user" class="user-info-section mb-6">
|
||||
<div class="user-avatar mb-4">
|
||||
<img v-if="authStore.user.avatar" :src="authStore.user.avatar" alt="头像" class="avatar-img" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ authStore.user.nickname?.charAt(0) || 'U' }}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mb-2">{{ authStore.user.nickname || '用户' }}</h3>
|
||||
<p v-if="authStore.user.email" class="text-sm color-gray mb-1">{{ authStore.user.email }}</p>
|
||||
<p v-if="authStore.user.phone" class="text-sm color-gray">{{ authStore.user.phone }}</p>
|
||||
|
||||
<BaseButton
|
||||
@click="authStore.logout"
|
||||
type="info"
|
||||
class="mt-4"
|
||||
:loading="authStore.isLoading"
|
||||
>
|
||||
退出登录
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<p class="w-100 text-xl">
|
||||
感谢使用本项目!本项目是开源项目,如果觉得有帮助,请在 GitHub 点个 Star,您的支持是我持续改进的动力。
|
||||
</p>
|
||||
@@ -803,6 +828,75 @@ function importOldData() {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
// 用户信息样式
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
||||
.user-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--color-select-bg);
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.color-gray {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
@apply text-lg;
|
||||
display: flex;
|
||||
|
||||
@@ -1,29 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
|
||||
import { onMounted } from "vue";
|
||||
import { IS_LOGIN } from "@/config/env.ts";
|
||||
import router from "@/router.ts";
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 页面状态
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
await authStore.logout()
|
||||
}
|
||||
|
||||
// 跳转到设置页面
|
||||
const goToSettings = () => {
|
||||
router.push('/setting')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!IS_LOGIN) {
|
||||
// 如果用户未登录,跳转到登录页
|
||||
if (!authStore.isLoggedIn) {
|
||||
router.push({path: "/login"});
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
if (!authStore.user) {
|
||||
authStore.fetchUserInfo()
|
||||
}
|
||||
router.push({path: "/login"});
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between min-h-screen">
|
||||
<div class="center flex-col gap-8">
|
||||
onMounted(() => {
|
||||
if (!IS_LOGIN) {
|
||||
router.push({path: "/login"});
|
||||
}
|
||||
})
|
||||
<div class="user-center">
|
||||
<div class="user-header">
|
||||
<div class="avatar">
|
||||
<img v-if="authStore.user?.avatar" :src="authStore.user.avatar" alt="头像" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ authStore.user?.nickname?.charAt(0) || 'U' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h2>{{ authStore.user?.nickname || '用户' }}</h2>
|
||||
<p v-if="authStore.user?.email">{{ authStore.user.email }}</p>
|
||||
<p v-if="authStore.user?.phone">{{ authStore.user.phone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-actions">
|
||||
<BaseButton @click="goToSettings" class="w-full mb-4" size="large">
|
||||
系统设置
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
@click="handleLogout"
|
||||
type="info"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
>
|
||||
退出登录
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-center {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--color-select-bg);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,11 +1,271 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import { uploadImportData } from "@/apis";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import {useAuthStore} from "@/stores/auth.ts";
|
||||
import {sendCode, register as registerApi, resetPassword, uploadImportData} from "@/apis";
|
||||
import {validateLoginForm, validateRegisterForm, validateResetPasswordForm} from "@/utils/validation.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import { getWechatAuthUrl, PHONE_CONFIG } from "@/config/auth.ts";
|
||||
|
||||
function sync() {
|
||||
// 状态管理
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 页面状态
|
||||
let currentMode = $ref<'login' | 'register' | 'forgot'>('login')
|
||||
let loginType = $ref<'code' | 'password'>('code') // 默认验证码登录
|
||||
let isLoading = $ref(false)
|
||||
let isSendingCode = $ref(false)
|
||||
let codeCountdown = $ref(0)
|
||||
let showWechatQR = $ref(false)
|
||||
let wechatQRUrl = $ref('')
|
||||
|
||||
// 表单数据
|
||||
const loginForm = $ref({
|
||||
account: '', // 支持邮箱或手机号
|
||||
password: ''
|
||||
})
|
||||
|
||||
const phoneLoginForm = $ref({
|
||||
phone: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const registerForm = $ref({
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const forgotForm = $ref({
|
||||
phone: '',
|
||||
email: '',
|
||||
code: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 错误信息
|
||||
const loginErrors = ref<Record<string, string>>({})
|
||||
const registerErrors = ref<Record<string, string>>({})
|
||||
const forgotErrors = ref<Record<string, string>>({})
|
||||
|
||||
// 发送验证码
|
||||
async function sendVerificationCode(phone: string, type: 'login' | 'register' | 'reset_password') {
|
||||
if (!phone) {
|
||||
Toast.error('请输入手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!PHONE_CONFIG.phoneRegex.test(phone)) {
|
||||
Toast.error('请输入有效的手机号')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSendingCode = true
|
||||
const response = await sendCode({ phone, type })
|
||||
|
||||
if (response.success) {
|
||||
Toast.success('验证码已发送')
|
||||
codeCountdown = PHONE_CONFIG.sendInterval
|
||||
const timer = setInterval(() => {
|
||||
codeCountdown--
|
||||
if (codeCountdown <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
Toast.error(response.msg || '发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Send code error:', error)
|
||||
Toast.error('发送验证码失败')
|
||||
} finally {
|
||||
isSendingCode = false
|
||||
}
|
||||
}
|
||||
|
||||
// 账号密码登录
|
||||
async function handleLogin() {
|
||||
// 判断是邮箱还是手机号
|
||||
const isEmail = loginForm.account.includes('@')
|
||||
const validation = validateLoginForm({
|
||||
email: isEmail ? loginForm.account : undefined,
|
||||
phone: !isEmail ? loginForm.account : undefined,
|
||||
password: loginForm.password,
|
||||
code: undefined,
|
||||
type: 'email'
|
||||
})
|
||||
|
||||
if (!validation.isValid) {
|
||||
Object.assign(loginErrors, validation.errors)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
await authStore.login({
|
||||
email: isEmail ? loginForm.account : undefined,
|
||||
phone: !isEmail ? loginForm.account : undefined,
|
||||
password: loginForm.password,
|
||||
type: 'email'
|
||||
})
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 手机号验证码登录
|
||||
async function handlePhoneCodeLogin() {
|
||||
const validation = validateLoginForm({
|
||||
phone: phoneLoginForm.phone,
|
||||
code: phoneLoginForm.code,
|
||||
password: undefined,
|
||||
type: 'phone'
|
||||
})
|
||||
|
||||
if (!validation.isValid) {
|
||||
Object.assign(loginErrors, validation.errors)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
await authStore.login({
|
||||
phone: phoneLoginForm.phone,
|
||||
code: phoneLoginForm.code,
|
||||
type: 'phone'
|
||||
})
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
async function handleRegister() {
|
||||
if (registerForm.password !== registerForm.confirmPassword) {
|
||||
Toast.error('两次密码输入不一致')
|
||||
return
|
||||
}
|
||||
|
||||
const validation = validateRegisterForm({
|
||||
phone: registerForm.phone,
|
||||
password: registerForm.password,
|
||||
code: registerForm.code,
|
||||
nickname: undefined,
|
||||
email: undefined
|
||||
})
|
||||
|
||||
if (!validation.isValid) {
|
||||
Object.assign(registerErrors, validation.errors)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
await authStore.register({
|
||||
phone: registerForm.phone,
|
||||
password: registerForm.password,
|
||||
code: registerForm.code,
|
||||
nickname: undefined,
|
||||
email: undefined
|
||||
})
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码
|
||||
async function handleForgotPassword() {
|
||||
if (forgotForm.newPassword !== forgotForm.confirmPassword) {
|
||||
Toast.error('两次密码输入不一致')
|
||||
return
|
||||
}
|
||||
|
||||
const validation = validateResetPasswordForm({
|
||||
phone: forgotForm.phone,
|
||||
email: forgotForm.email,
|
||||
code: forgotForm.code,
|
||||
newPassword: forgotForm.newPassword
|
||||
})
|
||||
|
||||
if (!validation.isValid) {
|
||||
Object.assign(forgotErrors, validation.errors)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
const response = await authStore.resetPassword({
|
||||
phone: forgotForm.phone,
|
||||
email: forgotForm.email,
|
||||
code: forgotForm.code,
|
||||
newPassword: forgotForm.newPassword
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
Toast.success('密码重置成功,请重新登录')
|
||||
switchMode('login')
|
||||
} else {
|
||||
Toast.error(response.msg || '重置失败')
|
||||
}
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录 - 显示二维码
|
||||
async function handleWechatLogin() {
|
||||
try {
|
||||
showWechatQR = true
|
||||
// 这里应该调用后端获取二维码
|
||||
// const response = await getWechatQR()
|
||||
// wechatQRUrl = response.qrUrl
|
||||
|
||||
// 暂时使用占位二维码
|
||||
wechatQRUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OTk5OSI+55So5o6l566h55CG6L295Lit6K+B77yBPC90ZXh0Pgo8L3N2Zz4K'
|
||||
|
||||
// 模拟轮询检查扫码状态
|
||||
const checkInterval = setInterval(async () => {
|
||||
// 这里应该轮询后端检查扫码状态
|
||||
// const result = await checkWechatLoginStatus()
|
||||
// if (result.success) {
|
||||
// clearInterval(checkInterval)
|
||||
// showWechatQR = false
|
||||
// // 登录成功处理
|
||||
// }
|
||||
}, 2000)
|
||||
|
||||
// 5分钟后自动关闭二维码
|
||||
setTimeout(() => {
|
||||
clearInterval(checkInterval)
|
||||
showWechatQR = false
|
||||
Toast.info('二维码已过期,请重新获取')
|
||||
}, 300000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Wechat login error:', error)
|
||||
Toast.error('微信登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换模式
|
||||
function switchMode(mode: 'login' | 'register' | 'forgot') {
|
||||
currentMode = mode
|
||||
clearErrors()
|
||||
}
|
||||
|
||||
// 清除错误
|
||||
function clearErrors() {
|
||||
loginErrors.value = {}
|
||||
registerErrors.value = {}
|
||||
forgotErrors.value = {}
|
||||
}
|
||||
|
||||
async function handleAudioChange(e) {
|
||||
@@ -30,37 +290,530 @@ async function s() {
|
||||
if (res.progress >= 100) clearInterval(timer);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 初始化页面
|
||||
onMounted(() => {
|
||||
// 检查是否有重定向地址
|
||||
const redirect = route.query.redirect as string
|
||||
if (redirect) {
|
||||
// 如果有重定向地址,可以显示提示信息
|
||||
Toast.info('请先登录后再访问该页面')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center h-screen">
|
||||
<div class=" flex flex-col gap-6 w-100">
|
||||
<div class="login-container">
|
||||
<!-- 左侧登录区域 -->
|
||||
<div class="login-section">
|
||||
<h1 class="mb-0 text-align-center">{{ APP_NAME }}</h1>
|
||||
<div class="flex center">
|
||||
<span class="shrink-0">账户:</span>
|
||||
<BaseInput type="text"/>
|
||||
</div>
|
||||
<div class="flex center">
|
||||
<span class="shrink-0">密码:</span>
|
||||
<BaseInput type="password"/>
|
||||
</div>
|
||||
<BaseButton class="w-full">登录</BaseButton>
|
||||
<BaseButton class="w-full" @click="sync">同步</BaseButton>
|
||||
<div class="upload relative">
|
||||
<BaseButton>上传</BaseButton>
|
||||
<input type="file"
|
||||
accept=".zip,.json"
|
||||
@change="handleAudioChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
<span class="agreement-text">继续操作即表示你同意我们的 <a href="" class="color-link">用户协议</a> 并确认已了解 <a href="" class="color-link">隐私政策</a>。</span>
|
||||
|
||||
<!-- 登录选项 -->
|
||||
<div v-if="currentMode === 'login' && !showWechatQR" class="login-options">
|
||||
<!-- Tab切换 -->
|
||||
<div class="login-tabs">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: loginType === 'code' }"
|
||||
@click="loginType = 'code'"
|
||||
>
|
||||
验证码登录
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: loginType === 'password' }"
|
||||
@click="loginType = 'password'"
|
||||
>
|
||||
密码登录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码登录表单 -->
|
||||
<template v-if="loginType === 'code'">
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="phoneLoginForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
:class="{ 'has-error': loginErrors.phone }"
|
||||
/>
|
||||
<div v-if="loginErrors.phone" class="error-text">{{ loginErrors.phone }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group flex gap-2">
|
||||
<div class="flex-1">
|
||||
<BaseInput
|
||||
v-model="phoneLoginForm.code"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:class="{ 'has-error': loginErrors.code }"
|
||||
/>
|
||||
<div v-if="loginErrors.code" class="error-text">{{ loginErrors.code }}</div>
|
||||
</div>
|
||||
<BaseButton
|
||||
@click="sendVerificationCode(phoneLoginForm.phone, 'login')"
|
||||
:disabled="isSendingCode || codeCountdown > 0"
|
||||
type="info"
|
||||
size="large"
|
||||
class="send-code-btn"
|
||||
>
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
@click="handlePhoneCodeLogin"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<!-- 密码登录表单 -->
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="loginForm.account"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入邮箱或手机号"
|
||||
:class="{ 'has-error': loginErrors.email || loginErrors.phone }"
|
||||
/>
|
||||
<div v-if="loginErrors.email" class="error-text">{{ loginErrors.email }}</div>
|
||||
<div v-if="loginErrors.phone" class="error-text">{{ loginErrors.phone }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
:class="{ 'has-error': loginErrors.password }"
|
||||
/>
|
||||
<div v-if="loginErrors.password" class="error-text">{{ loginErrors.password }}</div>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<!-- 底部操作链接 -->
|
||||
<div class="bottom-actions">
|
||||
<div class="color-link cp" @click="currentMode = 'register'">注册账号</div>
|
||||
<div class="color-link cp" @click="currentMode = 'forgot'">忘记密码?</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-end gap-4">
|
||||
<div>注册</div>
|
||||
<div>忘记密码</div>
|
||||
<!-- 注册模式 -->
|
||||
<template v-else-if="currentMode === 'register'" class="register-form">
|
||||
<h2 class="form-title">注册新账号</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="registerForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
:class="{ 'has-error': registerErrors.phone }"
|
||||
/>
|
||||
<div v-if="registerErrors.phone" class="error-text">{{ registerErrors.phone }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group flex gap-2">
|
||||
<div class="flex-1">
|
||||
<BaseInput
|
||||
v-model="registerForm.code"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:class="{ 'has-error': registerErrors.code }"
|
||||
/>
|
||||
<div v-if="registerErrors.code" class="error-text">{{ registerErrors.code }}</div>
|
||||
</div>
|
||||
<BaseButton
|
||||
@click="sendVerificationCode(registerForm.phone, 'register')"
|
||||
:disabled="isSendingCode || codeCountdown > 0"
|
||||
type="info"
|
||||
size="large"
|
||||
class="send-code-btn"
|
||||
>
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请设置密码(6-20位)"
|
||||
:class="{ 'has-error': registerErrors.password }"
|
||||
/>
|
||||
<div v-if="registerErrors.password" class="error-text">{{ registerErrors.password }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册
|
||||
</BaseButton>
|
||||
|
||||
<div class="back-link">
|
||||
<div class="color-link cp" @click="switchMode('login')">返回登录</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 忘记密码模式 -->
|
||||
<template v-else-if="currentMode === 'forgot'" class="forgot-form">
|
||||
<h2 class="form-title">重置密码</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="forgotForm.phone"
|
||||
type="tel"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
:class="{ 'has-error': forgotErrors.phone }"
|
||||
/>
|
||||
<div v-if="forgotErrors.phone" class="error-text">{{ forgotErrors.phone }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group flex gap-2">
|
||||
<div class="flex-1">
|
||||
<BaseInput
|
||||
v-model="forgotForm.code"
|
||||
type="text"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:class="{ 'has-error': forgotErrors.code }"
|
||||
/>
|
||||
<div v-if="forgotErrors.code" class="error-text">{{ forgotErrors.code }}</div>
|
||||
</div>
|
||||
<BaseButton
|
||||
@click="sendVerificationCode(forgotForm.phone, 'reset_password')"
|
||||
:disabled="isSendingCode || codeCountdown > 0"
|
||||
type="info"
|
||||
size="large"
|
||||
class="send-code-btn"
|
||||
>
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入新密码(6-20位)"
|
||||
:class="{ 'has-error': forgotErrors.newPassword }"
|
||||
/>
|
||||
<div v-if="forgotErrors.newPassword" class="error-text">{{ forgotErrors.newPassword }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<BaseInput
|
||||
v-model="forgotForm.email"
|
||||
type="email"
|
||||
size="large"
|
||||
placeholder="请输入邮箱地址(选填)"
|
||||
:class="{ 'has-error': forgotErrors.email }"
|
||||
/>
|
||||
<div v-if="forgotErrors.email" class="error-text">{{ forgotErrors.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="isLoading"
|
||||
@click="handleForgotPassword"
|
||||
>
|
||||
重置密码
|
||||
</BaseButton>
|
||||
|
||||
<div class="back-link">
|
||||
<div class="color-link cp" @click="switchMode('login')">返回登录</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 右侧微信二维码 -->
|
||||
<div class="qr-section">
|
||||
<div class="qr-container">
|
||||
<div class="qr-header">
|
||||
<h3>微信扫码登录</h3>
|
||||
<div v-if="showWechatQR" class="close-btn" @click="showWechatQR = false">
|
||||
<IconFluentDismiss12Regular />
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-content">
|
||||
<img
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="qr-image active"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OTk5OSI+55So5o6l566h55CG6L295Lit6K+B77yBPC90ZXh0Pgo8L3N2Zz4K"
|
||||
alt="微信登录二维码"
|
||||
class="qr-image"
|
||||
/>
|
||||
<p class="qr-tip" v-if="!showWechatQR">请使用微信扫描二维码登录</p>
|
||||
<p class="qr-tip active" v-else>正在扫码,请稍候...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
border-radius: 2rem;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.6rem 1rem;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
svg {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-select-bg);
|
||||
background: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.line-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: #999;
|
||||
font-size: 0.9rem;
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #cfcfcf;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单组样式
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
|
||||
&.flex {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab切换样式
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
padding: 1rem 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.3s;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-select-bg);
|
||||
border-bottom: 2px solid var(--color-select-bg);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
padding: 0.8rem 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-select-bg);
|
||||
border-bottom: 3px solid var(--color-select-bg);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码按钮样式
|
||||
.send-code-btn {
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 错误文本样式
|
||||
.error-text {
|
||||
color: #f56c6c;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
padding-left: 0.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// 链接样式
|
||||
.color-link {
|
||||
color: var(--color-select-bg);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.cp {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Input错误状态覆盖
|
||||
:deep(.base-input2.has-error) {
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
// 微信二维码样式
|
||||
.wechat-qr-container {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.qr-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.qr-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qr-tip {
|
||||
margin-top: 1rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 登录容器布局 - 居中显示
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.login-section {
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,8 @@ import BookList from "@/pages/article/BookList.vue";
|
||||
import Setting from "@/pages/setting/Setting.vue";
|
||||
import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/index.vue";
|
||||
import WechatCallback from "@/pages/user/wechat-callback.vue";
|
||||
import { useAuthStore } from "@/stores/auth.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -39,7 +41,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
|
||||
{path: '/test', component: () => import("@/pages/test/test.vue")},
|
||||
{path: '/:pathMatch(.*)*', redirect: '/word'},
|
||||
{path: '/:pathMatch(.*)*', redirect: '/words'},
|
||||
]
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
@@ -56,7 +58,27 @@ const router = VueRouter.createRouter({
|
||||
},
|
||||
})
|
||||
|
||||
router.beforeEach((to: any, from: any) => {
|
||||
// 路由守卫
|
||||
router.beforeEach(async (to: any, from: any) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 公共路由,不需要登录验证
|
||||
const publicRoutes = ['/login', '/wechat/callback']
|
||||
|
||||
// 如果目标路由是公共路由,直接放行
|
||||
if (publicRoutes.includes(to.path)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果用户未登录,跳转到登录页
|
||||
if (!authStore.isLoggedIn) {
|
||||
// 尝试初始化认证状态
|
||||
const isInitialized = await authStore.initAuth()
|
||||
if (!isInitialized) {
|
||||
return { path: '/login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
// console.log('beforeEach-to',to.path)
|
||||
// console.log('beforeEach-from',from.path)
|
||||
|
||||
168
src/stores/auth.ts
Normal file
168
src/stores/auth.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { login as loginApi, register as registerApi, logout as logoutApi, getUserInfo, resetPassword as resetPasswordApi, type LoginParams, type RegisterParams } from '@/apis/auth'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import router from '@/router.ts'
|
||||
export interface User {
|
||||
id: string
|
||||
email?: string
|
||||
phone?: string
|
||||
nickname?: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string>(localStorage.getItem('token') || '')
|
||||
const user = ref<User | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
// 设置token
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
// 清除token
|
||||
const clearToken = () => {
|
||||
token.value = ''
|
||||
localStorage.removeItem('token')
|
||||
user.value = null
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
const setUser = (userInfo: User) => {
|
||||
user.value = userInfo
|
||||
}
|
||||
|
||||
// 登录
|
||||
const login = async (params: LoginParams) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await loginApi(params)
|
||||
|
||||
if (response.success && response.data) {
|
||||
setToken(response.data.token)
|
||||
setUser(response.data.user)
|
||||
Toast.success('登录成功')
|
||||
|
||||
// 跳转到首页或用户中心
|
||||
router.push('/')
|
||||
return true
|
||||
} else {
|
||||
Toast.error(response.msg || '登录失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
Toast.error('登录失败,请重试')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = async () => {
|
||||
try {
|
||||
await logoutApi()
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} finally {
|
||||
clearToken()
|
||||
Toast.success('已退出登录')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const response = await getUserInfo()
|
||||
if (response.success && response.data) {
|
||||
setUser(response.data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Get user info error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
const register = async (params: RegisterParams) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await registerApi(params)
|
||||
|
||||
if (response.success && response.data) {
|
||||
setToken(response.data.token)
|
||||
setUser(response.data.user)
|
||||
Toast.success('注册成功')
|
||||
|
||||
// 跳转到首页或用户中心
|
||||
router.push('/')
|
||||
return true
|
||||
} else {
|
||||
Toast.error(response.msg || '注册失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Register error:', error)
|
||||
Toast.error('注册失败,请重试')
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const resetPassword = async (params: { email?: string; phone: string; code: string; newPassword: string }) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await resetPasswordApi(params)
|
||||
|
||||
if (response.success) {
|
||||
Toast.success('密码重置成功')
|
||||
return { success: true, msg: '密码重置成功' }
|
||||
} else {
|
||||
return { success: false, msg: response.msg || '重置失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reset password error:', error)
|
||||
return { success: false, msg: '重置密码失败,请重试' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户状态
|
||||
const initAuth = async () => {
|
||||
if (token.value) {
|
||||
const success = await fetchUserInfo()
|
||||
if (!success) {
|
||||
clearToken()
|
||||
}
|
||||
return success
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
setToken,
|
||||
clearToken,
|
||||
setUser,
|
||||
login,
|
||||
register,
|
||||
resetPassword,
|
||||
logout,
|
||||
fetchUserInfo,
|
||||
initAuth
|
||||
}
|
||||
})
|
||||
166
src/utils/validation.ts
Normal file
166
src/utils/validation.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// 邮箱验证
|
||||
export const validateEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
// 手机号验证(中国大陆)
|
||||
export const validatePhone = (phone: string): boolean => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
export const validatePassword = (password: string): { isValid: boolean; message?: string } => {
|
||||
if (!password) {
|
||||
return { isValid: false, message: '密码不能为空' }
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return { isValid: false, message: '密码长度不能少于6位' }
|
||||
}
|
||||
if (password.length > 20) {
|
||||
return { isValid: false, message: '密码长度不能超过20位' }
|
||||
}
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
// 验证码验证
|
||||
export const validateCode = (code: string): boolean => {
|
||||
return /^\d{6}$/.test(code)
|
||||
}
|
||||
|
||||
// 用户名验证
|
||||
export const validateNickname = (nickname: string): { isValid: boolean; message?: string } => {
|
||||
if (!nickname) {
|
||||
return { isValid: true } // 昵称可以为空
|
||||
}
|
||||
if (nickname.length < 2) {
|
||||
return { isValid: false, message: '昵称长度不能少于2位' }
|
||||
}
|
||||
if (nickname.length > 20) {
|
||||
return { isValid: false, message: '昵称长度不能超过20位' }
|
||||
}
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
// 综合验证函数
|
||||
export const validateLoginForm = (data: {
|
||||
email?: string
|
||||
phone?: string
|
||||
password: string
|
||||
code?: string
|
||||
type: 'email' | 'phone' | 'wechat'
|
||||
}): { isValid: boolean; errors: Record<string, string> } => {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (data.type === 'email') {
|
||||
if (!data.email) {
|
||||
errors.email = '邮箱不能为空'
|
||||
} else if (!validateEmail(data.email)) {
|
||||
errors.email = '请输入有效的邮箱地址'
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'phone') {
|
||||
if (!data.phone) {
|
||||
errors.phone = '手机号不能为空'
|
||||
} else if (!validatePhone(data.phone)) {
|
||||
errors.phone = '请输入有效的手机号'
|
||||
}
|
||||
|
||||
if (!data.code) {
|
||||
errors.code = '验证码不能为空'
|
||||
} else if (!validateCode(data.code)) {
|
||||
errors.code = '请输入6位数字验证码'
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type !== 'phone' && !data.password) {
|
||||
errors.password = '密码不能为空'
|
||||
} else if (data.type !== 'phone' && data.password) {
|
||||
const passwordValidation = validatePassword(data.password)
|
||||
if (!passwordValidation.isValid) {
|
||||
errors.password = passwordValidation.message
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
export const validateRegisterForm = (data: {
|
||||
email?: string
|
||||
phone: string
|
||||
password: string
|
||||
code: string
|
||||
nickname?: string
|
||||
}): { isValid: boolean; errors: Record<string, string> } => {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (!data.phone) {
|
||||
errors.phone = '手机号不能为空'
|
||||
} else if (!validatePhone(data.phone)) {
|
||||
errors.phone = '请输入有效的手机号'
|
||||
}
|
||||
|
||||
if (!data.code) {
|
||||
errors.code = '验证码不能为空'
|
||||
} else if (!validateCode(data.code)) {
|
||||
errors.code = '请输入6位数字验证码'
|
||||
}
|
||||
|
||||
const passwordValidation = validatePassword(data.password)
|
||||
if (!passwordValidation.isValid) {
|
||||
errors.password = passwordValidation.message
|
||||
}
|
||||
|
||||
const nicknameValidation = validateNickname(data.nickname || '')
|
||||
if (!nicknameValidation.isValid) {
|
||||
errors.nickname = nicknameValidation.message
|
||||
}
|
||||
|
||||
if (data.email && !validateEmail(data.email)) {
|
||||
errors.email = '请输入有效的邮箱地址'
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
export const validateResetPasswordForm = (data: {
|
||||
email?: string
|
||||
phone: string
|
||||
code: string
|
||||
newPassword: string
|
||||
}): { isValid: boolean; errors: Record<string, string> } => {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (!data.phone) {
|
||||
errors.phone = '手机号不能为空'
|
||||
} else if (!validatePhone(data.phone)) {
|
||||
errors.phone = '请输入有效的手机号'
|
||||
}
|
||||
|
||||
if (!data.code) {
|
||||
errors.code = '验证码不能为空'
|
||||
} else if (!validateCode(data.code)) {
|
||||
errors.code = '请输入6位数字验证码'
|
||||
}
|
||||
|
||||
const passwordValidation = validatePassword(data.newPassword)
|
||||
if (!passwordValidation.isValid) {
|
||||
errors.newPassword = passwordValidation.message
|
||||
}
|
||||
|
||||
if (data.email && !validateEmail(data.email)) {
|
||||
errors.email = '请输入有效的邮箱地址'
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user