重新提交项目内容

This commit is contained in:
李远 2026-03-05 09:27:51 +08:00
parent e523a3ce4b
commit cd4e1d7e54
7 changed files with 0 additions and 606 deletions

42
.gitignore vendored
View File

@ -1,42 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
#auto generated files
.eslintrc-auto-import.json

View File

@ -1,43 +0,0 @@
@import './user.scss';
*{margin: 0;padding: 0;box-sizing: border-box;}
html,body{
font-size: 16px;
color: black;
}
/* 过渡动画样式name="fade" 对应前缀 fade- */
/* 1. 进入/离开的初始状态 */
.user-enter-from,
.user-leave-to {
opacity: 0;
/* 初始透明 */
transform: translateX(20px);
/* 初始右移20px */
}
/* 2. 进入/离开的过渡过程 */
.user-enter-active,
.user-leave-active {
transition: all 0.5s ease;
/* 过渡时长、缓动效果 */
}
/* 3. 进入完成/离开开始的状态 */
.user-enter-to,
.user-leave-from {
opacity: 1;
/* 最终不透明 */
transform: translateX(0);
/* 最终回到原位 */
}
// 全局通知样式
.global-notification {
left: 50%;
transform: translateX(-50%);
// 2. 重写动画从顶部淡入取消右侧滑入
&.el-notification-fade-enter-active,
&.el-notification-fade-leave-active {
transition: all 0.3s ease-out !important;
transform: translate(-50%, -150%) !important;
}
}

View File

