Files
medical-mall/pages/user/register.uvue
2026-02-27 08:30:40 +08:00

552 lines
13 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="register-wrapper">
<!-- Header Logo -->
<view class="header">
<image :src="logoUrl" mode="aspectFit" class="logo" />
</view>
<!-- 注册表单区域 -->
<view class="register-box">
<view class="title">注册账号</view>
<!-- 注册表单 -->
<view class="form-content">
<!-- 邮箱 -->
<view class="input-group">
<view class="input-wrapper">
<image src="/static/user/phone_1.png" class="input-icon" />
<input
type="text"
placeholder="输入邮箱"
:value="email"
@input="(e: any) => email = e.detail.value"
class="input-field"
/>
</view>
</view>
<!-- 密码 -->
<view class="input-group">
<view class="input-wrapper">
<image src="/static/user/code_1.png" class="input-icon" />
<input
type="password"
placeholder="填写密码"
:value="password"
@input="(e: any) => password = e.detail.value"
class="input-field"
/>
</view>
</view>
<!-- 确认密码 -->
<view class="input-group">
<view class="input-wrapper">
<image src="/static/user/code_1.png" class="input-icon" />
<input
type="password"
placeholder="确认密码"
:value="confirmPassword"
@input="(e: any) => confirmPassword = e.detail.value"
class="input-field"
/>
</view>
</view>
</view>
<!-- 注册按钮 -->
<view class="register-btn" @click="handleRegister" :class="{ 'disabled': isLoading }">
注册
</view>
<!-- 已有账号 -->
<view class="tips">
<text class="tips-text">已有账号?</text>
<text class="tips-link" @click="navigateToLogin">立即登录</text>
</view>
<!-- 协议勾选 -->
<view class="protocol">
<checkbox-group @change="handleProtocolChange">
<checkbox
:checked="protocol"
:class="{ 'trembling': inAnimation }"
@animationend="inAnimation = false"
/>
<text class="protocol-text">
已阅读并同意
<text class="main-color" @click="navigateToTerms(3)">《用户协议》</text>
<text class="main-color" @click="navigateToTerms(4)">《隐私协议》</text>
</text>
</checkbox-group>
</view>
</view>
<!-- 底部版权信息 -->
<view class="footer">
<text class="footer-text">Copyright ©2024 Mall. All Rights Reserved</text>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { ensureUserProfile } from '@/utils/sapi.uts'
// 响应式数据
const email = ref<string>('')
const password = ref<string>('')
const confirmPassword = ref<string>('')
const protocol = ref<boolean>(false)
const inAnimation = ref<boolean>(false)
const isLoading = ref<boolean>(false)
const logoUrl = ref<string>('/static/logo.png')
// 处理协议勾选变化
const handleProtocolChange = (e: any) => {
protocol.value = !protocol.value
}
// 验证邮箱
const validateEmail = (): boolean => {
if (email.value.trim() === '') {
uni.showToast({
title: '请填写邮箱',
icon: 'none'
})
return false
}
// 基础邮箱格式校验(足够用于前端提示)
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
uni.showToast({
title: '请输入正确的邮箱',
icon: 'none'
})
return false
}
return true
}
// 验证密码
const validatePassword = (): boolean => {
if (password.value.trim() === '') {
uni.showToast({
title: '请填写密码',
icon: 'none'
})
return false
}
if (password.value.length < 6) {
uni.showToast({
title: '密码长度不能少于6位',
icon: 'none'
})
return false
}
// 密码不能过于简单
if (/^([0-9]|[a-z]|[A-Z]){0,6}$/i.test(password.value)) {
uni.showToast({
title: '您输入的密码过于简单',
icon: 'none'
})
return false
}
return true
}
// 验证确认密码
const validateConfirmPassword = (): boolean => {
if (confirmPassword.value.trim() === '') {
uni.showToast({
title: '请确认密码',
icon: 'none'
})
return false
}
if (confirmPassword.value !== password.value) {
uni.showToast({
title: '两次输入的密码不一致',
icon: 'none'
})
return false
}
return true
}
// 处理注册
const handleRegister = async () => {
// 检查协议
if (!protocol.value) {
inAnimation.value = true
uni.showToast({
title: '请先阅读并同意协议',
icon: 'none'
})
return
}
// 表单验证
if (!validateEmail()) {
return
}
if (!validatePassword()) {
return
}
if (!validateConfirmPassword()) {
return
}
isLoading.value = true
try {
// 使用 Supabase Auth邮箱 + 密码注册
const result = await supa.signUp(email.value.trim(), password.value)
console.log('📝 注册返回结果:', result)
console.log('📝 注册返回结果JSON:', JSON.stringify(result))
// 检查是否有错误(邮件发送失败等)
const errorCode = result?.getString('error_code') ?? ''
const errorMsg = result?.getString('msg') ?? ''
const code = result?.getNumber('code') ?? 0
console.log('📝 错误代码:', errorCode, '错误信息:', errorMsg, '状态码:', code)
// 如果返回 500 错误且是邮件发送失败,但用户可能已创建
if (code === 500 && (errorCode === 'unexpected_failure' || errorMsg.includes('confirmation email'))) {
console.warn('⚠️ 邮件发送失败,但用户可能已创建,尝试获取用户信息')
// 即使邮件发送失败,用户可能已经在 auth.users 中创建
// 这里我们仍然尝试创建用户资料
}
// signUp 返回的是 UTSJSONObjectSupabase signup API 返回结构:
// { user: {...}, session: {...} } - 如果邮箱验证未开启
// { user: {...} } - 如果邮箱验证已开启(需要验证邮箱后才能登录)
// { code: 500, error_code: ..., msg: ... } - 如果发生错误(但用户可能已创建)
let user: UTSJSONObject | null = null
let hasSession = false
if (result != null) {
// 尝试获取 user 字段
const userField = result.getJSON('user')
if (userField != null) {
user = userField
console.log('✅ 找到 user 字段:', user.getString('id'), user.getString('email'))
} else {
// 如果没有 user 字段,可能 result 本身就是 user 对象
const id = result.getString('id')
if (id != null && id !== '') {
user = result
console.log('✅ result 本身就是 user 对象:', id)
} else {
console.warn('⚠️ 未找到 user 信息,检查所有字段:', Object.keys(result.toMap() || {}))
}
}
// 检查是否有 session表示注册后自动登录成功
const sessionField = result.getJSON('session')
if (sessionField != null) {
hasSession = true
console.log('✅ 找到 session已自动登录')
// 如果有 session说明已经自动登录token 应该已经设置
// 此时可以直接创建用户资料
} else {
console.log(' 未找到 session可能需要邮箱验证')
}
}
// 如果返回错误且没有用户信息,说明注册失败
if (user == null && code !== 0 && code !== 200) {
// 如果是邮件发送失败,给出明确的错误提示
if (code === 500 && errorMsg.includes('confirmation email')) {
throw new Error('注册失败:邮件服务配置错误,请联系管理员或修改 Supabase 配置(设置 ENABLE_EMAIL_AUTOCONFIRM=true')
} else {
throw new Error(errorMsg || '注册失败,请重试')
}
}
// 如果获取到 user尝试创建业务侧用户资料ak_users
if (user != null) {
try {
const profileResult = await ensureUserProfile(user)
if (profileResult != null) {
console.log('✅ 用户资料创建成功:', profileResult.id)
} else {
console.warn('⚠️ 用户资料创建失败,但注册已成功')
// 如果创建失败,可能是因为 RLS 策略限制
// 建议用户登录后再自动创建(在 getCurrentUser 中处理)
}
} catch (profileError) {
console.error('❌ 创建用户资料异常:', profileError)
// 即使创建资料失败,也不阻止注册流程
// 用户登录时会自动创建(见 utils/store.uts 的 getCurrentUser
}
} else {
console.warn('⚠️ 注册成功但未获取到用户信息')
// 可能需要邮箱验证,用户验证邮箱后登录时会自动创建资料
}
// 如果注册后没有自动登录(需要邮箱验证),提示用户
if (!hasSession && user != null) {
console.log(' 需要邮箱验证,验证后登录时会自动创建用户资料')
}
uni.showToast({
title: '注册成功',
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/user/login'
})
}, 1500)
} catch (err) {
console.error('注册错误:', err)
let errorMessage = '注册失败,请重试'
if (err != null && typeof err === 'object') {
const error = err as Error
if (error.message != null && error.message.trim() !== '') {
errorMessage = error.message
// 如果是邮件发送失败,给出更友好的提示
if (error.message.includes('confirmation email') || error.message.includes('邮件')) {
errorMessage = '注册可能成功,但邮件发送失败,请稍后尝试登录'
}
}
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
})
} finally {
isLoading.value = false
}
}
// 跳转到登录页
const navigateToLogin = () => {
const pages = getCurrentPages() as any[]
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
const opts = currentPage?.options as any
const redirect = opts?.redirect as string | null
if (redirect != null && redirect.length > 0) {
uni.navigateTo({
url: `/pages/user/login?redirect=${redirect}`
})
} else {
uni.navigateTo({
url: '/pages/user/login'
})
}
}
// 跳转到协议页面
const navigateToTerms = (type: number) => {
uni.navigateTo({
url: `/pages/user/terms?type=${type}`
})
}
</script>
<style>
page {
background: #F5F5F5;
}
.register-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #F5F5F5;
}
/* Header Logo */
.header {
padding: 40rpx 0 0 60rpx;
background: #F5F5F5;
}
.logo {
width: 200rpx;
height: 80rpx;
}
/* 注册表单区域 */
.register-box {
flex: 1;
background: #FFFFFF;
margin: 60rpx 40rpx 0;
border-radius: 8rpx;
padding: 60rpx 50rpx 40rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
text-align: center;
margin-bottom: 50rpx;
}
/* 表单内容 */
.form-content {
margin-bottom: 40rpx;
}
.input-group {
margin-bottom: 30rpx;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
padding: 0 20rpx;
height: 88rpx;
border: 1rpx solid #E0E0E0;
border-radius: 4rpx;
background: #FFFFFF;
}
.input-wrapper:focus-within {
border-color: var(--view-theme, #FF4D4F);
}
.input-icon {
width: 32rpx;
height: 32rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.input-field {
flex: 1;
font-size: 28rpx;
height: 88rpx;
line-height: 88rpx;
color: #333333;
display: flex;
align-items: center;
}
.code-input {
flex: 1;
}
.code-btn {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
color: var(--view-theme, #FF4D4F);
font-size: 26rpx;
background: transparent;
border: none;
padding: 0;
line-height: 1;
}
.code-btn.disabled {
color: #999999;
}
/* 注册按钮 */
.register-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 88rpx;
margin-top: 50rpx;
background: linear-gradient(135deg, #FF4D4F 0%, #FF7A45 100%);
border-radius: 4rpx;
color: #FFFFFF;
font-size: 32rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3);
}
.register-btn.disabled {
background: #D9D9D9;
box-shadow: none;
opacity: 0.6;
}
/* 已有账号提示 */
.tips {
margin-top: 30rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.tips-text {
font-size: 28rpx;
color: #666666;
}
.tips-link {
font-size: 28rpx;
color: var(--view-theme, #FF4D4F);
margin-left: 8rpx;
}
/* 协议区域 */
.protocol {
margin-top: 40rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.protocol checkbox-group {
display: flex;
flex-direction: row;
align-items: center;
}
.protocol checkbox {
margin-right: 10rpx;
}
.protocol-text {
font-size: 24rpx;
color: #999999;
}
.main-color {
color: var(--view-theme, #FF4D4F);
}
.trembling {
animation: shake 0.6s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
20%, 40%, 60%, 80% { transform: translateX(10rpx); }
}
/* 底部版权 */
.footer {
padding: 40rpx 0;
text-align: center;
background: #F5F5F5;
}
.footer-text {
font-size: 22rpx;
color: #999999;
}
</style>