767 lines
21 KiB
Plaintext
767 lines
21 KiB
Plaintext
<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="账号名/手机号/邮箱"
|
||
:value="account"
|
||
@input="(e: any) => account = e.detail.value"
|
||
/>
|
||
</view>
|
||
<view class="field password-field">
|
||
<input
|
||
class="input"
|
||
: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>
|
||
|
||
<template v-else>
|
||
<view class="field">
|
||
<input
|
||
class="input"
|
||
type="text"
|
||
placeholder="输入手机号码"
|
||
maxlength="11"
|
||
:value="account"
|
||
@input="(e: any) => account = e.detail.value"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field code-row">
|
||
<input
|
||
class="input code-input"
|
||
type="text"
|
||
placeholder="填写验证码"
|
||
maxlength="6"
|
||
:value="captcha"
|
||
@input="(e: any) => captcha = e.detail.value"
|
||
/>
|
||
<view class="code-btn" :class="{ disabled: codeDisabled }" @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 '@/pages/user/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 isPasswordVisible = ref<boolean>(false)
|
||
|
||
const isLoading = ref<boolean>(false)
|
||
|
||
/**
|
||
* 【核心函数】:登录成功后,多条件校验是否为商家角色
|
||
* 优先级: session_uid (auth_id) -> id -> normalized email
|
||
*/
|
||
const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise<string | null> => {
|
||
const email = rawEmail.trim().toLowerCase()
|
||
console.log(`🔍 开始校验商家端角色 -> UID: ${uid}, Email: ${email}`)
|
||
|
||
try {
|
||
// 1. 尝试按 auth_id 查询
|
||
let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute()
|
||
let dataArray = res.data
|
||
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('✅ 按 auth_id 匹配成功,role:', role)
|
||
if (role === 'merchant' && id != null) return id
|
||
return null
|
||
}
|
||
|
||
// 2. 尝试按 id 查询 (兼容老数据)
|
||
res = await supa.from('ak_users').select('id, role').eq('id', uid).execute()
|
||
dataArray = res.data
|
||
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('✅ 按 id 匹配成功,role:', role)
|
||
if (role === 'merchant' && id != null) return id
|
||
return null
|
||
}
|
||
|
||
// 3. 尝试按 email 兜底查询
|
||
if (email !== '') {
|
||
res = await supa.from('ak_users').select('id, role').eq('email', email).execute()
|
||
dataArray = res.data
|
||
|
||
if (Array.isArray(dataArray) && dataArray.length > 0) {
|
||
// 如果按邮箱查出来多条,可能存在脏数据,只取第一条并记录日志
|
||
if (dataArray.length > 1) {
|
||
console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录,取第一条校验。Email:', email)
|
||
}
|
||
const obj = dataArray[0] as UTSJSONObject
|
||
const role = obj.getString('role')
|
||
const id = obj.getString('id')
|
||
console.log('✅ 按 email 匹配成功,role:', role)
|
||
if (role === 'merchant' && id != null) return id
|
||
return null
|
||
}
|
||
}
|
||
|
||
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('您还没有注册商家端账户,快去注册一个')
|
||
}
|
||
// 真实的查询异常/RLS异常抛出,防止误会为"未注册"
|
||
throw new Error('商家身份校验失败,请联系管理员检查用户数据')
|
||
}
|
||
}
|
||
|
||
const codeDisabled = ref<boolean>(false)
|
||
const codeText = ref<string>('获取验证码')
|
||
let codeTimer: number | null = null
|
||
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
|
||
|
||
console.log('检测到已有会话, 执行重定向...')
|
||
if (redirect != null && redirect.length > 0) {
|
||
uni.redirectTo({ url: decodeURIComponent(redirect) })
|
||
} else {
|
||
uni.reLaunch({ url: '/pages/mall/admin/homePage/index' })
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('检查登录状态失败:', e)
|
||
}
|
||
})
|
||
|
||
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 = setInterval(() => {
|
||
codeCountdown.value--
|
||
if (codeCountdown.value > 0) {
|
||
codeText.value = `${codeCountdown.value}秒后重试`
|
||
} else {
|
||
codeDisabled.value = false
|
||
codeText.value = '获取验证码'
|
||
if (codeTimer != null) {
|
||
clearInterval(codeTimer)
|
||
codeTimer = null
|
||
}
|
||
}
|
||
}, 1000) as unknown as number
|
||
}
|
||
|
||
const handleLogin = async () => {
|
||
if (!validateAccount()) return
|
||
|
||
// 特殊账号处理:仅在测试模式下保留直登逻辑,生产环境强制校验角色
|
||
if (IS_TEST_MODE && 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.setStorageSync('merchant_id', 'admin') // mock
|
||
|
||
uni.showToast({ title: '管理员登录成功', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.reLaunch({ url: '/pages/mall/admin/homePage/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) {
|
||
// 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') ?? ''
|
||
|
||
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' ||
|
||
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 merchantId = await checkMerchantAccess(sessionUid, account.value)
|
||
if (merchantId == null) {
|
||
await supa.signOut()
|
||
logout()
|
||
throw new Error('您还没有注册商家端账户,快去注册一个')
|
||
}
|
||
|
||
// 存入商家ID
|
||
uni.setStorageSync('merchant_id', merchantId)
|
||
} else {
|
||
uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' })
|
||
return
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '手机验证码登录功能开发中', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 更新 store 中的用户资料
|
||
try {
|
||
const profile = await getCurrentUser()
|
||
console.log('fetch profile success:', profile)
|
||
} catch (e) {
|
||
console.error('获取用户信息失败(忽略):', e)
|
||
}
|
||
|
||
// 显式保存用户ID到本地存储
|
||
const currentSession = supa.getSession()
|
||
if (currentSession.user != null) {
|
||
const uid = currentSession.user?.getString('id')
|
||
if (uid != null) {
|
||
uni.setStorageSync('user_id', uid)
|
||
}
|
||
}
|
||
|
||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||
|
||
// 成功跳转逻辑
|
||
setTimeout(() => {
|
||
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.reLaunch({ url: '/pages/mall/admin/homePage/index' })
|
||
}
|
||
}, 500)
|
||
} catch (err) {
|
||
console.error('登录错误:', err)
|
||
let msg = '登录失败,请重试'
|
||
if (err != null && typeof err === 'object') {
|
||
const e = err as Error
|
||
if (e.message != null && e.message.trim() !== '') msg = e.message
|
||
}
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
const navigateToRegister = () => {
|
||
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/register?redirect=${redirect}`
|
||
})
|
||
} else {
|
||
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;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg);
|
||
}
|
||
|
||
/* Header */
|
||
.header{
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding: 30px 72px;
|
||
}
|
||
.logo{
|
||
width: 300px;
|
||
height: 80px;
|
||
}
|
||
|
||
/* Main */
|
||
.main{
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 28px 18px;
|
||
}
|
||
|
||
/* Card */
|
||
.card{
|
||
width: min(980px, 92vw);
|
||
min-height: 460px;
|
||
background: var(--card);
|
||
border-radius: 16px;
|
||
box-shadow: var(--shadow);
|
||
padding: 40px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 32px;
|
||
}
|
||
|
||
/* Left */
|
||
.left{
|
||
flex: 0 0 52%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding-left: 18px;
|
||
}
|
||
.left-title{
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
margin-bottom: 10px;
|
||
}
|
||
.left-hint{
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 14px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.hint-text{ font-size: 13px; color: var(--muted); }
|
||
.hint-link{ font-size: 13px; color: var(--brand); }
|
||
|
||
.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: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
.qr-text{ font-size: 14px; color: var(--muted); }
|
||
.qr-sub{ font-size: 12px; color: var(--muted2); }
|
||
|
||
/* Divider */
|
||
.divider{
|
||
width: 1px;
|
||
background: var(--border);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Right */
|
||
.right{
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
.right-inner{
|
||
width: 360px; /* 京东右侧“窄列”观感 */
|
||
max-width: 100%;
|
||
margin-left: auto; /* 靠右 */
|
||
}
|
||
|
||
/* Tabs */
|
||
.tabs{
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 24px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.tab{
|
||
position: relative;
|
||
padding: 8px 2px;
|
||
}
|
||
.tab-text{
|
||
font-size: 16px;
|
||
color: var(--muted);
|
||
}
|
||
.tab.active .tab-text{
|
||
color: var(--brand);
|
||
font-weight: 600;
|
||
}
|
||
.tab-line{
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: -6px;
|
||
height: 2px;
|
||
background: var(--brand);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* 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 */
|
||
.code-row{
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.code-input{ flex: 1; }
|
||
.code-btn{
|
||
height: 44px;
|
||
padding: 0 12px;
|
||
border-radius: 10px;
|
||
background: #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: var(--brand); }
|
||
|
||
/* Button */
|
||
.btn{
|
||
margin-top: 16px;
|
||
height: 46px;
|
||
border-radius: 10px;
|
||
background: rgba(225, 37, 27, 0.45);
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.btn.disabled{ background: #d9d9d9; }
|
||
.btn-text{
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Actions:一行横排 */
|
||
.actions{
|
||
margin-top: 16px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 12px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
.action-item{
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.dot{
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
}
|
||
.dot.wechat{ background: #19be6b; }
|
||
.dot.qq{ background: #2d8cf0; }
|
||
|
||
.action-text{ font-size: 13px; color: var(--muted); }
|
||
.action-link{ font-size: 13px; color: var(--muted); }
|
||
.sep{ font-size: 13px; color: #e0e0e0; }
|
||
|
||
/* Footer */
|
||
.footer{
|
||
padding: 18px 0 28px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
}
|
||
.footer-text{ font-size: 12px; color: var(--muted2); }
|
||
|
||
/* ===== 自适应:断点全部用 px(避免 rpx 在宽屏放大) ===== */
|
||
@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>
|