@ -1,88 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/modules/user';
const routes = [
// 1. 补全根路径路由定义,避免通配符无限循环
{
path: '/',
name: 'Home',
component: () => import('@/views/Test/index.vue'), // 替换为你的首页组件
meta: { requiresAuth: true ,title: '首页' } // 首页需要登录
},
{
path: '/user',
name: 'User',
component: () => import('@/views/user/index.vue'),
redirect: '/user/login',
children: [
{
path: 'login',
name: 'Login',
component: () => import('@/views/user/Login.vue'),
meta: { isAuthPage: true, title: '登录' }
},
{
path: 'register',
name: 'Register',
component: () => import('@/views/user/Register.vue'),
meta: { isAuthPage: true, title: '注册' }
},
{
path: 'forgot-password',
name: 'ForgotPassword',
component: () => import('@/views/user/ForgotPassword.vue'),
meta: { isAuthPage: true, title: '忘记密码' }
},
{
path: 'change-password',
name: 'ChangePassword',
component: () => import('@/views/user/ChangePassword.vue'),
meta: { requiresAuth: true, title: '修改密码' }
}
]
},
{
path: '/:pathMatch(.*)*',
redirect: '/user/login' // 404 重定向到登录页,避免循环
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 全局前置守卫
router.beforeEach(async (to, from) => {
const userStore = useUserStore();
const isLoggedIn = !!userStore.token;
// 从修改密码页跳登录页时,直接放行(不跳首页)
const isFromChangePasswordToLogin = from.path === '/user/change-password' && to.path === '/user/login';
if (isFromChangePasswordToLogin) {
return true; // 放行,跳过后续的“已登录访问授权页跳首页”逻辑
}
// 1. 已登录用户访问授权页 → 重定向到首页
if (isLoggedIn && to.meta.isAuthPage) {
if (to.path === '/') return true;
return '/';
}
// 2. 未登录用户访问需要登录的页面(包括首页)→ 重定向到登录页
if (!isLoggedIn && to.meta.requiresAuth) {
if (to.path === '/user/login') return true;
return '/user/login';
}
// 3. 其他情况正常放行
return true;
});
// 可选:设置页面标题
router.afterEach((to) => {
if (to.meta.title) {
document.title = to.meta.title as string;
}
});
export default router;

View File

@ -1,214 +0,0 @@
import type { Ref } from 'vue';
import { setStorage, getStorage, removeStorage } from '@/utils/storage';
// 既支持指定的固定值,也支持任意字符串
export type CaptchaType = 'phone' | 'email' | string;
// 本地存储的状态类型
interface StoredCaptchaState {
type: CaptchaType; // 验证码类型
receiver: string; // 接收方(手机号/邮箱)
sendTime: number; // 发送时间戳(毫秒)
totalSeconds: number; // 总倒计时时长默认60
}
// 倒计时状态类型(供组件绑定)
export interface CaptchaCountdownState {
countdown: Ref<number>; // 剩余秒数
isCounting: Ref<boolean>; // 是否正在倒计时
timer: number | null; // 定时器标识
key: string; // 唯一标识(区分不同场景/类型的验证码register_phone
}
/**
*
* @param key register_phone/register_email/forgotPassword_phone
* @param totalSeconds 60
* @returns
*/
export const initCaptchaState = (key: string, totalSeconds: number = 60): CaptchaCountdownState => {
const state: CaptchaCountdownState = {
countdown: ref(0),
isCounting: ref(false),
timer: null,
key
};
// 页面刷新后从sessionStorage恢复状态
const stored = getStorage<StoredCaptchaState>(`captcha_${key}`, 'session');
if (stored) {
try {
const now = Date.now();
// 计算剩余时间 = 总时长 - (当前时间 - 发送时间戳)/1000
const remaining = stored.totalSeconds - Math.floor((now - stored.sendTime) / 1000);
// 剩余时间>0恢复倒计时
if (remaining > 0) {
state.countdown.value = remaining;
state.isCounting.value = true;
// 重启定时器
state.timer = window.setInterval(() => {
state.countdown.value--;
// 同步更新本地存储的剩余时间(可选,增强鲁棒性)
updateStoredCaptchaState(state.key, state.countdown.value);
if (state.countdown.value <= 0) {
clearCaptchaTimer(state); // 倒计时结束,清除状态
}
}, 1000);
} else {
// 剩余时间<=0清空本地存储
removeStorage(`captcha_${key}`, 'session');
}
} catch (e) {
console.error('恢复验证码倒计时状态失败:', e);
removeStorage(`captcha_${key}`, 'session');
}
}
return state;
};
/**
* 使
*/
const updateStoredCaptchaState = (key: string, remaining: number) => {
const stored = getStorage<StoredCaptchaState>(`captcha_${key}`, 'session');
if (stored) {
// 仅更新剩余时间(可选,核心是时间戳,剩余时间只是辅助)
setStorage(`captcha_${key}`, {
...stored,
// 可选:记录剩余时间,方便初始化时快速读取
remaining
}, 'session');
}
};
/**
* /
* @param type phone/email
* @param receiver /
* @param state initCaptchaState生成
* @param sendApi receiver
* @returns Promise<boolean>
*/
export const sendCaptcha = async (
type: CaptchaType,
receiver: string,
state: CaptchaCountdownState,
sendApi: (receiver: string) => Promise<any>
): Promise<boolean> => {
// ========== 核心:刷新后防重复发送的校验 ==========
const stored = getStorage<StoredCaptchaState>(`captcha_${state.key}`, 'session');
if (stored) {
const now = Date.now();
const elapsed = Math.floor((now - stored.sendTime) / 1000); // 已过去的秒数
// 未到冷却期60秒禁止发送
if (elapsed < stored.totalSeconds) {
const remaining = stored.totalSeconds - elapsed;
ElNotification({
message: `${remaining}秒后再发送验证码`,
type: 'warning',
duration: 2000,
customClass: 'global-notification'
});
// 恢复倒计时(防止内存状态和本地存储不一致)
state.countdown.value = remaining;
state.isCounting.value = true;
return false;
}
}
// ========== 原有校验 ==========
if (state.isCounting.value) {
ElNotification({
message: '验证码已发送,请勿重复点击',
type: 'warning',
duration: 2000,
customClass: 'global-notification'
});
return false;
}
if (!receiver) {
ElNotification({
message: `请输入${type === 'phone' ? '手机号' : '邮箱'}`,
type: 'warning',
duration: 2000,
customClass: 'global-notification'
});
return false;
}
try {
// 1. 调用真实发送接口
// await sendApi(receiver);
// 2. 发送成功提示
ElNotification({
message: `验证码已发送至${type === 'phone' ? '手机' : '邮箱'},请注意查收`,
type: 'success',
duration: 2000,
customClass: 'global-notification'
});
// 3. 记录发送状态到本地存储(核心:防刷新)
const storedState: StoredCaptchaState = {
type,
receiver,
sendTime: Date.now(), // 发送时间戳(毫秒)
totalSeconds: 60 // 总倒计时时长
};
setStorage(`captcha_${state.key}`, storedState, 'session');
// 4. 启动60秒倒计时
state.isCounting.value = true;
state.countdown.value = 60;
// 清除旧定时器(防止重复)
if (state.timer) clearInterval(state.timer);
// 5. 定时器逻辑(同步更新本地存储)
state.timer = window.setInterval(() => {
state.countdown.value--;
// 同步更新本地存储的剩余时间
updateStoredCaptchaState(state.key, state.countdown.value);
if (state.countdown.value <= 0) {
clearCaptchaTimer(state); // 倒计时结束,清除所有状态
}
}, 1000);
return true;
} catch (error) {
ElNotification({
message: `发送${type}验证码失败:${(error as Error).message || '未知错误'}`,
type: 'error',
duration: 2000,
customClass: 'global-notification'
});
console.error(`发送${type}验证码失败:`, error);
return false;
}
};
/**
* /+
* @param state
*/
export const clearCaptchaTimer = (state: CaptchaCountdownState): void => {
// 1. 清除定时器
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
// 2. 重置内存状态
state.countdown.value = 0;
state.isCounting.value = false;
// 3. 清空本地存储(核心:倒计时结束后允许重新发送)
removeStorage(`captcha_${state.key}`, 'session');
};

View File

@ -1,219 +0,0 @@
<template>
<div class="user-card">
<UserHeader />
<UserText :user-text="userText" @back="handleBack" />
<UserTabs v-model="activeTab" :tab-list="tabList" />
<el-form class="user-form" ref="changePasswordFormRef" :model="changePasswordForm" :rules="changePasswordRules">
<UserCaptcha v-model="changePasswordForm.code" @sendCode="handleSendCode" :show-send-button="showSendButton"
:send-state="sendState" />
<el-form-item prop="password">
<el-input v-model="changePasswordForm.password" placeholder="请输入新密码" type="password" show-password clearable
size="large" />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="changePasswordForm.confirmPassword" placeholder="请确认新密码" type="password" show-password
clearable size="large" />
</el-form-item>
<el-form-item class="form-submit">
<el-button type="primary" @click="handleChangePassword">重新登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
// type
import type { FormInstance, FormRules } from 'element-plus'
//
import UserHeader from './components/Header.vue'
import UserText from './components/Text.vue'
import UserTabs from './components/Tabs.vue'
import UserCaptcha from './components/Captcha.vue'
///store/
import { useUserStore } from '@/stores/modules/user'
import { getStorage } from '@/utils/storage'
import { md5Encrypt } from '@/utils/crypto'
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
//
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
//storeuser
const userStore = useUserStore()
const userInfo = getStorage<any>('userInfo')
//userText
const userText = ref({
backText: '返回',
account: userInfo?.username || ''
})
//tabs
const tabList = ref([
{ label: '邮箱验证', name: 'email' },
{ label: '手机验证', name: 'phone' },
])
const activeTab = ref<string>(getStorage('loginType', 'session') || 'email')
// ChangePassword
const changePasswordFormRef = ref<FormInstance>()
const changePasswordForm = ref({
email: userInfo?.email || '',
countryCode: '+86',
phone: userInfo?.mobile || '',
password: '',
confirmPassword: '',
code: '',
})
console.log('changePasswordForm.value', changePasswordForm.value);
const changePasswordRules = ref<FormRules>({
email: emailRules().email,
phone: phoneRules(changePasswordForm.value).phone,
password: passwordRules(changePasswordForm.value).password,
confirmPassword: passwordRules(changePasswordForm.value).confirmPassword,
code: codeRules().code,
})
//
const showSendButton = ref<boolean>(true)
const sendState = ref<object>({
isCounting: false,
countdown: 0,
})
//
const emailCaptchaState = initCaptchaState('email'); //
const phoneCaptchaState = initCaptchaState('phone'); //
//
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
//
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
//
// sendState
const updateSendState = (type: string) => {
if (type === 'email') {
sendState.value = {
isCounting: emailIsCounting,
countdown: emailCountdown
}
} else {
sendState.value = {
isCounting: phoneIsCounting,
countdown: phoneCountdown
}
}
}
// activeTabsendState
const initSendState = () => {
const currentType = activeTab.value === 'email' ? 'email' : 'phone'
updateSendState(currentType)
}
onMounted(() => {
//
initSendState()
})
// activeTabsendState
watch(
activeTab,
(newTab, oldTab) => {
activeTab.value = newTab;
console.log('activeTab.value', activeTab.value);
// taboldTaboldTabundefined
if (oldTab === undefined) return
if (newTab === 'email' || newTab === 'phone') {
// /
updateSendState(newTab)
} else if (newTab === 'code') {
//
const currentType: string = getStorage('forgotType', 'session') || 'email'
updateSendState(currentType)
}
},
{ immediate: false } //
)
//
const sendCodeApi = async () => {
//
const sendParams = {
email: changePasswordForm.value.email,
mobile: changePasswordForm.value.phone,
reg_type: activeTab.value === 'email' ? 1 : 2,
type:'resetPass_code'
}
return userStore.sendCode(sendParams)
}
//
const handleSendCode = async () => {
const currentType = activeTab.value
//
const type: string = currentType || 'email'
const receiver = type === 'phone' ? changePasswordForm.value.phone : changePasswordForm.value.email;
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState;
//
await sendCaptcha(
type,
receiver,
currentCaptchaState, //
() => sendCodeApi() // Promise
);
//
updateSendState(type)
}
//
const handleChangePassword = async () => {
if (!changePasswordFormRef.value) return;
try {
//
await changePasswordFormRef.value.validate();
//
const changePasswordParams = {
mobile: changePasswordForm.value.phone,
email: changePasswordForm.value.email,
reg_type: activeTab.value === 'email' ? 1 : 2,
code: changePasswordForm.value.code,
login_password1: md5Encrypt(changePasswordForm.value.password),
login_password2: md5Encrypt(changePasswordForm.value.confirmPassword),
};
console.log('changePasswordParams', changePasswordParams);
await userStore.changePassword(changePasswordParams);
changePasswordForm.value = {
email: userInfo?.email || '',
countryCode: '+86',
phone: userInfo?.mobile || '',
password: '',
confirmPassword: '',
code: ''
};
} catch (error) {
console.error('表单校验失败:', error);
}
}
//
const handleBack = () => {
};
</script>
<style scoped lang="scss">
.user-text {
margin-bottom: 0;
}
.user-form {
.form-submit {
margin-top: 24px;
}
}
</style>