登录、注册页样式修改

This commit is contained in:
2026-03-12 09:11:48 +08:00
parent affb2342eb
commit e6d95b52b9
5 changed files with 302 additions and 192 deletions

View File

@@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require("fs");
const path = 'D:/骅锋/mall/layouts/admin/components/AdminHeader.uvue'; const path = "D:/骅锋/mall/layouts/admin/components/AdminHeader.uvue";
let text = fs.readFileSync(path, 'utf-8'); let text = fs.readFileSync(path, "utf-8");
const targetScript = `<script setup lang="uts"> const targetScript = `<script setup lang="uts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -63,7 +63,10 @@ function handleLogout() {
const props = defineProps<{`; const props = defineProps<{`;
text = text.replace(/<script setup lang=["']uts["']>[\s\S]*?const props = defineProps</, targetScript + ""); text = text.replace(
/<script setup lang=["']uts["']>[\s\S]*?const props = defineProps</,
targetScript + "",
);
fs.writeFileSync(path, text, 'utf-8'); fs.writeFileSync(path, text, "utf-8");
console.log('Done'); console.log("Done");

View File

@@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require("fs");
const path = 'D:/骅锋/mall/pages/mall/admin/userCenter/index.uvue'; const path = "D:/骅锋/mall/pages/mall/admin/userCenter/index.uvue";
let text = fs.readFileSync(path, 'utf-8'); let text = fs.readFileSync(path, "utf-8");
const targetScript = `<script setup lang="uts"> const targetScript = `<script setup lang="uts">
import { reactive, computed, onMounted } from 'vue' import { reactive, computed, onMounted } from 'vue'
@@ -135,9 +135,12 @@ const onSubmit = async () => {
} }
</script>`; </script>`;
text = text.replace(/<script setup lang=["']uts["']>[\s\S]*?<\/script>/, targetScript); text = text.replace(
/<script setup lang=["']uts["']>[\s\S]*?<\/script>/,
targetScript,
);
text = text.replace(/value="demo"/, ':value="userAccount"'); text = text.replace(/value="demo"/, ':value="userAccount"');
text = text.replace(/src="\/static\/logo\.png"/, ':src="avatarUrl"'); text = text.replace(/src="\/static\/logo\.png"/, ':src="avatarUrl"');
fs.writeFileSync(path, text, 'utf-8'); fs.writeFileSync(path, text, "utf-8");
console.log('Done'); console.log("Done");

View File

@@ -188,7 +188,7 @@ onMounted(async () => {
await ensureSupabaseReady() await ensureSupabaseReady()
const mId = uni.getStorageSync('merchant_id') as string | null const mId = uni.getStorageSync('merchant_id') as string | null
if (!mId) { if (!mId) {
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' }) uni.showToast({ title: '商家未获取到信息,请重新登录', icon: 'none' })
return return
} }
formData.value.merchant_id = mId formData.value.merchant_id = mId

View File

@@ -60,14 +60,18 @@
@input="(e: any) => account = e.detail.value" @input="(e: any) => account = e.detail.value"
/> />
</view> </view>
<view class="field"> <view class="field password-field">
<input <input
class="input" class="input"
type="password" :type="isPasswordVisible ? 'text' : 'password'"
placeholder="密码" placeholder="密码"
:value="password" :value="password"
@input="(e: any) => password = e.detail.value" @input="(e: any) => password = e.detail.value"
/> />
<view class="eye-btn" @click="isPasswordVisible = !isPasswordVisible">
<!-- 睁眼表示可见(type='text'), 闭眼表示不可见(type='password') -->
<text class="eye-icon">{{ isPasswordVisible ? '👁️' : '🙈' }}</text>
</view>
</view> </view>
</template> </template>
@@ -163,6 +167,7 @@ const loginType = ref<number>(0)
const account = ref<string>('') const account = ref<string>('')
const password = ref<string>('') const password = ref<string>('')
const captcha = ref<string>('') const captcha = ref<string>('')
const isPasswordVisible = ref<boolean>(false)
const isLoading = ref<boolean>(false) const isLoading = ref<boolean>(false)
@@ -170,32 +175,38 @@ const isLoading = ref<boolean>(false)
* 【核心函数】:登录成功后,多条件校验是否为商家角色 * 【核心函数】:登录成功后,多条件校验是否为商家角色
* 优先级: session_uid (auth_id) -> id -> normalized email * 优先级: session_uid (auth_id) -> id -> normalized email
*/ */
const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise<boolean> => { const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise<string | null> => {
const email = rawEmail.trim().toLowerCase() const email = rawEmail.trim().toLowerCase()
console.log(`🔍 开始校验商家端角色 -> UID: ${uid}, Email: ${email}`) console.log(`🔍 开始校验商家端角色 -> UID: ${uid}, Email: ${email}`)
try { try {
// 1. 尝试按 auth_id 查询 // 1. 尝试按 auth_id 查询
let res = await supa.from('ak_users').select('role').eq('auth_id', uid).execute() let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute()
let dataArray = res.data let dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) { if (Array.isArray(dataArray) && dataArray.length > 0) {
const role = (dataArray[0] as UTSJSONObject).getString('role') const obj = dataArray[0] as UTSJSONObject
const role = obj.getString('role')
const id = obj.getString('id')
console.log('✅ 按 auth_id 匹配成功role:', role) console.log('✅ 按 auth_id 匹配成功role:', role)
return role === 'merchant' if (role === 'merchant' && id != null) return id
return null
} }
// 2. 尝试按 id 查询 (兼容老数据) // 2. 尝试按 id 查询 (兼容老数据)
res = await supa.from('ak_users').select('role').eq('id', uid).execute() res = await supa.from('ak_users').select('id, role').eq('id', uid).execute()
dataArray = res.data dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) { if (Array.isArray(dataArray) && dataArray.length > 0) {
const role = (dataArray[0] as UTSJSONObject).getString('role') const obj = dataArray[0] as UTSJSONObject
const role = obj.getString('role')
const id = obj.getString('id')
console.log('✅ 按 id 匹配成功role:', role) console.log('✅ 按 id 匹配成功role:', role)
return role === 'merchant' if (role === 'merchant' && id != null) return id
return null
} }
// 3. 尝试按 email 兜底查询 // 3. 尝试按 email 兜底查询
if (email !== '') { if (email !== '') {
res = await supa.from('ak_users').select('role').eq('email', email).execute() res = await supa.from('ak_users').select('id, role').eq('email', email).execute()
dataArray = res.data dataArray = res.data
if (Array.isArray(dataArray) && dataArray.length > 0) { if (Array.isArray(dataArray) && dataArray.length > 0) {
@@ -203,9 +214,12 @@ const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise<bool
if (dataArray.length > 1) { if (dataArray.length > 1) {
console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录取第一条校验。Email:', email) console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录取第一条校验。Email:', email)
} }
const role = (dataArray[0] as UTSJSONObject).getString('role') const obj = dataArray[0] as UTSJSONObject
const role = obj.getString('role')
const id = obj.getString('id')
console.log('✅ 按 email 匹配成功role:', role) console.log('✅ 按 email 匹配成功role:', role)
return role === 'merchant' if (role === 'merchant' && id != null) return id
return null
} }
} }
@@ -336,6 +350,7 @@ const handleLogin = async () => {
class_id: '' class_id: ''
} as UserProfile } as UserProfile
setUserProfile(adminProfile) setUserProfile(adminProfile)
uni.setStorageSync('merchant_id', 'admin') // mock
uni.showToast({ title: '管理员登录成功', icon: 'success' }) uni.showToast({ title: '管理员登录成功', icon: 'success' })
setTimeout(() => { setTimeout(() => {
@@ -385,12 +400,15 @@ const handleLogin = async () => {
const sessionUser = result.user const sessionUser = result.user
let sessionUid = sessionUser?.getString('id') ?? '' let sessionUid = sessionUser?.getString('id') ?? ''
const isMerchant = await checkMerchantAccess(sessionUid, account.value) const merchantId = await checkMerchantAccess(sessionUid, account.value)
if (!isMerchant) { if (merchantId == null) {
await supa.signOut() await supa.signOut()
logout() logout()
throw new Error('您还没有注册商家端账户,快去注册一个') throw new Error('您还没有注册商家端账户,快去注册一个')
} }
// 存入商家ID
uni.setStorageSync('merchant_id', merchantId)
} else { } else {
uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' }) uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' })
return return
@@ -619,6 +637,24 @@ const handleQQLogin = () => uni.showToast({ title: 'QQ登录开发中', icon: 'n
/* Form */ /* Form */
.form{ margin-top: 10px; } .form{ margin-top: 10px; }
.field{ margin-bottom: 14px; } .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{ .input{
width: 100%; width: 100%;
height: 44px; height: 44px;

View File

@@ -1,86 +1,97 @@
<template> <template>
<view class="register-wrapper"> <view class="page">
<!-- Header Logo --> <!-- Header Logo -->
<view class="header"> <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>
<!-- 注册表单区域 --> <!-- 注册表单区域 -->
<view class="register-box"> <view class="main">
<view class="title">注册账号</view> <view class="register-box">
<text class="title">注册账号</text>
<!-- 注册表单 --> <!-- 注册表单 -->
<view class="form-content"> <view class="form-content">
<!-- 邮箱 --> <!-- 邮箱 -->
<view class="input-group"> <view class="input-group">
<view class="input-wrapper"> <view class="input-wrapper">
<image src="/static/user/phone_1.png" class="input-icon" /> <image src="/static/user/phone_1.png" class="input-icon" />
<input <input
type="text" type="text"
placeholder="输入邮箱" placeholder="输入邮箱"
:value="email" :value="email"
@input="(e: any) => email = e.detail.value" @input="(e: any) => email = e.detail.value"
class="input-field" 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> </view>
<!-- 密码 --> <!-- 注册按钮 -->
<view class="input-group"> <view class="register-btn" @click="handleRegister" :class="{ 'disabled': isLoading }">
<view class="input-wrapper"> <text class="btn-text">注册</text>
<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> </view>
<!-- 确认密码 --> <!-- 协议勾选 -->
<view class="input-group"> <view class="protocol">
<view class="input-wrapper"> <checkbox-group class="protocol-group" @change="handleProtocolChange">
<image src="/static/user/code_1.png" class="input-icon" /> <checkbox
<input :checked="protocol"
type="password" class="protocol-checkbox"
placeholder="确认密码" :class="{ 'trembling': inAnimation }"
:value="confirmPassword" @animationend="inAnimation = false"
@input="(e: any) => confirmPassword = e.detail.value"
class="input-field"
/> />
</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> </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> </view>
<!-- 底部版权信息 --> <!-- 底部版权信息 -->
@@ -101,6 +112,8 @@
const protocol = ref<boolean>(false) const protocol = ref<boolean>(false)
const inAnimation = ref<boolean>(false) const inAnimation = ref<boolean>(false)
const isLoading = 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 logoUrl = ref<string>('/static/logo.png')
// 处理协议勾选变化 // 处理协议勾选变化
@@ -314,176 +327,208 @@
</script> </script>
<style> <style>
page { /* Base Layout */
background: #F5F5F5; .page {
}
.register-wrapper {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #F5F5F5; background-color: #FFFFFF;
} }
/* Header Logo */ /* Header Area */
.header { .header {
padding: 40rpx 0 0 60rpx; width: 100%;
background: #F5F5F5; 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 { .logo {
width: 200rpx; width: 180px;
height: 80rpx; height: 64px;
} }
/* 注册表单区域 */ .header-right {
.register-box { 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; flex: 1;
background: #FFFFFF; display: flex;
margin: 60rpx 40rpx 0; flex-direction: row;
border-radius: 8rpx; justify-content: center;
padding: 60rpx 50rpx 40rpx; align-items: flex-start;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); padding-top: 80px;
background-color: #FFFFFF;
}
.register-box {
width: 420px;
max-width: 92vw;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
} }
.title { .title {
font-size: 40rpx; font-size: 26px;
font-weight: 600; font-weight: 600;
color: #333333; color: #333333;
text-align: center; text-align: center;
margin-bottom: 50rpx; margin-bottom: 40px;
} }
/* 表单内容 */ /* Form Content */
.form-content { .form-content {
margin-bottom: 40rpx; display: flex;
flex-direction: column;
width: 100%;
} }
.input-group { .input-group {
margin-bottom: 30rpx; margin-bottom: 24px;
width: 100%;
} }
.input-wrapper { .input-wrapper {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
padding: 0 20rpx; height: 48px;
height: 88rpx; border: 1px solid #D9D9D9;
border: 1rpx solid #E0E0E0; border-radius: 4px;
border-radius: 4rpx; background-color: #FFFFFF;
background: #FFFFFF; padding: 0 16px;
transition-property: border-color, box-shadow;
transition-duration: 0.3s;
} }
.input-wrapper:focus-within { .input-wrapper:focus-within {
border-color: var(--view-theme, #FF4D4F); border-color: var(--view-theme, #FF4D4F);
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.1);
} }
.input-icon { .input-icon {
width: 32rpx; width: 22px;
height: 32rpx; height: 22px;
flex-shrink: 0; margin-right: 12px;
margin-right: 20rpx; opacity: 0.4;
} }
.input-field { .input-field {
flex: 1; flex: 1;
font-size: 28rpx; height: 100%;
height: 88rpx; font-size: 15px;
line-height: 88rpx;
color: #333333; color: #333333;
display: flex;
align-items: center;
}
.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; background: transparent;
border: none; border: none;
padding: 0; outline: none;
line-height: 1;
} }
.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; color: #999999;
} }
/* 注册按钮 */ /* Register Button */
.register-btn { .register-btn {
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 88rpx; height: 48px;
margin-top: 50rpx; margin-top: 10px;
background: linear-gradient(135deg, #FF4D4F 0%, #FF7A45 100%); background: linear-gradient(90deg, var(--view-theme, #FF4D4F) 0%, #FF7A45 100%);
border-radius: 4rpx; border-radius: 4px;
color: #FFFFFF; cursor: pointer;
font-size: 32rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3);
} }
.register-btn.disabled { .register-btn.disabled {
background: #D9D9D9;
box-shadow: none;
opacity: 0.6; opacity: 0.6;
cursor: not-allowed;
background: #D9D9D9;
} }
/* 已有账号提示 */ .btn-text {
.tips { color: #FFFFFF;
margin-top: 30rpx; font-size: 16px;
display: flex; font-weight: 500;
flex-direction: row; letter-spacing: 2px;
align-items: center;
justify-content: center;
} }
.tips-text { /* Protocol */
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; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%;
} }
.protocol checkbox-group { .protocol-group {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.protocol checkbox { .protocol-checkbox {
margin-right: 10rpx; transform: scale(0.8);
margin-right: 4px;
} }
.protocol-text { .protocol-text {
font-size: 24rpx; font-size: 13px;
color: #999999; color: #666666;
} }
.main-color { .main-color {
font-size: 13px;
color: var(--view-theme, #FF4D4F); color: var(--view-theme, #FF4D4F);
cursor: pointer;
} }
.trembling { .trembling {
@@ -491,20 +536,43 @@ page {
} }
@keyframes shake { @keyframes shake {
0%, 100% { transform: translateX(0); } 0%, 100% { transform: translateX(0) scale(0.8); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px) scale(0.8); }
20%, 40%, 60%, 80% { transform: translateX(10rpx); } 20%, 40%, 60%, 80% { transform: translateX(4px) scale(0.8); }
} }
/* 底部版权 */ /* Footer */
.footer { .footer {
padding: 40rpx 0; padding: 40px 0;
text-align: center; display: flex;
background: #F5F5F5; flex-direction: row;
justify-content: center;
} }
.footer-text { .footer-text {
font-size: 22rpx; font-size: 13px;
color: #999999; 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> </style>