647 lines
15 KiB
Plaintext
647 lines
15 KiB
Plaintext
<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>
|