Merge remote-tracking branch 'origin/huangzhenbao-admin'
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="page" :style="cssVars">
|
||||
<!-- Header:仅保留左侧Logo -->
|
||||
<view class="header">
|
||||
@@ -60,14 +60,18 @@
|
||||
@input="(e: any) => account = e.detail.value"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field password-field">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
placeholder="密码"
|
||||
:value="password"
|
||||
@input="(e: any) => password = e.detail.value"
|
||||
/>
|
||||
<view class="eye-btn" @click="isPasswordVisible = !isPasswordVisible">
|
||||
<!-- 睁眼(猴子双手打开)表示可见, 闭眼(猴子捂眼)表示不可见 -->
|
||||
<text class="eye-icon">{{ isPasswordVisible ? '🙉' : '🙈' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -141,11 +145,11 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { AkReq } from '@/uni_modules/ak-req/index.uts'
|
||||
import { IS_TEST_MODE, PUSH_SERVER_URL } from '@/ak/config.uts'
|
||||
import { IS_TEST_MODE } from '@/ak/config.uts'
|
||||
import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/store.uts'
|
||||
import { UserProfile } from '@/pages/user/types.uts'
|
||||
import { ensureUserProfile } from '@/utils/sapi.uts'
|
||||
import type { UserProfile } from '@/pages/user/types.uts'
|
||||
import { AkReq } from '@/uni_modules/ak-req/index.uts'
|
||||
import { PUSH_SERVER_URL } from '@/ak/config.uts'
|
||||
|
||||
const cssVars = {
|
||||
'--bg': '#f5f6f8',
|
||||
@@ -165,9 +169,64 @@ const loginType = ref<number>(0)
|
||||
const account = ref<string>('')
|
||||
const password = ref<string>('')
|
||||
const captcha = ref<string>('')
|
||||
const isPasswordVisible = ref<boolean>(false)
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* 【核心函数】:登录成功后,多条件校验是否为商家角色
|
||||
* 优先级: session_uid (auth_id) -> id -> normalized email
|
||||
*/
|
||||
const checkAdminOrMerchantAccess = async (uid: string, rawEmail: string) : Promise<UTSJSONObject | null> => {
|
||||
const email = rawEmail.trim().toLowerCase()
|
||||
console.log(`🔍 开始校验后台或商家端角色 -> UID: ${uid}, Email: ${email}`)
|
||||
|
||||
const parseRoleData = (dataArray: any | null): UTSJSONObject | null => {
|
||||
if (Array.isArray(dataArray) && dataArray.length > 0) {
|
||||
const obj = dataArray[0] as UTSJSONObject
|
||||
const role = obj.getString('role')
|
||||
const id = obj.getString('id')
|
||||
console.log('✅ 匹配成功,role:', role)
|
||||
if ((role === 'merchant' || role === 'admin') && id != null) {
|
||||
return { id, role } as UTSJSONObject
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 尝试按 auth_id 查询
|
||||
let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute()
|
||||
let parsed = parseRoleData(res.data)
|
||||
if (parsed != null) return parsed
|
||||
|
||||
// 2. 尝试按 id 查询 (兼容老数据)
|
||||
res = await supa.from('ak_users').select('id, role').eq('id', uid).execute()
|
||||
parsed = parseRoleData(res.data)
|
||||
if (parsed != null) return parsed
|
||||
|
||||
// 3. 尝试按 email 兜底查询
|
||||
if (email !== '') {
|
||||
res = await supa.from('ak_users').select('id, role').eq('email', email).execute()
|
||||
const dataArray = res.data
|
||||
if (Array.isArray(dataArray) && dataArray.length > 1) {
|
||||
console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录,取第一条校验。Email:', email)
|
||||
}
|
||||
parsed = parseRoleData(dataArray)
|
||||
if (parsed != null) return parsed
|
||||
}
|
||||
|
||||
console.error('❌ 未能在 ak_users 中找到该用户的有效角色记录')
|
||||
throw new Error('NOT_REGISTERED')
|
||||
} catch (e) {
|
||||
console.error('❌ 查询角色过程异常:', e)
|
||||
if (e instanceof Error && e.message === 'NOT_REGISTERED') {
|
||||
throw new Error('该账户无后台或商家端权限,请联系管理员核对')
|
||||
}
|
||||
throw new Error('后台身份校验失败,请联系管理员检查用户数据')
|
||||
}
|
||||
}
|
||||
|
||||
const codeDisabled = ref<boolean>(false)
|
||||
const codeText = ref<string>('获取验证码')
|
||||
let codeTimer: number | null = null
|
||||
@@ -175,20 +234,20 @@ const codeCountdown = ref<number>(0)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
// 检查是否已有 Session
|
||||
const sessionInfo = supa.getSession()
|
||||
if (sessionInfo != null && sessionInfo.user != null) {
|
||||
// 生产模式或主动进入登录页时,若已登录则尝试跳转回原页面或首页
|
||||
// 在测试模式下,依然执行自动重定向,方便登录成功后的跳转逻辑
|
||||
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
|
||||
|
||||
// 注意:IS_TEST_MODE 仅在 boot 页禁止自动跳转,在登录页若已有 Session,建议还是允许其进入首页
|
||||
console.log('检测到已有会话, 执行重定向...')
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.redirectTo({ url: decodeURIComponent(redirect) })
|
||||
} else if (!IS_TEST_MODE) {
|
||||
// 非测试模式下,自动跳转首页
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/mall/admin/homePage/index' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -262,7 +321,7 @@ const getCode = async () => {
|
||||
const handleLogin = async () => {
|
||||
if (!validateAccount()) return
|
||||
|
||||
// 特殊账号处理:仅在测试模式允许本地绕过登录(不推荐生产使用)
|
||||
// 特殊账号处理:仅在测试模式下保留直登逻辑,生产环境强制校验角色
|
||||
if (IS_TEST_MODE && account.value === 'admin' && password.value === 'admin') {
|
||||
setIsLoggedIn(true)
|
||||
const adminProfile = {
|
||||
@@ -282,11 +341,11 @@ const handleLogin = async () => {
|
||||
class_id: ''
|
||||
} as UserProfile
|
||||
setUserProfile(adminProfile)
|
||||
uni.setStorageSync('user_id', 'admin')
|
||||
// uni.setStorageSync('merchant_id', 'admin') // mock removed
|
||||
|
||||
uni.showToast({ title: '管理员登录成功(测试模式)', icon: 'success' })
|
||||
uni.showToast({ title: '管理员登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
uni.reLaunch({ url: '/pages/mall/admin/homePage/index' })
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
@@ -304,13 +363,12 @@ const handleLogin = async () => {
|
||||
if (loginType.value === 0) {
|
||||
const isEmail = account.value.includes('@')
|
||||
if (isEmail) {
|
||||
// 邮箱 + 密码登录(Supabase Auth)
|
||||
// 1) 调用 Supabase Auth 登录
|
||||
const result = await supa.signIn(account.value.trim(), password.value)
|
||||
console.log('signIn result:', result)
|
||||
|
||||
// 检查登录是否失败
|
||||
if (result.user == null) {
|
||||
// 检查是否是邮箱未确认的错误
|
||||
const rawData = result.raw as UTSJSONObject
|
||||
const errorMsg = rawData?.getString('msg') ?? ''
|
||||
const errorCode = rawData?.getString('error_code') ?? ''
|
||||
@@ -320,12 +378,35 @@ const handleLogin = async () => {
|
||||
errorMsg.includes('邮箱') && errorMsg.includes('确认')) {
|
||||
throw new Error('邮箱未确认,请先检查邮箱并点击确认链接')
|
||||
} else if (errorMsg.includes('Invalid login credentials') ||
|
||||
errorCode === 'invalid_credentials') {
|
||||
errorCode === 'invalid_credentials' ||
|
||||
errorMsg.includes('Invalid credentials')) {
|
||||
throw new Error('邮箱或密码错误')
|
||||
} else {
|
||||
throw new Error(errorMsg || '登录失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 【核心逻辑】:执行商家端角色准入校验
|
||||
// 优先使用 session user id 来查询数据库里的真实 user 数据兜底校验
|
||||
const sessionUser = result.user
|
||||
let sessionUid = sessionUser?.getString('id') ?? ''
|
||||
|
||||
const accessData = await checkAdminOrMerchantAccess(sessionUid, account.value)
|
||||
if (accessData == null) {
|
||||
await supa.signOut()
|
||||
logout()
|
||||
throw new Error('该账户无后台或商家端权限')
|
||||
}
|
||||
|
||||
const currRole = accessData.getString('role')
|
||||
const currId = accessData.getString('id')
|
||||
// uni.setStorageSync('adminRole', currRole) // 移除本地缓存依赖,强制按单例会话状态
|
||||
|
||||
if (currRole === 'merchant') {
|
||||
// uni.setStorageSync('merchant_id', currId) // 移除本地缓存依赖,强制按单例会话状态
|
||||
} else {
|
||||
uni.removeStorageSync('merchant_id')
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' })
|
||||
return
|
||||
@@ -335,71 +416,47 @@ const handleLogin = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功后强制同步用户资料到 ak_users(确保 auth_id 与 role 落表)
|
||||
// 更新 store 中的用户资料
|
||||
try {
|
||||
const sessionInfo = supa.getSession()
|
||||
if (sessionInfo?.user != null) {
|
||||
const syncResult = await ensureUserProfile(sessionInfo.user)
|
||||
console.log('ensureUserProfile sync result:', syncResult)
|
||||
}
|
||||
const profile = await getCurrentUser()
|
||||
console.log('fetch profile success:', profile)
|
||||
} catch (e) {
|
||||
console.error('同步用户资料到 ak_users 失败(不阻塞登录):', e)
|
||||
console.error('获取用户信息失败(忽略):', e)
|
||||
}
|
||||
|
||||
// 登录成功后按 redirect 参数跳转(守卫放在需要权限的页面入口)
|
||||
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.redirectTo({ url: decodeURIComponent(redirect) })
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
}
|
||||
|
||||
// 显式保存用户ID到本地存储,确保页面刷新或重启后 SupabaseService 能恢复身份
|
||||
// 已移除对 shared storage user_id 的依赖
|
||||
const currentSession = supa.getSession()
|
||||
if (currentSession.user != null) {
|
||||
const uid = currentSession.user?.getString('id')
|
||||
if (uid != null) {
|
||||
uni.setStorageSync('user_id', uid)
|
||||
console.log('用户ID已保存到本地存储:', uid)
|
||||
// 获取并上报推送 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 currentUid = uidStored || (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, 'user_id:', currentUid)
|
||||
} 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' })
|
||||
// 登录成功后的跳转不应受 IS_TEST_MODE 限制,否则用户点击登录后无反馈
|
||||
|
||||
// 获取并上报推送 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, 'user_id:', currentUid)
|
||||
} catch (e) { console.warn('上报 CID 失败:', e) }
|
||||
}
|
||||
},
|
||||
fail: (err: any) => { console.warn('获取 Push CID 失败:', err) }
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('getPushClientId 调用异常:', e)
|
||||
}
|
||||
|
||||
// 成功跳转逻辑
|
||||
setTimeout(() => {
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
@@ -408,7 +465,7 @@ const handleLogin = async () => {
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.redirectTo({ url: decodeURIComponent(redirect) })
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/mall/consumer/index' })
|
||||
uni.reLaunch({ url: '/pages/mall/admin/homePage/index' })
|
||||
}
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
@@ -599,15 +656,36 @@ const handleQQLogin = () => uni.showToast({ title: 'QQ登录开发中', icon: 'n
|
||||
/* Form */
|
||||
.form{ margin-top: 10px; }
|
||||
.field{ margin-bottom: 14px; }
|
||||
.password-field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.eye-btn {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.eye-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.input{
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--inputbg);
|
||||
padding: 0 14px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Code row */
|
||||
|
||||
@@ -1,86 +1,97 @@
|
||||
<template>
|
||||
<view class="register-wrapper">
|
||||
<view class="page">
|
||||
<!-- Header Logo -->
|
||||
<view class="header">
|
||||
<image :src="logoUrl" mode="aspectFit" class="logo" />
|
||||
<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="register-box">
|
||||
<view class="title">注册账号</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 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="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 class="register-btn" @click="handleRegister" :class="{ 'disabled': isLoading }">
|
||||
<text class="btn-text">注册</text>
|
||||
</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 class="protocol">
|
||||
<checkbox-group class="protocol-group" @change="handleProtocolChange">
|
||||
<checkbox
|
||||
:checked="protocol"
|
||||
class="protocol-checkbox"
|
||||
:class="{ 'trembling': inAnimation }"
|
||||
@animationend="inAnimation = false"
|
||||
/>
|
||||
</view>
|
||||
<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="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>
|
||||
|
||||
<!-- 底部版权信息 -->
|
||||
@@ -93,7 +104,6 @@
|
||||
<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'
|
||||
import { PUSH_SERVER_URL } from '@/ak/config.uts'
|
||||
|
||||
@@ -104,6 +114,8 @@
|
||||
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')
|
||||
|
||||
// 处理协议勾选变化
|
||||
@@ -204,95 +216,67 @@
|
||||
|
||||
try {
|
||||
// 使用 Supabase Auth:邮箱 + 密码注册
|
||||
const result = await supa.signUp(email.value.trim(), password.value)
|
||||
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)
|
||||
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 中创建
|
||||
// 这里我们仍然尝试创建用户资料
|
||||
// 兼容不同平台的报错字段
|
||||
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('该账户已经存在,请更换账户')
|
||||
}
|
||||
|
||||
// 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)')
|
||||
// 2. 如果没有返回能够标识用户的字段,应当直接按失败处理
|
||||
if (user == null) {
|
||||
if ((codeNum === 500 || codeStr === '500') && errorMsg.includes('confirmation email')) {
|
||||
throw new Error('注册失败:邮件服务配置错误')
|
||||
} else {
|
||||
throw new Error(errorMsg || '注册失败,请重试')
|
||||
throw new Error(errorMsg || '该账户可能已经存在,或注册请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果获取到 user,尝试创建业务侧用户资料(ak_users)
|
||||
// 【核心修改】:移除手动调用 ensureUserProfile 逻辑
|
||||
// 既然已经设置了数据库触发器 (Trigger),用户信息会自动从 auth.users 同步到 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)
|
||||
// 注册后立即登出,确保用户必须通过登录流程(且经过角色校验)才能进入首页
|
||||
await supa.signOut()
|
||||
} catch (signOutError) {
|
||||
console.error('❌ 登出异常:', signOutError)
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 注册成功但未获取到用户信息')
|
||||
// 可能需要邮箱验证,用户验证邮箱后登录时会自动创建资料
|
||||
}
|
||||
|
||||
// 如果注册后没有自动登录(需要邮箱验证),提示用户
|
||||
if (!hasSession && user != null) {
|
||||
console.log('ℹ️ 需要邮箱验证,验证后登录时会自动创建用户资料')
|
||||
}
|
||||
|
||||
// 获取并上报推送 CID(若可用)
|
||||
@@ -384,163 +368,208 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.register-wrapper {
|
||||
/* Base Layout */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #F5F5F5;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Header Logo */
|
||||
/* Header Area */
|
||||
.header {
|
||||
padding: 40rpx 0 0 60rpx;
|
||||
background: #F5F5F5;
|
||||
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: 200rpx;
|
||||
height: 80rpx;
|
||||
width: 180px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
/* 注册表单区域 */
|
||||
.register-box {
|
||||
.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;
|
||||
background: #FFFFFF;
|
||||
margin: 60rpx 40rpx 0;
|
||||
border-radius: 8rpx;
|
||||
padding: 60rpx 50rpx 40rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
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: 40rpx;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
margin-bottom: 50rpx;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* 表单内容 */
|
||||
/* Form Content */
|
||||
.form-content {
|
||||
margin-bottom: 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 30rpx;
|
||||
margin-bottom: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
height: 88rpx;
|
||||
border: 1rpx solid #E0E0E0;
|
||||
border-radius: 4rpx;
|
||||
background: #FFFFFF;
|
||||
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: 32rpx;
|
||||
height: 32rpx;
|
||||
flex-shrink: 0;
|
||||
margin-right: 20rpx;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 12px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
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;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-btn.disabled {
|
||||
.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: 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);
|
||||
height: 48px;
|
||||
margin-top: 10px;
|
||||
background: linear-gradient(90deg, var(--view-theme, #FF4D4F) 0%, #FF7A45 100%);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.register-btn.disabled {
|
||||
background: #D9D9D9;
|
||||
box-shadow: none;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #D9D9D9;
|
||||
}
|
||||
|
||||
/* 已有账号提示 */
|
||||
.tips {
|
||||
margin-top: 30rpx;
|
||||
text-align: center;
|
||||
.btn-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.tips-link {
|
||||
font-size: 28rpx;
|
||||
color: var(--view-theme, #FF4D4F);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* 协议区域 */
|
||||
/* Protocol */
|
||||
.protocol {
|
||||
margin-top: 40rpx;
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.protocol checkbox {
|
||||
margin-right: 10rpx;
|
||||
.protocol-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.protocol-checkbox {
|
||||
transform: scale(0.8);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.protocol-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.main-color {
|
||||
font-size: 13px;
|
||||
color: var(--view-theme, #FF4D4F);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trembling {
|
||||
@@ -548,20 +577,43 @@ page {
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(10rpx); }
|
||||
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: 40rpx 0;
|
||||
text-align: center;
|
||||
background: #F5F5F5;
|
||||
padding: 40px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 22rpx;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user