Files
medical-mall/pages/user/register.uvue

579 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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="page">
<!-- Header Logo -->
<view class="header">
<view class="header-inner">
<image :src="logoUrl" mode="aspectFit" class="logo" />
<!-- 已有账号 -->
<view class="header-right">
<text class="tips-text">已有账号?</text>
<text class="tips-link" @click="navigateToLogin">立即登录</text>
</view>
</view>
</view>
<!-- 注册表单区域 -->
<view class="main">
<view class="register-box">
<text class="title">注册账号</text>
<!-- 注册表单 -->
<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="isPasswordVisible ? 'text' : 'password'"
placeholder="填写密码"
:value="password"
@input="(e: any) => password = e.detail.value"
class="input-field"
/>
<view class="eye-btn" @click="isPasswordVisible = !isPasswordVisible">
<!-- 睁眼表示可见, 闭眼表示不可见 -->
<text class="eye-icon">{{ isPasswordVisible ? '👁️' : '🙈' }}</text>
</view>
</view>
</view>
<!-- 确认密码 -->
<view class="input-group">
<view class="input-wrapper">
<image src="/static/user/code_1.png" class="input-icon" />
<input
:type="isConfirmPasswordVisible ? 'text' : 'password'"
placeholder="确认密码"
:value="confirmPassword"
@input="(e: any) => confirmPassword = e.detail.value"
class="input-field"
/>
<view class="eye-btn" @click="isConfirmPasswordVisible = !isConfirmPasswordVisible">
<text class="eye-icon">{{ isConfirmPasswordVisible ? '👁️' : '🙈' }}</text>
</view>
</view>
</view>
</view>
<!-- 注册按钮 -->
<view class="register-btn" @click="handleRegister" :class="{ 'disabled': isLoading }">
<text class="btn-text">注册</text>
</view>
<!-- 协议勾选 -->
<view class="protocol">
<checkbox-group class="protocol-group" @change="handleProtocolChange">
<checkbox
:checked="protocol"
class="protocol-checkbox"
: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>
<!-- 底部版权信息 -->
<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'
// 响应式数据
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 isPasswordVisible = ref<boolean>(false)
const isConfirmPasswordVisible = 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 options = new UTSJSONObject()
const metaData = new UTSJSONObject()
// 【核心修改】:商家端注册时,固定声明 user_role 为 'merchant'
// 该元数据会被 Supabase Auth 存储,并由数据库触发器自动同步到 ak_users 业务表的 role 字段
metaData.set('user_role', 'merchant')
options.set('data', metaData)
const result = await supa.signUp(email.value.trim(), password.value, options)
console.log('📝 注册返回结果:', result)
// 检查是否有错误
const errorCode = result?.getString('error_code') ?? ''
const errorMsg = result?.getString('msg') ?? ''
const code = result?.getNumber('code') ?? 0
let user: UTSJSONObject | null = null
if (result != null) {
const userField = result.getJSON('user')
if (userField != null) {
user = userField
} else {
const id = result.getString('id')
if (id != null && id !== '') {
user = result
}
}
}
// 如果返回错误且没有用户信息,说明注册失败
if (user == null && code !== 0 && code !== 200) {
if (code === 500 && errorMsg.includes('confirmation email')) {
throw new Error('注册失败:邮件服务配置错误')
} else {
throw new Error(errorMsg || '注册失败,请重试')
}
}
// 【核心修改】:移除手动调用 ensureUserProfile 逻辑
// 既然已经设置了数据库触发器 (Trigger),用户信息会自动从 auth.users 同步到 ak_users
// 前端不再执行二次插入,避免并发冲突或重复写入
if (user != null) {
try {
// 注册后立即登出,确保用户必须通过登录流程(且经过角色校验)才能进入首页
await supa.signOut()
} catch (signOutError) {
console.error('❌ 登出异常:', signOutError)
}
}
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>
/* Base Layout */
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
}
/* Header Area */
.header {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
border-bottom: 1px solid #EEEEEE;
background-color: #FFFFFF;
}
.header-inner {
width: min(1200px, 92vw);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 24px 0;
}
.logo {
width: 180px;
height: 64px;
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
}
.tips-text {
font-size: 15px;
color: #666666;
}
.tips-link {
font-size: 15px;
color: var(--view-theme, #FF4D4F);
margin-left: 8px;
cursor: pointer;
}
/* Main Form Area */
.main {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
padding-top: 80px;
background-color: #FFFFFF;
}
.register-box {
width: 420px;
max-width: 92vw;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
}
.title {
font-size: 26px;
font-weight: 600;
color: #333333;
text-align: center;
margin-bottom: 40px;
}
/* Form Content */
.form-content {
display: flex;
flex-direction: column;
width: 100%;
}
.input-group {
margin-bottom: 24px;
width: 100%;
}
.input-wrapper {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
height: 48px;
border: 1px solid #D9D9D9;
border-radius: 4px;
background-color: #FFFFFF;
padding: 0 16px;
transition-property: border-color, box-shadow;
transition-duration: 0.3s;
}
.input-wrapper:focus-within {
border-color: var(--view-theme, #FF4D4F);
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.1);
}
.input-icon {
width: 22px;
height: 22px;
margin-right: 12px;
opacity: 0.4;
}
.input-field {
flex: 1;
height: 100%;
font-size: 15px;
color: #333333;
background: transparent;
border: none;
outline: none;
}
.input-field::placeholder {
color: #BFBFBF;
}
/* Eye Button */
.eye-btn {
padding: 0 8px;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
}
.eye-icon {
font-size: 18px;
color: #999999;
}
/* Register Button */
.register-btn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 48px;
margin-top: 10px;
background: linear-gradient(90deg, var(--view-theme, #FF4D4F) 0%, #FF7A45 100%);
border-radius: 4px;
cursor: pointer;
}
.register-btn.disabled {
opacity: 0.6;
cursor: not-allowed;
background: #D9D9D9;
}
.btn-text {
color: #FFFFFF;
font-size: 16px;
font-weight: 500;
letter-spacing: 2px;
}
/* Protocol */
.protocol {
margin-top: 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
}
.protocol-group {
display: flex;
flex-direction: row;
align-items: center;
}
.protocol-checkbox {
transform: scale(0.8);
margin-right: 4px;
}
.protocol-text {
font-size: 13px;
color: #666666;
}
.main-color {
font-size: 13px;
color: var(--view-theme, #FF4D4F);
cursor: pointer;
}
.trembling {
animation: shake 0.6s;
}
@keyframes shake {
0%, 100% { transform: translateX(0) scale(0.8); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) scale(0.8); }
20%, 40%, 60%, 80% { transform: translateX(4px) scale(0.8); }
}
/* Footer */
.footer {
padding: 40px 0;
display: flex;
flex-direction: row;
justify-content: center;
}
.footer-text {
font-size: 13px;
color: #999999;
}
/* Responsive */
@media screen and (max-width: 768px) {
.header-inner {
padding: 16px 20px;
}
.logo {
width: 140px;
height: 50px;
}
.main {
padding-top: 40px;
}
.register-box {
width: 100%;
padding: 0 24px;
}
.title {
font-size: 24px;
margin-bottom: 30px;
}
}
</style>