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

654 lines
17 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" :style="cssVars">
<!-- Header仅保留左侧Logo -->
<view class="header">
<view class="header-left">
<image :src="logoUrl" mode="aspectFit" class="logo" />
</view>
</view>
<!-- Main -->
<view class="main">
<view class="card">
<!-- Left扫码区>=768显示 -->
<view class="left">
<text class="left-title">APP 扫码登录</text>
<view class="left-hint">
<text class="hint-text">打开 APP 扫一扫</text>
<text class="hint-link" @click="handleTutorial">查看教程</text>
</view>
<view class="qr-wrap">
<view class="qr">
<view class="qr-placeholder">
<text class="qr-text">二维码占位</text>
<text class="qr-sub">220×220</text>
</view>
</view>
</view>
</view>
<!-- Divider -->
<view class="divider"></view>
<!-- Right表单区 -->
<view class="right">
<view class="right-inner">
<!-- Tabs -->
<view class="tabs">
<view class="tab" :class="{ active: loginType === 0 }" @click="loginType = 0">
<text class="tab-text">密码登录</text>
<view v-if="loginType === 0" class="tab-line"></view>
</view>
<view class="tab" :class="{ active: loginType === 1 }" @click="loginType = 1">
<text class="tab-text">短信登录</text>
<view v-if="loginType === 1" class="tab-line"></view>
</view>
</view>
<!-- Form -->
<view class="form">
<template v-if="loginType === 0">
<view class="field">
<input
class="input"
type="text"
placeholder="账号名/手机号/邮箱"
v-model="account"
/>
</view>
<view class="field">
<input
class="input"
type="password"
placeholder="密码"
v-model="password"
/>
</view>
</template>
<template v-else>
<view class="field">
<input
class="input"
type="text"
placeholder="输入手机号码"
maxlength="11"
v-model="account"
/>
</view>
<view class="field code-row">
<input
class="input code-input"
type="text"
placeholder="填写验证码"
maxlength="6"
v-model="captcha"
/>
<view class="code-btn" :class="codeDisabled ? 'disabled' : ''" @click="getCode">
<text class="code-text">{{ codeText }}</text>
</view>
</view>
</template>
<!-- Button -->
<view class="btn" :class="{ disabled: isLoading }" @click="handleLogin">
<text class="btn-text">登录</text>
</view>
<!-- Actions一行横排 -->
<view class="actions">
<view class="action-item" @click="handleWechatLogin">
<view class="dot wechat"></view>
<text class="action-text">微信登录</text>
</view>
<text class="sep">|</text>
<view class="action-item" @click="handleQQLogin">
<view class="dot qq"></view>
<text class="action-text">QQ登录</text>
</view>
<text class="sep">|</text>
<text class="action-link" @click="handleForgotPassword">忘记密码</text>
<text class="sep">|</text>
<text class="action-link" @click="navigateToRegister">立即注册</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- Footer -->
<view class="footer">
<text class="footer-text">Copyright ©2024 Mall. All Rights Reserved</text>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { IS_TEST_MODE } from '@/ak/config.uts'
import { getCurrentUser, logout, setIsLoggedIn, setUserProfile } from '@/utils/store.uts'
import type { UserProfile } from '@/types/mall-types.uts'
const cssVars = {
'--bg': '#f5f6f8',
'--card': '#ffffff',
'--brand': '#e1251b',
'--text': '#333333',
'--muted': '#666666',
'--muted2': '#999999',
'--border': '#eeeeee',
'--inputbg': '#f6f7f9',
'--shadow': '0 2px 12px rgba(0,0,0,0.06)'
}
const logoUrl = ref<string>('/static/logo.png')
const loginType = ref<number>(0)
const account = ref<string>('')
const password = ref<string>('')
const captcha = ref<string>('')
const isLoading = ref<boolean>(false)
const codeDisabled = ref<boolean>(false)
const codeText = ref<string>('获取验证码')
const codeTimer = ref<number>(0)
const codeCountdown = ref<number>(0)
const checkLoginStatus = (): void => {
try {
if (IS_TEST_MODE) return
const sessionInfo = supa.getSession()
if (sessionInfo != null && sessionInfo.user != null) {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const opts = currentPage.options as UTSJSONObject
const redirect = opts.getString('redirect')
if (redirect != null && redirect != '') {
uni.reLaunch({ url: `/pages/mall/consumer/index` })
} else {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
}
} else {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
}
}
} catch (e) {
console.error('检查登录状态失败:', e)
}
}
onMounted(() => {
checkLoginStatus()
})
const validateAccount = (): boolean => {
if (account.value.trim() === '') {
uni.showToast({ title: '请填写账号', icon: 'none' })
return false
}
if (loginType.value === 1) {
if (!/^1[3-9]\d{9}$/.test(account.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
}
return true
}
const validateCaptcha = (): boolean => {
if (captcha.value.trim() === '') {
uni.showToast({ title: '请填写验证码', icon: 'none' })
return false
}
if (!/^\d{6}$/.test(captcha.value)) {
uni.showToast({ title: '请输入正确的验证码', icon: 'none' })
return false
}
return true
}
const getCode = async () => {
if (codeDisabled.value) return
if (!validateAccount()) return
uni.showToast({ title: '验证码已发送', icon: 'success' })
codeDisabled.value = true
codeCountdown.value = 60
codeText.value = `${codeCountdown.value}秒后重试`
codeTimer.value = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value > 0) {
codeText.value = `${codeCountdown.value}秒后重试`
} else {
codeDisabled.value = false
codeText.value = '获取验证码'
if (codeTimer.value != 0) {
clearInterval(codeTimer.value)
codeTimer.value = 0
}
}
}, 1000) as number
}
const handleLogin = async () => {
if (!validateAccount()) return
// 特殊账号处理admin/admin 直接跳转
if (account.value === 'admin' && password.value === 'admin') {
setIsLoggedIn(true)
const adminProfile = {
id: 'admin',
username: 'Admin',
email: 'admin@mall.com',
gender: 'unknown',
birthday: '',
height_cm: 0,
weight_kg: 0,
bio: 'Administrator',
avatar_url: '/static/logo.png',
preferred_language: 'zh-CN',
role: 'admin',
school_id: '',
grade_id: '',
class_id: ''
} as UserProfile
setUserProfile(adminProfile)
uni.showToast({ title: '管理员登录成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
}, 500)
return
}
if (loginType.value === 0) {
if (!validatePassword()) return
} else {
if (!validateCaptcha()) return
}
isLoading.value = true
try {
logout()
if (loginType.value === 0) {
const isEmail = account.value.includes('@')
if (isEmail) {
// 邮箱 + 密码登录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') ?? ''
if (errorMsg.includes('email') && errorMsg.includes('confirm') ||
errorCode === 'email_not_confirmed' ||
errorMsg.includes('邮箱') && errorMsg.includes('确认')) {
throw new Error('邮箱未确认,请先检查邮箱并点击确认链接')
} else if (errorMsg.includes('Invalid login credentials') ||
errorCode === 'invalid_credentials') {
throw new Error('邮箱或密码错误')
} else {
throw new Error(errorMsg != '' ? errorMsg : '登录失败,请重试')
}
}
} else {
uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' })
return
}
} else {
uni.showToast({ title: '手机验证码登录功能开发中', icon: 'none' })
return
}
// 尝试获取/补全用户资料,但失败时不再阻塞登录
try {
const profile = await getCurrentUser()
console.log('current user profile:', profile)
} catch (e) {
console.error('获取用户信息失败(忽略,不阻塞登录):', e)
}
// 显式保存用户ID到本地存储确保页面刷新或重启后 SupabaseService 能恢复身份
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)
}
}
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/mall/consumer/index' })
}, 500)
} catch (err) {
console.error('登录错误:', err)
let msg = '登录失败,请重试'
// UTS 不支持 typeof 检查,直接尝试转换
try {
const e = err as Error
if (e.message != null && e.message.trim() !== '') msg = e.message
} catch (e2) {
// 忽略转换错误,使用默认消息
}
uni.showToast({ title: msg, icon: 'none' })
} finally {
isLoading.value = false
}
}
const navigateToRegister = (): void => {
uni.navigateTo({
url: '/pages/user/register'
})
}
const handleTutorial = () => uni.showToast({ title: '扫码教程开发中', icon: 'none' })
const handleForgotPassword = () => uni.showToast({ title: '忘记密码开发中', icon: 'none' })
const handleWechatLogin = () => uni.showToast({ title: '微信登录开发中', icon: 'none' })
const handleQQLogin = () => uni.showToast({ title: 'QQ登录开发中', icon: 'none' })
</script>
<style scoped>
/* Base */
.page{
/* min-height: 100vh; */ /* UVUE 不支持 vh */
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f6f8; /* UVUE 暂不支持 cssVars 在 style 标签中的变量引用 */
}
/* Header */
.header{
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
padding: 30px 40px; /* 调整边距 */
}
.logo{
width: 240px;
height: 64px;
}
/* Main */
.main{
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 20px 10px;
}
/* Card */
.card{
/* width: min(980px, 92vw); UVUE 不支持 min/vw */
/* min-height: 460px; */
width: 90%;
background-color: #ffffff;
border-radius: 16px;
/* box-shadow: var(--shadow); */
padding: 30px;
display: flex;
flex-direction: column; /* App端改为列式布局兼容性更好或者用 row 需注意 */
/* gap: 32px; UVUE 不支持 gap */
}
/* Left - 暂隐藏或简化 */
/* .left{ display: none; } */
.left{
/* flex: 0 0 52%; UVUE flex 简写支持不全,建议用 flex-grow/basis */
flex-direction: column;
align-items: flex-start;
justify-content: center;
/* padding-left: 18px; */
display: none; /* 移动端 App 暂时隐藏扫码区 */
}
.left-title{
font-size: 18px;
font-weight: 700; /* 600 -> 700 */
color: #333333;
margin-bottom: 10px;
}
.left-hint{
display: flex;
flex-direction: row;
align-items: center;
/* gap: 14px; */
margin-bottom: 18px;
}
/* 替代 gap */
.hint-text{ font-size: 13px; color: #666666; margin-right: 14px; }
.hint-link{ font-size: 13px; color: #e1251b; }
.qr-wrap{
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.qr{
width: 240px;
height: 240px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.qr-placeholder{
width: 220px;
height: 220px;
border: 1px solid #e6e6e6;
border-radius: 8px;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* gap: 8px; */
}
/* 替代 gap */
.qr-text{ font-size: 14px; color: #666666; margin-bottom: 8px; }
.qr-sub{ font-size: 12px; color: #999999; }
/* Divider */
.divider{
display: none; /* 移动端隐藏分割线 */
}
/* Right */
.right{
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.right-inner{
/* width: 360px; */
/* max-width: 100%; UVUE 不支持百分比 max-width */
width: 100%;
margin-left: auto;
}
/* Tabs */
.tabs{
display: flex;
flex-direction: row;
align-items: center;
justify-content: center; /* 移动端居中 */
/* gap: 24px; */
margin-bottom: 18px;
}
.tab{
position: relative;
padding: 8px 12px; /* 增加内边距替代 gap */
margin: 0 12px;
}
.tab-text{
font-size: 16px;
color: #666666;
}
.tab.active .tab-text{
color: #e1251b;
font-weight: 700; /* 600 -> 700 */
}
.tab-line{
position: absolute;
left: 0;
right: 0;
bottom: -6px;
height: 2px;
background-color: #e1251b;
border-radius: 2px;
}
/* Form */
.form{ margin-top: 10px; }
.field{ margin-bottom: 14px; }
.input{
width: 100%;
height: 44px;
border-radius: 10px;
background-color: #f6f7f9;
padding: 0 14px;
font-size: 14px;
color: #333333;
/* box-sizing: border-box; */ /* App-UVUE 默认就是 border-box */
}
/* Code row */
.code-row{
display: flex;
flex-direction: row;
align-items: center;
/* gap: 10px; */
}
.code-input{ flex: 1; margin-right: 10px; }
.code-btn{
height: 44px;
padding: 0 12px;
border-radius: 10px;
background-color: #fff;
border: 1px solid #eee;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.code-btn.disabled{ opacity: 0.5; }
.code-text{ font-size: 13px; color: #e1251b; }
/* Button */
.btn{
margin-top: 16px;
height: 46px;
border-radius: 10px;
background-color: rgba(225, 37, 27, 0.45); /* 注意 rgba 兼容性,建议用 hex 或 view opacity */
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.btn.disabled{ background-color: #d9d9d9; }
.btn-text{
color: #fff;
font-size: 16px;
font-weight: 700; /* 600 -> 700 */
}
/* Actions一行横排 */
.actions{
margin-top: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
/* gap: 12px; */
flex-wrap: wrap; /* 允许换行 */
}
.action-item{
display: flex;
flex-direction: row;
align-items: center;
/* gap: 8px; */
margin: 0 6px;
}
.dot{
width: 16px;
height: 16px;
border-radius: 8px; /* 50% -> 8px (一半) */
margin-right: 8px;
}
.dot.wechat{ background-color: #19be6b; }
.dot.qq{ background-color: #2d8cf0; }
.action-text{ font-size: 13px; color: #666666; }
.action-link{ font-size: 13px; color: #666666; margin: 0 6px; }
.sep{ font-size: 13px; color: #e0e0e0; margin: 0 6px; }
/* Footer */
.footer{
padding: 18px 0 28px;
display: flex;
flex-direction: row;
justify-content: center;
}
.footer-text{ font-size: 12px; color: #999999; }
/* ===== 自适应:移除复杂 Media Query使用简单流式布局 ===== */
/*
@media screen and (max-width: 1024px){
.header{ padding: 24px 20px; }
.logo{ width: 240px; height: 68px; }
.card{ width: 92vw; padding: 28px; gap: 22px; }
.right-inner{ width: 360px; }
}
@media screen and (max-width: 768px){
.card{ flex-direction: column; min-height: auto; }
.left{ display: none; }
.divider{ display: none; }
.right-inner{ width: 100%; margin-left: 0; }
.actions{ flex-wrap: wrap; }
}
@media screen and (max-width: 520px){
.sep{ display: none; }
}
*/
</style>