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

647 lines
15 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 { onLoad } from '@dcloudio/uni-app'
import supa from '@/components/supadb/aksupainstance.uts'
import { AkReq } from '@/uni_modules/ak-req/index.uts'
import { PUSH_SERVER_URL } from '@/ak/config.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 registerMode = ref<string>('consumer')
const getOptionText = (opts: UTSJSONObject, key: string): string => {
try {
const value = opts.getString(key)
if (value != null && value != '') {
return value
}
} catch (e) {}
try {
const rawValue = opts[key]
if (rawValue != null) {
return '' + rawValue
}
} catch (e) {}
return ''
}
const getRegisterRole = (): string => {
if (registerMode.value == 'delivery') {
return 'delivery'
}
if (registerMode.value == 'merchant') {
return 'merchant'
}
return 'customer'
}
onLoad((opts) => {
if (opts != null) {
const optsObj = opts as UTSJSONObject
const mode = getOptionText(optsObj, 'mode')
if (mode == 'delivery') {
registerMode.value = 'delivery'
uni.setNavigationBarTitle({ title: '服务人员注册' })
} else if (mode == 'merchant') {
registerMode.value = 'merchant'
uni.setNavigationBarTitle({ title: '商家注册' })
} else {
registerMode.value = 'consumer'
uni.setNavigationBarTitle({ title: '用户注册' })
}
}
})
const handleProtocolChange = (e: UniCheckboxGroupChangeEvent): void => {
protocol.value = protocol.value == false
}
const validateEmail = (): boolean => {
if (email.value.trim() == '') {
uni.showToast({
title: '请填写邮箱',
icon: 'none'
})
return false
}
const atIndex = email.value.indexOf('@')
const dotIndex = email.value.lastIndexOf('.')
if (atIndex == -1 || dotIndex == -1 || atIndex > dotIndex) {
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
}
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 (): Promise<void> => {
if (protocol.value == false) {
inAnimation.value = true
uni.showToast({
title: '请先阅读并同意协议',
icon: 'none'
})
return
}
if (validateEmail() == false) {
return
}
if (validatePassword() == false) {
return
}
if (validateConfirmPassword() == false) {
return
}
isLoading.value = true
try {
// 使用 Supabase Auth邮箱 + 密码注册
const options = new UTSJSONObject()
const metaData = new UTSJSONObject()
metaData.set('user_role', getRegisterRole())
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') ?? result?.getString('message') ?? ''
const codeStr = result?.getString('code') ?? ''
let codeNum = 0
try {
codeNum = result?.getNumber('code') ?? 0
} catch(e) {
// 忽略转换异常
}
// 1. 明确判断是否为账户已存在 (PostgreSQL code 23505 或包含 duplicate/already exists)
if (codeStr === '23505' || errorCode === 'user_already_exists' || errorMsg.includes('already exists') || errorMsg.includes('duplicate')) {
throw new Error('该账户已经存在,请更换账户')
}
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
}
}
}
// 2. 如果没有返回能够标识用户的字段,应当直接按失败处理
if (user == null) {
if ((codeNum === 500 || codeStr === '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)
}
}
// 获取并上报推送 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 {
const uidStored = uni.getStorageSync('user_id') || null
const currentSession = supa.getSession()
const currentUid = uidStored || (currentSession && currentSession.user ? currentSession.user.getString('id') : null)
await AkReq.request({
url: `${PUSH_SERVER_URL}/api/v1/push/register`,
method: 'POST',
data: { cid, platform: 'android', user_id: currentUid },
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(() => {
let url = '/pages/user/login'
if (registerMode.value == 'delivery') {
url = '/pages/user/login?mode=delivery'
} else if (registerMode.value == 'merchant') {
url = '/pages/user/login?mode=merchant'
}
uni.redirectTo({
url
})
}, 1500)
} catch (err) {
console.error('注册错误:', err)
let errorMessage = '注册失败,请重试'
if (err != null) {
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 = (): void => {
let url = '/pages/user/login'
if (registerMode.value == 'delivery') {
url = '/pages/user/login?mode=delivery'
} else if (registerMode.value == 'merchant') {
url = '/pages/user/login?mode=merchant'
}
uni.navigateTo({
url
})
}
const navigateToTerms = (type: number): void => {
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>