feat:增加登录模块

This commit is contained in:
2025-06-25 23:48:43 +08:00
parent 37b2e588db
commit 99f828381d
21 changed files with 822 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
<template>
<el-empty v-if="dailyItemInfo.itemList.length === 0" />
<el-empty v-if="dailyItemInfo.itemList.length === 0" description="暂无记录"/>
<div v-else class="food-daily-item card-item">
<daily-summary :date="dailyItemInfo.date"/>
@@ -82,11 +82,11 @@ const getFoodDailyByDate = (date: string) => {
* @param record 美食记录
*/
const selectFoodRecordClick = async (record: IRecord) => {
// foodStore.foodRecordApiType = 'UPDATE'
foodStore.recordApiType = 'UPDATE'
// appStore.isShowMobileBack = true
// foodStore.currentFoodRecord = foodRecord
// await router.push({ name: 'MobileHomeFoodRecordForm' })
appStore.isShowMobileBack = true
foodStore.currentRecord = record
await router.push('/record/recordForm')
}
</script>

View File

@@ -8,7 +8,7 @@
</div>
<div class="food-list">
<el-empty v-if="foodStore.recipeList.length === 0" description="暂无美食记录"/>
<el-empty v-if="foodStore.recipeList.length === 0" description="暂无记录"/>
<div v-else class="list">
<food-card v-for="(item, index) in foodStore.recipeList"

View File

@@ -0,0 +1,130 @@
<template>
<div class="common-login">
<el-form ref="loginFormRef" :model="authStore.loginForm"
:rules="rules" class="login-form"
autocomplete="on" label-position="left"
@submit.native.prevent>
<!-- 账户名表单选项 -->
<el-form-item prop="username" class="form-item">
<el-input
v-model="authStore.loginForm.username"
placeholder="请输入账号" clearable
>
<template #prefix>
<i class="fa-solid fa-user"></i>
</template>
</el-input>
</el-form-item>
<!-- password表单选项 -->
<el-form-item prop="password" class="form-item">
<el-input
v-model="authStore.loginForm.password"
placeholder="请输入密码" type="password"
show-password clearable
@keyup.enter.native="handleLogin('common')"
>
<template #prefix>
<i class="fa-solid fa-lock"></i>
</template>
</el-input>
</el-form-item>
<el-form-item>
<login-auth />
</el-form-item>
<el-form-item class="login-option">
<el-checkbox v-model="authStore.loginForm.isRemember">记住密码</el-checkbox>
<el-link type="primary">忘记密码</el-link>
</el-form-item>
</el-form>
<el-button type="primary" style="width: 100%;"
:disabled="!authStore.isVerified"
:loading="authStore.loginLoading"
@click.prevent="handleLogin('common')">登录
</el-button>
<div />
<div class="login-type">
<el-button @click="authStore.changeLoginType('phone')">手机号登录</el-button>
<el-button>扫码登录</el-button>
</div>
<div class="login-others-type">
<el-divider content-position="center">其他登录方式</el-divider>
<div class="type-list">
<i class="fa-brands fa-weixin" style="color: #3CB034"></i>
<i class="fa-brands fa-qq" style="color: #29A4D9"></i>
<i class="fa-solid fa-envelope" style="color: #4971B8"
@click="authStore.changeLoginType('email')"></i>
<i class="fa-brands fa-github" style="color: #555555"></i>
</div>
</div>
<div class="login-register">
<span>还没有账户</span>
<el-link type="primary" @click="authStore.changeLoginType('register')">创建账户</el-link>
</div>
</div>
</template>
<script setup lang="ts">
import LoginAuth from '@/components/Login/LoginAuth.vue'
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules'
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import type { ILoginType } from '@/types/auth'
import { isMobile } from 'vue3-common/utils/layoutUtil'
const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore()
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref()
onMounted(() => {
const storageLoginForm = getLocalStorage('loginForm')
if (storageLoginForm) {
authStore.loginForm = JSON.parse(storageLoginForm)
}
})
const handleLogin = async (type: ILoginType) => {
loginFormRef.value.validate(async (valid: any) => {
if (valid) {
if (!isMobile()) {
ElMessage.error('目前只支持移动端')
return
}
authStore.loginLoading = true
try {
await authStore.handleLogin(type)
userStore.handleSession(authStore.session)
// 2. 存储账户密码到localStorage中
if (authStore.loginForm.isRemember) {
setLocalStorage('loginForm', JSON.stringify(authStore.loginForm))
}
await router.push('/record')
} finally {
authStore.loginLoading = false
}
} else {
ElMessage.error('请输入正确的信息')
return false
}
})
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="email-login">
<el-form ref="loginFormRef" :model="authStore.emailLoginForm"
:rules="rules" class="login-form"
autocomplete="on" label-position="left"
@submit.native.prevent>
<el-form-item prop="email" class="form-item">
<el-input
v-model="authStore.emailLoginForm.email"
placeholder="请输入邮箱地址" clearable
>
<template #prefix>
<i class="fa-solid fa-envelope"></i>
</template>
</el-input>
</el-form-item>
<el-form-item prop="code" class="code-item">
<el-input
v-model="authStore.emailLoginForm.code"
placeholder="请输入验证码" type="password"
show-password clearable
>
<template #prefix>
<i class="fa-solid fa-shield-halved"></i>
</template>
</el-input>
<el-button type="primary"
:disabled="authStore.isGettingCode"
@click="authStore.getAuthCode('email')">
{{ authStore.isGettingCode ? authStore.getCodeCountdown + 's' : '获取验证码' }}
</el-button>
</el-form-item>
</el-form>
<el-button type="primary" style="width: 100%;"
:loading="authStore.loginLoading"
@click.prevent="handleLogin()">登录
</el-button>
<el-button style="width: 100%;margin-left: 0;"
@click="authStore.changeLoginType('common')">取消
</el-button>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules'
const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore()
const router = useRouter()
const loginFormRef = ref()
const handleLogin = async () => {
loginFormRef.value.validate(async (valid: any) => {
if (valid) {
authStore.loginLoading = true
try {
await authStore.handleLogin('email')
await router.push('/home')
} finally {
authStore.loginLoading = false
}
} else {
ElMessage.error('请输入正确的信息')
return false
}
})
}
</script>

View File

@@ -0,0 +1,182 @@
<template>
<div class="login-auth">
<div class="slider-container"
@mousedown="startDrag" @mousemove="handleDrag"
@mouseup="endDrag" @mouseleave="endDrag"
@touchstart="startDrag" @touchmove="handleDrag" @touchend="endDrag">
<div ref="draggableRef" class="draggable"
:style="{ left: `${loginAuth.dragLeft}px` }">
<i v-if="authStore.isVerified" class="fas fa-check"></i>
<i v-else class="fas fa-arrow-right"></i>
</div>
<div :style="{ width: loginAuth.progressWidth + '%' }" class="slider-progress"></div>
<div class="slider-message" :style="{color: authStore.isVerified ? '#FFFFFF' : '#696969'}">
<span v-if="authStore.isVerified">验证成功</span>
<span v-else-if="loginAuth.isDragging">拖动中...</span>
<span v-else>向右拖动滑块 完成验证</span>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useAuthStore } from '@/store/auth'
const draggableRef = ref(null)
const authStore = useAuthStore()
const loginAuth = reactive({
isDragging: false,
initialX: 0,
offsetX: 0,
dragLeft: 0,
progressWidth: 0,
targetWidth: 100,
// 记录拖动的开始时间
startTime: 0,
// 记录拖动过程中的位置
positions: [],
// 记录拖动过程中的时间戳
timestamps: []
})
const startDrag = (event) => {
// 验证成功后禁止拖动
if (authStore.isVerified) return
loginAuth.isDragging = true
const touch = event.changedTouches ? event.changedTouches[0] : event
loginAuth.initialX = touch.clientX
loginAuth.offsetX = draggableRef.value?.offsetLeft
loginAuth.startTime = Date.now()
loginAuth.positions = []
loginAuth.timestamps = []
loginAuth.positions.push(loginAuth.initialX)
loginAuth.timestamps.push(loginAuth.startTime)
}
const handleDrag = (event) => {
if (loginAuth.isDragging && !authStore.isVerified) {
const touch = event.changedTouches ? event.changedTouches[0] : event
const dx = touch.clientX - loginAuth.initialX
const newLeft = loginAuth.offsetX + dx
const sliderWidth = draggableRef.value?.parentNode.offsetWidth
const handleWidth = draggableRef.value?.offsetWidth
if (newLeft < 0) {
loginAuth.dragLeft = 0
loginAuth.progressWidth = 0
} else if (newLeft > sliderWidth - handleWidth) {
loginAuth.dragLeft = sliderWidth - handleWidth
loginAuth.progressWidth = loginAuth.targetWidth
} else {
loginAuth.dragLeft = newLeft
loginAuth.progressWidth = (newLeft / (sliderWidth - handleWidth)) * loginAuth.targetWidth
}
loginAuth.positions.push(touch.clientX)
loginAuth.timestamps.push(Date.now())
}
}
const endDrag = () => {
loginAuth.isDragging = false
if (loginAuth.progressWidth === loginAuth.targetWidth && !checkIsHuman()) {
authStore.isVerified = true
} else {
loginAuth.dragLeft = 0
loginAuth.progressWidth = 0
}
}
const checkIsHuman = () => {
const endTime = Date.now()
const dragDuration = endTime - loginAuth.startTime
const speedChanges = []
for (let i = 1; i < loginAuth.positions.length; i++) {
const distance = loginAuth.positions[i] - loginAuth.positions[i - 1]
const time = loginAuth.timestamps[i] - loginAuth.timestamps[i - 1]
const speed = time > 0 ? distance / time : 0
speedChanges.push(speed)
}
// 计算速度变化的标准差
const meanSpeed = speedChanges.reduce((sum, speed) => sum + speed, 0) / speedChanges.length
const variance = speedChanges.reduce((sum, speed) => sum + Math.pow(speed - meanSpeed, 2), 0) / speedChanges.length
const stdDevSpeed = Math.sqrt(variance)
// 检查是否有停顿
let hasPause = false
for (let i = 1; i < loginAuth.timestamps.length; i++) {
if (loginAuth.timestamps[i] - loginAuth.timestamps[i - 1] > 200) {
hasPause = true
break
}
}
// 综合判断是否为机器操作
return dragDuration >= 100 && stdDevSpeed >= 0.1 && hasPause
}
</script>
<style lang="scss">
.login-auth {
width: 100%;
.slider-container {
position: relative;
height: 32px;
background-color: #E7E9E8;
border: 1px solid #E7E9E8;
border-radius: 5px;
overflow: hidden;
.draggable {
position: absolute;
top: 0;
left: 0;
width: 35px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--el-color-primary);
color: #FFFFFF;
cursor: pointer;
z-index: 3;
border-left: 1px solid #E7E9E8;
border-radius: 3px;
}
.slider-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: var(--el-color-primary);
z-index: 1;
}
.slider-message {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
user-select: none;
span {
transition: opacity 0.3s ease;
opacity: 1;
}
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="phone-login">
<el-form ref="loginFormRef" :model="authStore.phoneLoginForm"
:rules="rules" class="login-form"
autocomplete="on" label-position="left"
@submit.native.prevent>
<el-form-item prop="phoneNumber" class="form-item">
<el-input
v-model="authStore.phoneLoginForm.phoneNumber"
placeholder="请输入手机号" clearable
>
<template #prefix>
<i class="fa-solid fa-mobile-screen"></i>
</template>
</el-input>
</el-form-item>
<el-form-item prop="code" class="code-item">
<el-input
v-model="authStore.phoneLoginForm.code"
placeholder="请输入验证码" type="password"
show-password clearable
>
<template #prefix>
<i class="fa-solid fa-shield-halved"></i>
</template>
</el-input>
<el-button type="primary"
:disabled="authStore.isGettingCode"
@click="authStore.getAuthCode('phone')">
{{ authStore.isGettingCode ? authStore.getCodeCountdown + 's' : '获取验证码' }}
</el-button>
</el-form-item>
</el-form>
<el-button type="primary" style="width: 100%;"
:loading="authStore.loginLoading"
@click.prevent="handleLogin()">登录
</el-button>
<el-button style="width: 100%;margin-left: 0;"
@click="authStore.changeLoginType('common')">取消
</el-button>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules'
const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore()
const router = useRouter()
const loginFormRef = ref()
const handleLogin = async () => {
loginFormRef.value.validate(async (valid: any) => {
if (valid) {
authStore.loginLoading = true
try {
await authStore.handleLogin('phone')
await router.push('/home')
} finally {
authStore.loginLoading = false
}
} else {
ElMessage.error('请输入正确的信息')
return false
}
})
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="register">
<el-form ref="loginFormRef" :model="authStore.registerForm"
:rules="rules" class="login-form"
autocomplete="on" label-position="left"
@submit.native.prevent>
<!-- 账户名表单选项 -->
<el-form-item prop="username" class="form-item">
<el-input
v-model="authStore.registerForm.username"
placeholder="请输入账号" clearable
>
<template #prefix>
<i class="fa-solid fa-user"></i>
</template>
</el-input>
</el-form-item>
<!-- password表单选项 -->
<el-form-item prop="password" class="form-item">
<el-input
v-model="authStore.registerForm.password"
placeholder="请输入密码" type="password"
show-password clearable
@keyup.enter.native="handleLogin()"
>
<template #prefix>
<i class="fa-solid fa-lock"></i>
</template>
</el-input>
</el-form-item>
<!-- password表单选项 -->
<el-form-item prop="password" class="form-item">
<el-input
v-model="authStore.registerForm.confirmPassword"
placeholder="请输入密码" type="password"
show-password clearable
@keyup.enter.native="handleLogin()"
>
<template #prefix>
<i class="fa-solid fa-lock"></i>
</template>
</el-input>
</el-form-item>
</el-form>
<el-button type="primary" style="width: 100%;"
:loading="authStore.loginLoading"
@click.prevent="handleLogin()">注册
</el-button>
<el-button style="width: 100%;margin-left: 0;"
@click="authStore.changeLoginType('common')">取消
</el-button>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth'
import { ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
import { loginRules } from '@/utils/element/elRules'
const rules = reactive<FormRules>(loginRules)
const authStore = useAuthStore()
const loginFormRef = ref()
const handleLogin = async () => {
loginFormRef.value.validate(async (valid: any) => {
if (valid) {
const { password, confirmPassword } = authStore.registerForm
if (password !== confirmPassword) {
ElMessage.error('请确认密码是否一致')
return
}
authStore.loginLoading = true
try {
await authStore.handleRegister()
ElMessage.success('注册成功 请登录')
await authStore.changeLoginType('common')
} finally {
authStore.loginLoading = false
}
} else {
ElMessage.error('请输入正确的信息')
return false
}
})
}
</script>

View File

@@ -30,24 +30,10 @@ onMounted(async () => {
})
const getAverageCount = () => {
if (foodStore.queryDateType === 'month') {
return foodStore.recordList.length
} else {
const result = {}
// 遍历数据,按月统计出现次数
foodStore.recordList.forEach((item) => {
const date = new Date(item.date)
const month = String(date.getMonth() + 1)
if (result[month]) {
result[month].count += 1
} else {
result[month] = { count: 1 }
}
})
return foodStore.recordList.length / Object.keys(result).length
if (foodStore.recordStats.length === 0) {
return 0
}
const sum = foodStore.recordStats.map((item) => item.value).reduce((acc, curr) => acc + curr, 0)
return (sum / foodStore.recordStats.length).toFixed(2)
}
</script>