564 lines
14 KiB
Plaintext
564 lines
14 KiB
Plaintext
<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'
|
||
import { AkReq } from '@/uni_modules/ak-req/index.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 返回的是 UTSJSONObject,Supabase 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('ℹ️ 需要邮箱验证,验证后登录时会自动创建用户资料')
|
||
}
|
||
|
||
// 获取并上报推送 CID(若可用)
|
||
try {
|
||
uni.getPushClientId({
|
||
success: async (res: any) => {
|
||
const cid = (res && (res.clientid || res.cid)) ? (res.clientid || res.cid) : (typeof res === 'string' ? res : null)
|
||
if (cid != null && cid !== '') {
|
||
try { uni.setStorageSync('uni_push2_cid', cid) } catch (e) {}
|
||
try {
|
||
await AkReq.request({
|
||
url: '/api/v1/push/register',
|
||
method: 'POST',
|
||
data: { cid, platform: 'android' },
|
||
contentType: 'application/json'
|
||
})
|
||
console.log('CID 已上报后台:', cid)
|
||
} catch (e) { console.warn('上报 CID 失败:', e) }
|
||
}
|
||
},
|
||
fail: (err: any) => { console.warn('获取 Push CID 失败:', err) }
|
||
})
|
||
} catch (e) {
|
||
console.warn('getPushClientId 调用异常:', e)
|
||
}
|
||
|
||
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: 100%;
|
||
color: #333333;
|
||
}
|
||
|
||
.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;
|
||
text-align: 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;
|
||
align-items: center;
|
||
justify-content: 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>
|