更新:登录功能
This commit is contained in:
22
pages/component/component.vue
Normal file
22
pages/component/component.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<no-data></no-data>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
title: this.$t('')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
24
pages/index/index.vue
Normal file
24
pages/index/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="title bg-[red] text-left text-[#fff]">这是一个等待开发的首页</view>
|
||||
<view class="description bg-[red]">首页的内容是在线课程</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
163
pages/login/README.md
Normal file
163
pages/login/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 登录功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本模块实现了从 nuttyreading-hw2 项目迁移的完整登录功能,包括:
|
||||
|
||||
- ✅ 验证码登录/注册
|
||||
- ✅ 密码登录
|
||||
- ✅ 忘记密码
|
||||
- ✅ 用户协议和隐私政策
|
||||
- ✅ 游客体验入口
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue3 Composition API
|
||||
- TypeScript
|
||||
- Pinia (状态管理)
|
||||
- WotUI (UI 组件库)
|
||||
- Tailwind CSS + SCSS
|
||||
- UniApp
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
pages/user/
|
||||
├── login.vue # 登录页面
|
||||
└── forget.vue # 忘记密码页面
|
||||
|
||||
api/modules/
|
||||
├── auth.ts # 认证相关 API
|
||||
└── common.ts # 通用 API
|
||||
|
||||
stores/
|
||||
└── user.ts # 用户状态管理
|
||||
|
||||
types/
|
||||
└── user.ts # 用户相关类型定义
|
||||
|
||||
utils/
|
||||
└── validator.ts # 表单验证工具
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 登录相关
|
||||
|
||||
1. **验证码登录/注册**
|
||||
- 接口:`GET book/user/registerOrLogin`
|
||||
- 参数:`{ tel: string, code: string }`
|
||||
|
||||
2. **密码登录**
|
||||
- 接口:`POST book/user/login`
|
||||
- 参数:`{ phone: string, password: string }`
|
||||
|
||||
3. **重置密码**
|
||||
- 接口:`POST book/user/setPassword`
|
||||
- 参数:`{ phone: string, code: string, password: string }`
|
||||
|
||||
### 通用接口
|
||||
|
||||
1. **发送邮箱验证码**
|
||||
- 接口:`GET common/user/getMailCaptcha`
|
||||
- 参数:`{ email: string }`
|
||||
|
||||
2. **获取协议内容**
|
||||
- 接口:`GET common/agreement/detail`
|
||||
- 参数:`{ id: number }` (111: 用户协议, 112: 隐私政策)
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 登录页面
|
||||
|
||||
访问路径:`/pages/user/login`
|
||||
|
||||
**验证码登录**:
|
||||
1. 输入邮箱地址
|
||||
2. 点击"Get Code"获取验证码
|
||||
3. 输入收到的验证码
|
||||
4. 勾选用户协议
|
||||
5. 点击"Go Login"登录
|
||||
|
||||
**密码登录**:
|
||||
1. 点击"Password Login"切换到密码登录
|
||||
2. 输入邮箱地址和密码
|
||||
3. 勾选用户协议
|
||||
4. 点击"Go Login"登录
|
||||
|
||||
### 2. 忘记密码
|
||||
|
||||
访问路径:`/pages/user/forget`
|
||||
|
||||
1. 输入邮箱地址
|
||||
2. 点击"Get Code"获取验证码
|
||||
3. 输入验证码
|
||||
4. 输入新密码(需满足强度要求)
|
||||
5. 再次输入新密码确认
|
||||
6. 点击"Submit"提交
|
||||
|
||||
### 3. 密码强度要求
|
||||
|
||||
- **强密码**:8位以上,包含大小写字母、数字和特殊字符
|
||||
- **中等密码**:8位以上,包含大小写字母、数字、特殊字符中的两项
|
||||
- **弱密码**:8位以上
|
||||
- **最低要求**:6-20位,必须包含字母和数字
|
||||
|
||||
## 状态管理
|
||||
|
||||
使用 Pinia 管理用户状态:
|
||||
|
||||
```typescript
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 登录成功后设置用户信息
|
||||
userStore.setUserInfo(userInfo)
|
||||
|
||||
// 检查登录状态
|
||||
if (userStore.isLoggedIn) {
|
||||
// 已登录
|
||||
}
|
||||
|
||||
// 登出
|
||||
userStore.logout()
|
||||
```
|
||||
|
||||
## 国际化
|
||||
|
||||
支持中英文切换,翻译文件位于:
|
||||
- `locale/en.json` - 英文
|
||||
- `locale/zh-Hans.json` - 简体中文
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API 地址**:已配置为 `https://global.nuttyreading.com/`
|
||||
2. **请求头**:自动添加 `token`、`appType: 'abroad'`、`version_code`
|
||||
3. **Token 失效**:自动处理 401 错误,清除用户信息并跳转登录页
|
||||
4. **验证码倒计时**:60秒,防止重复发送
|
||||
5. **协议同意**:登录和获取验证码前必须同意用户协议和隐私政策
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. 测试验证码登录流程
|
||||
2. 测试密码登录流程
|
||||
3. 测试忘记密码流程
|
||||
4. 测试登录方式切换
|
||||
5. 测试表单验证
|
||||
6. 测试协议弹窗
|
||||
7. 测试多平台兼容性(H5、小程序、APP)
|
||||
|
||||
## 已知问题
|
||||
|
||||
- 游客页面 `/pages/visitor/visitor` 需要单独实现
|
||||
- 部分图标可能需要根据实际设计调整
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-11-02)
|
||||
- ✅ 完成登录功能迁移
|
||||
- ✅ 实现验证码登录和密码登录
|
||||
- ✅ 实现忘记密码功能
|
||||
- ✅ 添加用户协议和隐私政策
|
||||
- ✅ 支持中英文国际化
|
||||
364
pages/login/forget.vue
Normal file
364
pages/login/forget.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="title">{{ $t('forget.title') }}</view>
|
||||
|
||||
<!-- 邮箱输入 -->
|
||||
<view class="input-box">
|
||||
<text class="input-tit">{{ $t('login.email') }}</text>
|
||||
<input
|
||||
class="input-text"
|
||||
type="text"
|
||||
v-model="email"
|
||||
:placeholder="$t('login.emailPlaceholder')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<view class="input-box">
|
||||
<text class="input-tit">{{ $t('login.code') }}</text>
|
||||
<input
|
||||
class="input-text"
|
||||
type="number"
|
||||
v-model="code"
|
||||
:placeholder="$t('login.codePlaceholder')"
|
||||
/>
|
||||
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="getCode">
|
||||
{{ t('login.getCode') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="input-box">
|
||||
<text class="input-tit">{{ $t('forget.password') }}</text>
|
||||
<input
|
||||
class="input-text"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
v-model="password"
|
||||
:placeholder="$t('forget.passwordPlaceholder')"
|
||||
@input="inputMethod(password)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 密码强度提示 -->
|
||||
<view v-if="note !== ''" class="password-hint" style="font-size: 28rpx; color: #999;">
|
||||
<text style="line-height: 34rpx; padding-top: 15rpx; display: block;">
|
||||
{{ note }}
|
||||
</text>
|
||||
<text v-html="str2" style="margin-top: 10rpx; display: block;"></text>
|
||||
</view>
|
||||
|
||||
<!-- 确认密码输入 -->
|
||||
<view class="input-box">
|
||||
<text class="input-tit">{{ $t('forget.passwordAgain') }}</text>
|
||||
<input
|
||||
class="input-text"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
v-model="confirmPassword"
|
||||
:placeholder="$t('forget.passwordAgainPlaceholder')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="btn-box">
|
||||
<button @click="onSubmit" class="submit-btn">{{ $t('forget.submit') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { commonApi } from '@/api/modules/common'
|
||||
import { resetPassword } from '@/api/modules/auth'
|
||||
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 表单数据
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
// 验证码相关
|
||||
const codeText = ref('Get Code')
|
||||
const readonly = ref(false)
|
||||
|
||||
// 密码强度相关
|
||||
const passwordOk = ref(false)
|
||||
const note = ref('')
|
||||
const str2 = ref('')
|
||||
|
||||
let codeTimer: any = null
|
||||
|
||||
/**
|
||||
* 表单验证函数
|
||||
*/
|
||||
// 邮箱是否为空
|
||||
const isEmailEmpty = () => {
|
||||
if (!email.value) {
|
||||
uni.showToast({
|
||||
title: t('login.emailPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 邮箱格式验证
|
||||
const isEmailVerified = (emailVal: string) => {
|
||||
if (!validateEmail(emailVal)) {
|
||||
uni.showToast({
|
||||
title: t('login.emailError'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 验证码是否为空
|
||||
const isCodeEmpty = () => {
|
||||
if (!code.value) {
|
||||
uni.showToast({
|
||||
title: t('login.codePlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 密码是否为空
|
||||
const isPasswordEmpty = () => {
|
||||
if (!password.value) {
|
||||
uni.showToast({
|
||||
title: t('forget.passwordPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 确认密码是否为空
|
||||
const isConfirmPasswordEmpty = () => {
|
||||
if (!confirmPassword.value) {
|
||||
uni.showToast({
|
||||
title: t('forget.passwordAgainPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 密码是否匹配
|
||||
const isPasswordMatch = () => {
|
||||
if (confirmPassword.value !== password.value) {
|
||||
uni.showToast({
|
||||
title: t('forget.passwordNotMatch'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 密码强度验证
|
||||
const isPasswordStrongEnough = () => {
|
||||
if (!passwordOk.value) {
|
||||
uni.showToast({
|
||||
title: note.value || t('forget.passwordStrengthWeak'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const getCode = async () => {
|
||||
if (readonly.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEmailEmpty()) return
|
||||
if (!isEmailVerified(email.value)) return
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
await commonApi.sendMailCaptcha(email.value)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('login.sendCodeSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
getCodeState()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('Send code error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码倒计时
|
||||
*/
|
||||
const getCodeState = () => {
|
||||
if (codeTimer) {
|
||||
clearInterval(codeTimer)
|
||||
}
|
||||
|
||||
readonly.value = true
|
||||
let countdown = 60
|
||||
codeText.value = `60S`
|
||||
|
||||
codeTimer = setInterval(() => {
|
||||
countdown--
|
||||
codeText.value = `${countdown}S`
|
||||
if (countdown <= 0) {
|
||||
clearInterval(codeTimer)
|
||||
codeText.value = t('login.getCode')
|
||||
readonly.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码强度验证
|
||||
*/
|
||||
const inputMethod = (value: string) => {
|
||||
passwordOk.value = false
|
||||
const strength = checkPasswordStrength(value)
|
||||
|
||||
if (strength === 'strong') {
|
||||
str2.value = `<span style='color:#18bc37'>${t('forget.passwordStrengthStrong')}</span>`
|
||||
note.value = ''
|
||||
passwordOk.value = true
|
||||
} else if (strength === 'medium') {
|
||||
note.value = t('forget.passwordStrengthWeak')
|
||||
str2.value = `<span style='color:#2979ff'>${t('forget.passwordStrengthMedium')}</span>`
|
||||
passwordOk.value = true
|
||||
} else if (strength === 'weak') {
|
||||
note.value = t('forget.passwordStrengthWeak')
|
||||
str2.value = ''
|
||||
} else {
|
||||
passwordOk.value = false
|
||||
note.value = t('forget.passwordStrengthWeak')
|
||||
str2.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交重置密码
|
||||
*/
|
||||
const onSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!isEmailEmpty()) return
|
||||
if (!isEmailVerified(email.value)) return
|
||||
if (!isCodeEmpty()) return
|
||||
if (!isPasswordEmpty()) return
|
||||
if (!isPasswordStrongEnough()) return
|
||||
if (!isConfirmPasswordEmpty()) return
|
||||
if (!isPasswordMatch()) return
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
await resetPassword(email.value, code.value, password.value)
|
||||
uni.hideLoading()
|
||||
|
||||
uni.showModal({
|
||||
title: t('global.tips'),
|
||||
content: t('forget.passwordChanged'),
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('Reset password error:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
background-color: #ffffff;
|
||||
padding: 0 30rpx;
|
||||
min-height: 100vh;
|
||||
|
||||
.title {
|
||||
padding: 50rpx 0 50rpx 30rpx;
|
||||
font-size: 60rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 100rpx;
|
||||
padding-top: 30rpx;
|
||||
border-bottom: 1rpx solid #eeeeee;
|
||||
align-items: center;
|
||||
|
||||
.input-tit {
|
||||
font-size: 30rpx;
|
||||
line-height: 34rpx;
|
||||
width: 230rpx;
|
||||
text-align: right;
|
||||
padding-right: 25rpx;
|
||||
padding-bottom: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-text {
|
||||
flex: 1;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
height: 60rpx;
|
||||
background-color: #f8f9fb;
|
||||
font-size: 28rpx;
|
||||
padding: 0 14rpx;
|
||||
min-width: 0;
|
||||
border-radius: 10rpx;
|
||||
color: #999;
|
||||
line-height: 60rpx;
|
||||
margin-left: 20rpx;
|
||||
border: none;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.active {
|
||||
color: $app-theme-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-hint {
|
||||
padding: 10rpx 0 10rpx 205rpx;
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
margin-top: 70rpx;
|
||||
|
||||
.submit-btn {
|
||||
font-size: 32rpx;
|
||||
background: linear-gradient(90deg, #54a966 0%, #54a966 100%);
|
||||
color: #fff;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 50rpx;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
667
pages/login/login.vue
Normal file
667
pages/login/login.vue
Normal file
@@ -0,0 +1,667 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- Logo 背景区域 -->
|
||||
<view class="logo-bg">
|
||||
<text class="welcome-text">Hello! Welcome to<br>Taimed International</text>
|
||||
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-1"></image>
|
||||
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单区域 -->
|
||||
<view class="form-box">
|
||||
<!-- 登录方式标题 -->
|
||||
<view class="login-method">
|
||||
<view class="title active">
|
||||
<template v-if="loginType === 2000">{{ $t('login.codeLogin') }}</template>
|
||||
<template v-if="loginType === 1000">{{ $t('login.passwordLogin') }}</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 验证码登录 -->
|
||||
<view v-if="loginType === 2000">
|
||||
<view class="input-tit">{{ $t('login.email') }}</view>
|
||||
<view class="input-box">
|
||||
<input
|
||||
v-model="email"
|
||||
:placeholder="$t('login.emailPlaceholder')"
|
||||
placeholder-class="grey"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-tit">{{ $t('login.code') }}</view>
|
||||
<view class="input-box">
|
||||
<input
|
||||
v-model="code"
|
||||
:placeholder="$t('login.codePlaceholder')"
|
||||
placeholder-class="grey"
|
||||
maxlength="6"
|
||||
@confirm="onSubmit"
|
||||
/>
|
||||
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="onSetCode">
|
||||
{{ t('login.getCode') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码登录 -->
|
||||
<view v-if="loginType === 1000">
|
||||
<view class="input-tit">{{ $t('login.email') }}</view>
|
||||
<view class="input-box">
|
||||
<input
|
||||
v-model="phoneEmail"
|
||||
:placeholder="$t('login.emailPlaceholder')"
|
||||
placeholder-class="grey"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-tit">{{ $t('login.password') }}</view>
|
||||
<view class="input-box">
|
||||
<input
|
||||
class="input-item"
|
||||
v-model="password"
|
||||
:password="!isSee"
|
||||
:placeholder="$t('login.passwordPlaceholder')"
|
||||
placeholder-class="grey"
|
||||
@confirm="onSubmit"
|
||||
/>
|
||||
<image
|
||||
v-if="isSee"
|
||||
src="@/static/icon/ic_logon_display.png"
|
||||
mode="aspectFit"
|
||||
class="eye-icon"
|
||||
@click="isSee = false"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
src="@/static/icon/ic_logon_hide.png"
|
||||
mode="aspectFit"
|
||||
class="eye-icon"
|
||||
@click="isSee = true"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议同意 -->
|
||||
<view class="protocol-box">
|
||||
<view class="select" :class="{ active: agree }" @click="agreeAgreements"></view>
|
||||
<view class="protocol-text">
|
||||
{{ $t('login.agree') }}
|
||||
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
|
||||
and
|
||||
<text class="highlight" @click="yszc">《{{ $t('login.privacyPolicy') }}》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="btn-box">
|
||||
<button @click="onSubmit" class="login-btn" :class="{ active: btnShow }">
|
||||
{{ $t('login.goLogin') }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="qie-huan">
|
||||
<view v-if="loginType === 2000" @click="changeLoginType(1000)">
|
||||
{{ $t('login.switchToPassword') }}
|
||||
</view>
|
||||
<view v-if="loginType === 1000" class="switch-links">
|
||||
<text @click="changeLoginType(2000)">{{ $t('login.switchToCode') }}</text>
|
||||
<text @click="onPageJump('/pages/login/forget')">{{ $t('login.forgotPassword') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 游客体验 -->
|
||||
<view class="youke-l">
|
||||
<view @click="onPageJump('/pages/visitor/visitor')">
|
||||
{{ $t('login.noLogin') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户协议弹窗 -->
|
||||
<wd-popup v-model="yhxyShow" position="bottom">
|
||||
<view class="tanchu">
|
||||
<view class="dp-title" v-html="yhxyText.title"></view>
|
||||
<view class="dp-content" v-html="yhxyText.content"></view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 隐私政策弹窗 -->
|
||||
<wd-popup v-model="yszcShow" position="bottom">
|
||||
<view class="tanchu">
|
||||
<view class="dp-title" v-html="yszcText.title"></view>
|
||||
<view class="dp-content" v-html="yszcText.content"></view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { loginWithCode, loginWithPassword } from '@/api/modules/auth'
|
||||
import { commonApi } from '@/api/modules/common'
|
||||
import { validateEmail } from '@/utils/validator'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 登录类型:2000-验证码登录,1000-密码登录
|
||||
const loginType = ref<1000 | 2000>(2000)
|
||||
|
||||
// 表单数据
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const password = ref('')
|
||||
const phoneEmail = ref('')
|
||||
const agree = ref(false)
|
||||
const isSee = ref(false)
|
||||
|
||||
// 验证码相关
|
||||
const codeText = ref('Get Code')
|
||||
const readonly = ref(false)
|
||||
const btnShow = ref(true)
|
||||
|
||||
// 用户协议和隐私政策
|
||||
const yhxyShow = ref(false)
|
||||
const yszcShow = ref(false)
|
||||
const yhxyText = ref<any>({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
const yszcText = ref<any>({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
let codeTimer: any = null
|
||||
|
||||
/**
|
||||
* 切换登录方式
|
||||
* @param type 1000-密码登录,2000-验证码登录
|
||||
*/
|
||||
const changeLoginType = (type: 1000 | 2000) => {
|
||||
loginType.value = type
|
||||
code.value = ''
|
||||
password.value = ''
|
||||
const temporaryEmail = email.value || phoneEmail.value
|
||||
email.value = temporaryEmail
|
||||
phoneEmail.value = temporaryEmail
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单验证
|
||||
*/
|
||||
// 是否同意协议
|
||||
const isAgree = () => {
|
||||
if (!agree.value) {
|
||||
uni.showToast({
|
||||
title: t('login.agreeFirst'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 是否填写邮箱
|
||||
const isEmailEmpty = () => {
|
||||
if (!email.value) {
|
||||
uni.showToast({
|
||||
title: t('login.emailPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 是否填写验证码
|
||||
const isCodeEmpty = () => {
|
||||
if (!code.value) {
|
||||
uni.showToast({
|
||||
title: t('login.codePlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 邮箱格式验证
|
||||
const isEmailVerified = (email: string) => {
|
||||
console.log(email, validateEmail(email))
|
||||
if (!validateEmail(email)) {
|
||||
uni.showToast({
|
||||
title: t('login.emailError'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 是否填写手机号或邮箱
|
||||
const isPhoneEmailEmpty = () => {
|
||||
if (!phoneEmail.value) {
|
||||
uni.showToast({
|
||||
title: t('login.emailPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 是否填写密码
|
||||
const isPasswordEmpty = () => {
|
||||
if (!password.value) {
|
||||
uni.showToast({
|
||||
title: t('login.passwordPlaceholder'),
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 提交登录
|
||||
*/
|
||||
// 验证码登录
|
||||
const verifyCodeLogin = async () => {
|
||||
if (!isEmailEmpty()) return false
|
||||
|
||||
if (!isEmailVerified(email.value)) return false
|
||||
|
||||
if (!isCodeEmpty()) return false
|
||||
|
||||
const res = await loginWithCode(email.value, code.value)
|
||||
|
||||
return res
|
||||
}
|
||||
// 密码登录
|
||||
const passwordLogin = async () => {
|
||||
if (!isPhoneEmailEmpty()) return false
|
||||
|
||||
if (!isEmailVerified(phoneEmail.value)) return false
|
||||
|
||||
if (!isPasswordEmpty) return false
|
||||
|
||||
const res = await loginWithPassword(phoneEmail.value, password.value)
|
||||
|
||||
return res
|
||||
}
|
||||
// 提交登录
|
||||
const onSubmit = async () => {
|
||||
if(!isAgree()) return false
|
||||
|
||||
let res = null
|
||||
|
||||
switch (loginType.value) {
|
||||
case 2000:
|
||||
res = await verifyCodeLogin()
|
||||
break
|
||||
case 1000:
|
||||
res = await passwordLogin()
|
||||
break
|
||||
}
|
||||
|
||||
console.log('res', res)
|
||||
|
||||
if (res && res.userInfo && res.token) {
|
||||
res.userInfo.token = res.token.token
|
||||
console.log('设置用户信息login', res.userInfo)
|
||||
userStore.setUserInfo(res.userInfo)
|
||||
uni.showToast({
|
||||
title: t('login.loginSuccess'),
|
||||
duration: 600,
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 600)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const onSetCode = async () => {
|
||||
if (readonly.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if(!isAgree()) return false
|
||||
|
||||
if (!isEmailEmpty()) return false
|
||||
|
||||
if (!isEmailVerified(email.value)) return false
|
||||
|
||||
try {
|
||||
uni.showLoading()
|
||||
await commonApi.sendMailCaptcha(email.value)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: t('login.sendCodeSuccess'),
|
||||
icon: 'none'
|
||||
})
|
||||
getCodeState()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('Send code error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码倒计时
|
||||
*/
|
||||
const getCodeState = () => {
|
||||
if (codeTimer) {
|
||||
clearInterval(codeTimer)
|
||||
}
|
||||
|
||||
readonly.value = true
|
||||
let countdown = 60
|
||||
codeText.value = '60S'
|
||||
|
||||
codeTimer = setInterval(() => {
|
||||
countdown--
|
||||
codeText.value = `${countdown}S`
|
||||
if (countdown <= 0) {
|
||||
clearInterval(codeTimer)
|
||||
codeText.value = t('login.getCode')
|
||||
readonly.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示用户协议
|
||||
*/
|
||||
const yhxy = () => {
|
||||
yhxyShow.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示隐私政策
|
||||
*/
|
||||
const yszc = () => {
|
||||
yszcShow.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
*/
|
||||
const onPageJump = (url: string) => {
|
||||
uni.navigateTo({
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取协议内容
|
||||
*/
|
||||
const getAgreements = async (id: number) => {
|
||||
const res = await commonApi.getAgreement(id)
|
||||
if (!res.content) return false
|
||||
let content = res.content || ''
|
||||
content = content.replace(
|
||||
/<h5>/g,
|
||||
'<view style="font-weight: bold;font-size: 32rpx;margin-top: 20rpx;margin-bottom: 20rpx;">'
|
||||
)
|
||||
content = content.replace(/<\/h5>/g, '</view>')
|
||||
return {
|
||||
title: res.title,
|
||||
content: content
|
||||
}
|
||||
}
|
||||
const loadAgreements = async () => {
|
||||
// 获取用户协议
|
||||
const yhxyRes = await getAgreements(111)
|
||||
yhxyText.value = yhxyRes
|
||||
|
||||
// 获取隐私政策
|
||||
const yszcRes = await getAgreements(112)
|
||||
yszcText.value = yszcRes
|
||||
}
|
||||
|
||||
// 同意协议
|
||||
const agreeAgreements = () => {
|
||||
agree.value = !agree.value;
|
||||
uni.setStorageSync('Agreements_agreed', agree.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAgreements()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.logo-bg {
|
||||
background-image: url('@/static/icon/login_bg.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
height: 25vh;
|
||||
position: relative;
|
||||
|
||||
.welcome-text {
|
||||
font-size: 45rpx;
|
||||
line-height: 65rpx;
|
||||
position: absolute;
|
||||
top: 120rpx;
|
||||
left: 60rpx;
|
||||
color: #fff;
|
||||
letter-spacing: 6rpx;
|
||||
}
|
||||
|
||||
.icon-hua-1 {
|
||||
position: absolute;
|
||||
bottom: 60rpx;
|
||||
left: 245rpx;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.icon-hua-2 {
|
||||
position: absolute;
|
||||
bottom: 10rpx;
|
||||
right: 30rpx;
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.form-box {
|
||||
padding: calc(var(--status-bar-height) + 40rpx) 60rpx 50rpx 60rpx;
|
||||
background-color: #fff;
|
||||
min-height: 75vh;
|
||||
|
||||
.login-method {
|
||||
text-align: center;
|
||||
padding: 0 96rpx;
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
font-size: 40rpx;
|
||||
letter-spacing: 3rpx;
|
||||
color: #666;
|
||||
|
||||
&.active {
|
||||
position: relative;
|
||||
color: $app-theme-color;
|
||||
padding-bottom: 35rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-tit {
|
||||
margin-top: 20rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: $app-theme-color;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8rpx;
|
||||
border-bottom: solid 2rpx #efeef4;
|
||||
margin: 30rpx 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.input-item {
|
||||
font-size: 28rpx;
|
||||
flex: 1;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
height: 60rpx;
|
||||
background-color: #f8f9fb;
|
||||
font-size: 28rpx;
|
||||
padding: 0 14rpx;
|
||||
min-width: 0;
|
||||
border-radius: 10rpx;
|
||||
color: #999;
|
||||
line-height: 60rpx;
|
||||
margin-left: 20rpx;
|
||||
border: none;
|
||||
|
||||
&.active {
|
||||
color: $app-theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
.eye-icon {
|
||||
width: 36rpx;
|
||||
height: 24rpx;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-box {
|
||||
line-height: 38rpx;
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
|
||||
.select {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background-image: url('@/static/icon/ic_gender_unselected.png');
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
margin-right: 15rpx;
|
||||
margin-top: 2rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.active {
|
||||
background-image: url('@/static/icon/ic_agreed.png');
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: $app-theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-box {
|
||||
margin-top: 40rpx;
|
||||
|
||||
.login-btn {
|
||||
font-size: 32rpx;
|
||||
background-color: #e5e5e5;
|
||||
color: #fff;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 50rpx;
|
||||
border: none;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(90deg, #54a966 0%, #54a966 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qie-huan {
|
||||
font-size: 26rpx;
|
||||
margin: 20rpx 0 0 0;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
|
||||
.switch-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.youke-l {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 60rpx 0 0 0;
|
||||
font-size: 26rpx;
|
||||
color: $app-theme-color;
|
||||
|
||||
view {
|
||||
font-weight: bold;
|
||||
border: 1px solid $app-theme-color;
|
||||
border-radius: 10rpx;
|
||||
padding: 5rpx 15rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tanchu {
|
||||
padding: 40rpx 10rpx;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
|
||||
.dp-title {
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 50rpx;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dp-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
line-height: 45rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user