feat:修改app注册,登录页面和流程

This commit is contained in:
zheng020 2026-07-02 19:04:29 +08:00
parent 4eab42b3fb
commit 8d72b796fa
25 changed files with 3121 additions and 2336 deletions

View File

@ -8,8 +8,24 @@ import { getDeviceFingerprint } from "@/utils/deviceFingerprint.js";
const HIDE_TIME_KEY = "app_last_hide_time"; const HIDE_TIME_KEY = "app_last_hide_time";
export default { export default {
// uni.storage needs_welcome
// TopfansWelcome onLaunch false =
// square handleEnterTopfans true
globalData: {
welcomeShownThisSession: false,
},
onLaunch: function () { onLaunch: function () {
console.log("App Launch"); console.log("App Launch");
// TopfansWelcome
//
// 1) "" onLaunch
// 2) "" / onLaunch
//
// - home onShow onLaunch
// - " TOPFANS " true
// storage needs_welcome /
// app /
this.globalData.welcomeShownThisSession = false;
// bootstrap spec §9.1 v2.3 X-Device-Fingerprint // bootstrap spec §9.1 v2.3 X-Device-Fingerprint
// getDeviceFingerprint setStorageSync // getDeviceFingerprint setStorageSync
getDeviceFingerprint(); getDeviceFingerprint();

View File

@ -0,0 +1,149 @@
<template>
<view class="login-bg">
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<view v-if="showBack" class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<!-- 背景图片层 -->
<image
class="background-image"
src="/static/login/portal/bg.png"
mode="aspectFill"
></image>
<!-- 女孩图片点缀 -->
<image
class="character-upper"
src="/static/login/portal/character1.png"
mode="aspectFit"
></image>
<!-- 粉色玩偶点缀 -->
<image
class="character-lower"
src="/static/login/portal/character2.png"
mode="aspectFit"
></image>
<!-- TOPFANS 标题 -->
<view v-if="showLogo" class="title-wrapper">
<image
class="topfans-logo"
src="/static/login/portal/topfans-logo.png"
mode="aspectFit"
></image>
</view>
<!-- 内容插槽 -->
<slot></slot>
</view>
</template>
<script setup>
defineProps({
showLogo: {
type: Boolean,
default: true,
},
showBack: {
type: Boolean,
default: true,
},
});
const goBack = () => {
uni.navigateBack();
};
</script>
<style scoped>
.login-bg {
position: relative;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
}
.status-bar {
width: 100%;
height: 44rpx;
position: relative;
z-index: 10;
flex-shrink: 0;
}
.nav-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 96rpx;
left: 32rpx;
/* background: rgba(255,255,255,0.5);
border-radius: 50%; */
}
.nav-back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
/* 背景图:覆盖整个屏幕 */
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
opacity: 0.68;
}
/* 上方人物图(女孩) */
.character-upper {
position: absolute;
left: 50%;
transform: translateX(-50%) rotate(0.5deg);
width: 100%;
height: 100%;
z-index: 1;
object-fit: contain;
}
/* 下方粉色玩偶 */
.character-lower {
position: absolute;
top: 448rpx;
left: 50%;
transform: translateX(-50%) rotate(0.5deg);
width: 135%;
height: 100%;
z-index: 2;
object-fit: contain;
}
/* TOPFANS 标题 */
.title-wrapper {
width: 758rpx;
height: 192rpx;
position: relative;
z-index: 10;
padding-top: 30rpx;
display: flex;
justify-content: center;
flex-shrink: 0;
}
.topfans-logo {
width: 100%;
height: 100%;
/* filter: drop-shadow(0 4rpx 9rpx rgba(177, 19, 19, 0.49)); */
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<LoginBackground :show-logo="false">
<view class="welcome-container">
<!-- 主卡片女孩 + 粉色玩偶 -->
<view class="welcome-card">
<image
class="welcome-bg"
src="/static/login/portal/welcome-bg.png"
mode="aspectFill"
></image>
</view>
<!-- 进入 TOPFANS 世界按钮 -->
<view class="enter-btn-wrapper">
<view class="enter-btn" @tap="handleEnter">
<text class="enter-btn-text">进入 TOPFANS 世界</text>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import LoginBackground from "@/components/LoginBackground.vue";
const emit = defineEmits(["enter"]);
const handleEnter = () => {
emit("enter");
};
</script>
<style scoped>
.welcome-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
overflow: hidden;
}
/* 背景 */
.bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.68;
}
.bg-character {
position: absolute;
top: 60rpx;
left: 50%;
transform: translateX(-50%) rotate(0.5deg);
width: 128%;
height: 60%;
z-index: 1;
filter: blur(2rpx);
opacity: 0.9;
object-fit: contain;
object-position: center top;
}
.bg-character-shadow {
position: absolute;
top: 470rpx;
left: 50%;
transform: translateX(-50%) rotate(0.5deg);
width: 145%;
height: 50%;
z-index: 2;
filter: blur(4rpx);
opacity: 0.9;
object-fit: contain;
object-position: center bottom;
}
.topfans-logo-img {
position: absolute;
top: 100rpx;
left: 50%;
transform: translateX(-50%);
width: 698rpx;
height: 150rpx;
z-index: 10;
filter: drop-shadow(0 4rpx 18rpx rgba(177, 19, 19, 0.49));
}
/* 欢迎卡片 */
.welcome-card {
position: absolute;
top: 128rpx;
left: 20rpx;
width: 710rpx;
height: 1376rpx;
border-radius: 56rpx;
border: 2rpx solid rgba(255, 255, 255, 0.15);
box-shadow: 0 0 41rpx 0 rgba(0, 0, 0, 0.31);
overflow: hidden;
z-index: 5;
}
.welcome-bg {
width: 100%;
height: 100%;
}
/* 进入按钮 */
.enter-btn-wrapper {
position: absolute;
left: 50%;
bottom: 160rpx;
transform: translateX(-50%);
z-index: 10;
}
.enter-btn {
width: 450rpx;
height: 90rpx;
border-radius: 58rpx;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.38) 0%,
rgba(252, 100, 102, 0.78) 64%,
rgba(244, 88, 104, 0.78) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
}
.enter-btn-text {
font-size: 46rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
</style>

View File

@ -77,6 +77,24 @@
} }
} }
}, },
{
"path": "pages/login/portal",
"style": {
"navigationStyle": "custom",
"app-plus": {
"bounce": "none"
}
}
},
{
"path": "pages/login/quickLogin",
"style": {
"navigationStyle": "custom",
"app-plus": {
"bounce": "none"
}
}
},
{ {
"path": "pages/register/register", "path": "pages/register/register",
"style": { "style": {

View File

@ -1,709 +1,365 @@
<template> <template>
<view class="login-container"> <LoginBackground :show-logo="true">
<!-- 背景图片层 --> <!-- 内容区域 -->
<image class="background-image" src="/static/background/login-bg.png" mode="aspectFill"></image> <view class="login-content">
<!-- 女孩图片 --> <!-- 头像 -->
<image class="girl-image" src="/static/login/person-photo.png" mode="aspectFit"></image> <view class="avatar-wrapper">
<!-- 粉色玩偶遮挡女孩脸的下半部分 --> <image
<image class="doll-image" src="/static/login/login-character.png" mode="aspectFit"></image> class="avatar-image"
<view class="background-overlay"></view> src="/static/login/portal/register-avatar.png"
mode="aspectFit"
<!-- 内容区域 --> ></image>
<view class="content-wrapper"> </view>
<!-- TOPFANS 标题 -->
<view class="title-wrapper"> <!-- 登录卡片 -->
<text class="title-text">TOPFANS</text> <view class="login-card">
</view> <!-- 标题 -->
<!-- 表单区域居中部分 --> <view class="card-title-wrapper">
<view class="form-container"> <text class="card-title">账号密码登录</text>
<view class="form-wrapper"> </view>
<!-- 手机号输入框 -->
<view class="input-wrapper"> <!-- 表单 -->
<input <view class="form-area">
class="input-field" <!-- 手机号 -->
type="number" <view class="input-group">
v-model="form.phone" <view class="input-outer">
placeholder="请输入手机号" <view class="input-inner">
maxlength="11" <input
placeholder-class="input-placeholder" class="input-field"
/> type="number"
</view> v-model="form.phone"
placeholder="输入您的手机号"
<!-- 密码输入框 --> placeholder-class="input-placeholder"
<view class="input-wrapper password-wrapper"> maxlength="11"
<input />
class="input-field" </view>
:type="showPassword ? 'text' : 'password'" </view>
v-model="form.password" </view>
placeholder="请输入密码"
placeholder-class="input-placeholder" <!-- 密码 -->
/> <view class="input-group">
<view class="eye-icon" @click="togglePassword"> <view class="input-outer">
<text class="eye-text">{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text> <view class="input-inner">
</view> <input
</view> class="input-field"
:type="showPassword ? 'text' : 'password'"
<!-- 错误提示 --> v-model="form.password"
<view v-if="errorMessage" class="error-message"> placeholder="输入您的密码"
<text>{{ errorMessage }}</text> placeholder-class="input-placeholder"
</view> />
</view>
<!-- 登录按钮 --> </view>
<button class="btn-primary" @click="handleLogin">登录</button> </view>
<!-- 注册按钮 --> <!-- 错误提示 -->
<view class="register-link" @click="goToRegister"> <view v-if="errorMessage" class="error-message">
<text>还没有账号去注册</text> <text>{{ errorMessage }}</text>
</view> </view>
</view> </view>
</view>
<!-- 忘记密码 -->
<!-- 底部服务条款 --> <view class="forgot-password-wrapper">
<view class="footer-text"> <text class="forgot-password" @tap="handleForgotPassword"
<view class="agreement-wrapper"> >忘记密码</text
<view class="agreement-checkbox" @click="toggleAgreement"> >
<view class="custom-checkbox" :class="{ 'checked': agreedToTerms }"> </view>
<view v-if="agreedToTerms" class="checkbox-inner"></view>
</view> <!-- 分割线 -->
<text class="agreement-label">我已阅读并同意</text> <view class="divider"></view>
</view>
<text class="agreement-link" @click="showAgreementModal">Topfans用户服务协议</text> <!-- 登录按钮 -->
</view> <view class="login-btn-wrapper">
</view> <view class="login-btn" @tap="handleLogin">
</view> <text class="login-btn-text">登录</text>
</view>
<!-- 提示弹窗 --> </view>
<view v-if="showTipDialog" class="tip-dialog-mask" @click="closeTipDialog"> </view>
<view class="tip-dialog" @click.stop> </view>
<view class="tip-dialog-header"> </LoginBackground>
<text class="tip-dialog-title">提示</text>
<text class="tip-dialog-close" @click="closeTipDialog">×</text>
</view>
<view class="tip-dialog-content">
<text class="tip-text">阅读并同意以下条款</text>
<text class="tip-agreement-link" @click="openAgreementFromTip">Topfans用户服务协议</text>
</view>
</view>
</view>
<!-- 协议全文弹窗 -->
<view v-if="showAgreementDialog" class="agreement-dialog-mask" @click="closeAgreementDialog">
<view class="agreement-dialog" @click.stop>
<view class="agreement-dialog-header">
<text class="agreement-dialog-title">Topfans用户服务协议</text>
<text class="agreement-dialog-close" @click="closeAgreementDialog">×</text>
</view>
<scroll-view class="agreement-dialog-content" scroll-y>
<text class="agreement-text">{{ agreementContent || '协议内容加载中...' }}</text>
</scroll-view>
<view class="agreement-dialog-footer">
<button class="agreement-confirm-btn" @click="agreeAndClose">我同意</button>
</view>
</view>
</view>
</view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from "vue";
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from "@dcloudio/uni-app";
import { useStore } from 'vuex'; import { useStore } from "vuex";
import { validatePhone, validatePassword } from '@/utils/validator'; import LoginBackground from "@/components/LoginBackground.vue";
import { AGREEMENT_CONTENT } from '@/utils/agreement'; import { validatePhone, validatePassword } from "@/utils/validator";
import { resetAllGuides } from '@/utils/guideConfig'; import { resetAllGuides } from "@/utils/guideConfig";
const store = useStore(); const store = useStore();
// //
const form = ref({ const form = ref({
phone: '', phone: "",
password: '' password: "",
}); });
const showPassword = ref(false); const showPassword = ref(false);
const errorMessage = ref(''); const errorMessage = ref("");
const agreedToTerms = ref(false);
const agreementContent = ref('');
const showAgreementDialog = ref(false);
const showTipDialog = ref(false);
// //
const getPageParams = () => { const getPageParams = () => {
const pages = getCurrentPages() const pages = getCurrentPages();
if (pages.length > 0) { if (pages.length > 0) {
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1];
if (currentPage.options && currentPage.options.error) { if (currentPage.options && currentPage.options.error) {
errorMessage.value = decodeURIComponent(currentPage.options.error) errorMessage.value = decodeURIComponent(currentPage.options.error);
} }
} }
} };
// //
onLoad(() => { onLoad(() => {
getPageParams() getPageParams();
}) });
//
const getAgreementContent = () => {
return AGREEMENT_CONTENT;
};
// / // /
const togglePassword = () => { const togglePassword = () => {
showPassword.value = !showPassword.value; showPassword.value = !showPassword.value;
}; };
// //
const goToRegister = () => { const goToRegister = () => {
// resetAllGuides();
resetAllGuides(); uni.removeStorageSync("is_new_user");
uni.removeStorageSync('is_new_user'); uni.removeStorageSync("has_seen_welcome");
uni.removeStorageSync('has_seen_welcome'); uni.reLaunch({ url: "/pages/register/register" });
uni.navigateTo({
url: '/pages/register/register'
});
}; };
// //
const toggleAgreement = () => { const handleForgotPassword = () => {
agreedToTerms.value = !agreedToTerms.value; uni.showToast({ title: "忘记密码功能开发中", icon: "none" });
};
//
const showAgreementModal = () => {
//
if (!agreementContent.value) {
agreementContent.value = getAgreementContent();
}
//
showAgreementDialog.value = true;
};
//
const closeAgreementDialog = () => {
showAgreementDialog.value = false;
};
//
const agreeAndClose = () => {
agreedToTerms.value = true;
showAgreementDialog.value = false;
};
//
const openTipDialog = () => {
showTipDialog.value = true;
};
//
const closeTipDialog = () => {
showTipDialog.value = false;
};
//
const openAgreementFromTip = () => {
showTipDialog.value = false;
//
setTimeout(() => {
showAgreementModal();
}, 300);
}; };
// //
const handleLogin = async () => { const handleLogin = async () => {
// const phoneValidation = validatePhone(form.value.phone);
if (!agreedToTerms.value) { if (!phoneValidation.valid) {
openTipDialog(); errorMessage.value = phoneValidation.message;
return; return;
} }
// const passwordValidation = validatePassword(form.value.password);
const phoneValidation = validatePhone(form.value.phone); if (!passwordValidation.valid) {
if (!phoneValidation.valid) { errorMessage.value = passwordValidation.message;
errorMessage.value = phoneValidation.message; return;
return; }
}
errorMessage.value = "";
const passwordValidation = validatePassword(form.value.password);
if (!passwordValidation.valid) { try {
errorMessage.value = passwordValidation.message; uni.showLoading({ title: "登录中...", mask: true });
return;
} await store.dispatch("user/login", {
mobile: form.value.phone,
errorMessage.value = ''; password: form.value.password,
});
try {
// // needs_welcome=true TopfansWelcome
uni.showLoading({ uni.setStorageSync("needs_welcome", true);
title: '登录中...',
mask: true uni.hideLoading();
}); uni.reLaunch({ url: "/pages/square/square" });
} catch (error) {
await store.dispatch('user/login', { uni.hideLoading();
mobile: form.value.phone, errorMessage.value = error.message || "登录失败,请重试";
password: form.value.password uni.showToast({
}); title: errorMessage.value,
icon: "none",
uni.hideLoading(); duration: 2000,
});
// }
uni.reLaunch({
url: '/pages/square/square'
});
} catch (error) {
uni.hideLoading();
// message
errorMessage.value = error.message || '登录失败,请重试';
uni.showToast({
title: errorMessage.value,
icon: 'none',
duration: 2000
});
}
}; };
</script> </script>
<style scoped> <style scoped>
.login-container { .login-content {
position: relative; position: relative;
width: 100%; z-index: 10;
min-height: 100vh; width: 100%;
height: 100vh; display: flex;
overflow: hidden; flex-direction: column;
display: flex; align-items: center;
flex-direction: column; padding-bottom: 80rpx;
} }
.background-image { /* 头像 */
position: absolute; .avatar-wrapper {
top: 0; width: 296rpx;
left: 0; height: 274rpx;
width: 100%; margin-top: 130rpx;
height: 100%; filter: drop-shadow(2rpx 4rpx 98rpx rgba(0, 0, 0, 0.94));
z-index: 0; z-index: 11;
} }
.girl-image { .avatar-image {
position: absolute; width: 100%;
top: 55%; height: 100%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 180%;
height: 180%;
z-index: 1;
object-fit: contain;
object-position: center center;
} }
.doll-image { /* 卡片 */
position: absolute; .login-card {
bottom: -70%; width: 710rpx;
left: 45%; margin-top: -104rpx;
transform: translateX(-50%); padding: 30rpx 104rpx 30rpx;
width: 150%; box-sizing: border-box;
height: 150%; border-radius: 56rpx;
border: 1px solid rgba(255, 255, 255, 0.15);
z-index: 2; background: rgba(255, 255, 255, 0.1);
object-fit: contain; backdrop-filter: blur(64rpx);
object-position: center bottom; -webkit-backdrop-filter: blur(64rpx);
box-shadow: 0 0 20.5px 0 rgba(0, 0, 0, 0.31);
display: flex;
flex-direction: column;
align-items: center;
z-index: 12;
} }
.background-overlay { /* 标题 */
position: absolute; .card-title-wrapper {
top: 0; position: relative;
left: 0; width: 324rpx;
width: 100%; height: 84rpx;
height: 100%; display: flex;
background: rgba(0, 0, 0, 0.3); align-items: center;
z-index: 3; justify-content: center;
margin-bottom: 56rpx;
background:
linear-gradient(240deg, #cdaffa 9.77%, rgba(205, 175, 250, 0) 62.44%),
linear-gradient(108deg, #f7cbbf 13.91%, rgba(247, 203, 191, 0) 51.34%),
linear-gradient(297deg, #81a9f8 31.28%, rgba(129, 169, 248, 0) 90.74%),
linear-gradient(80deg, #69f4e9 39.86%, rgba(164, 252, 245, 0) 66.88%);
border-radius: 84rpx;
} }
.content-wrapper { .card-title {
position: relative; position: relative;
z-index: 10; z-index: 1;
width: 100%; font-size: 44rpx;
flex: 1; color: #fff9e7;
min-height: 100%; text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
display: flex; font-family: "Abhaya Libre Medium", sans-serif;
flex-direction: column;
align-items: center;
padding: 0 60rpx 60rpx 60rpx;
box-sizing: border-box;
} }
.title-wrapper { /* 表单 */
width: 100%; .form-area {
padding-top: 120rpx; width: 100%;
margin-bottom: 80rpx; display: flex;
text-align: center; flex-direction: column;
position: relative; gap: 32rpx;
flex-shrink: 0; padding: 0 14rpx;
} }
.form-container { .input-group {
flex: 1; width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
} }
.title-text { .input-outer {
font-size: 150rpx; width: 100%;
font-weight: 600; height: 74rpx;
letter-spacing: 12rpx; border-radius: 58rpx;
background: linear-gradient(to right, #B52920 0%, #86D9E0 80%, #56C1FF 100%); background: linear-gradient(
-webkit-background-clip: text; 90deg,
background-clip: text; rgba(255, 222, 8, 0.06) 0%,
-webkit-text-fill-color: transparent; rgba(252, 100, 102, 0.12) 64%,
display: inline-block; rgba(244, 88, 104, 0.12) 100%
line-height: 1.2; );
text-transform: uppercase; box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
font-family: 'TheMiladiatorRegular', sans-serif !important; display: flex;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.15)); align-items: center;
display: flex; padding: 4rpx;
justify-content: center; box-sizing: border-box;
} }
.form-wrapper { .input-inner {
width: 100%; width: 100%;
} height: 60rpx;
border-radius: 58rpx;
.input-wrapper { background: rgba(255, 255, 255, 0.65);
position: relative; display: flex;
width: 100%; align-items: center;
height: 100rpx; padding: 0 24rpx;
margin-bottom: 40rpx; box-sizing: border-box;
background: rgba(255, 255, 255, 0.9);
border-radius: 50rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
box-sizing: border-box;
}
.password-wrapper {
padding-right: 100rpx;
} }
.input-field { .input-field {
flex: 1; width: 100%;
height: 100%; height: 100%;
font-size: 32rpx; font-size: 30rpx;
color: #333333; color: #333;
} }
.input-placeholder { .input-placeholder {
color: #999999; font-size: 30rpx;
} color: #565656;
font-family: "Abhaya Libre Medium", sans-serif;
.eye-icon {
position: absolute;
right: 40rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.eye-text {
font-size: 40rpx;
} }
/* 错误提示 */
.error-message { .error-message {
width: 100%; width: 100%;
padding: 20rpx 0; padding-left: 24rpx;
margin-bottom: 20rpx;
text-align: center;
} }
.error-message text { .error-message text {
font-size: 28rpx; font-size: 24rpx;
color: #ff4444; color: #ff6b6b;
} }
.btn-primary { /* 忘记密码 */
width: 100%; .forgot-password-wrapper {
height: 100rpx; width: 100%;
margin-bottom: 30rpx; display: flex;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%); justify-content: center;
border-radius: 50rpx; /* padding: 0 18rpx; */
border: none; margin-top: 48rpx;
font-size: 36rpx; margin-bottom: 24rpx;
font-weight: bold;
color: #e6e6e6;
display: flex;
align-items: center;
justify-content: center;
} }
.btn-primary::after { .forgot-password {
border: none; font-size: 30rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
} }
.register-link { /* 分割线 */
width: 100%; .divider {
margin-top: 40rpx; width: 612rpx;
text-align: center; height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 28rpx 0 24rpx;
} }
.register-link text { /* 登录按钮 */
font-size: 28rpx; .login-btn-wrapper {
color: #e6e6e6; width: 100%;
text-decoration: underline; display: flex;
justify-content: center;
padding: 64rpx 0;
} }
.footer-text { .login-btn {
position: relative; width: 270rpx;
width: 100%; height: 90rpx;
margin-top: auto; border-radius: 58rpx;
padding: 0rpx 0 80rpx 0; background: linear-gradient(
text-align: center; 90deg,
rgba(255, 222, 8, 0.28) 0%,
rgba(252, 100, 102, 0.58) 64%,
rgba(244, 88, 104, 0.58) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
} }
.agreement-wrapper { .login-btn-text {
display: flex; font-size: 36rpx;
align-items: center; color: #fff9e7;
justify-content: center; text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
flex-wrap: wrap; font-family: "Abhaya Libre Medium", sans-serif;
padding: 0 40rpx;
}
.agreement-checkbox {
display: flex;
align-items: center;
margin-right: 10rpx;
}
/* 自定义圆形checkbox样式 */
.agreement-checkbox {
display: flex;
align-items: center;
margin-right: 10rpx;
cursor: pointer;
}
.custom-checkbox {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #e6e6e6;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
position: relative;
}
.custom-checkbox.checked {
border-color: #e6e6e6;
background-color: transparent;
}
.checkbox-inner {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #000000;
}
.agreement-label {
font-size: 24rpx;
color: #e6e6e6;
margin-left: 10rpx;
}
.agreement-link {
font-size: 24rpx;
color: #e6e6e6;
text-decoration: underline;
cursor: pointer;
}
/* 协议弹窗样式 */
.agreement-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.agreement-dialog {
width: 100%;
max-width: 700rpx;
max-height: 80vh;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.agreement-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.agreement-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.agreement-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.agreement-dialog-content {
flex: 1;
padding: 40rpx;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
}
.agreement-text {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
width: 100%;
box-sizing: border-box;
display: block;
}
.agreement-dialog-footer {
padding: 30rpx 40rpx;
border-top: 1rpx solid #e5e5e5;
}
.agreement-confirm-btn {
width: 100%;
height: 88rpx;
background: #000000;
color: #e6e6e6;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.agreement-confirm-btn::after {
border: none;
}
/* 提示弹窗样式 */
.tip-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.tip-dialog {
width: 100%;
max-width: 600rpx;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tip-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.tip-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.tip-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.tip-dialog-content {
padding: 40rpx;
text-align: center;
}
.tip-text {
font-size: 32rpx;
color: #333333;
line-height: 1.6;
display: block;
margin-bottom: 20rpx;
}
.tip-agreement-link {
font-size: 32rpx;
color: #007AFF;
text-decoration: underline;
cursor: pointer;
display: block;
} }
</style> </style>

View File

@ -0,0 +1,553 @@
<template>
<LoginBackground :show-logo="true" :showBack="false">
<!-- 内容卡片登录注册选择卡 -->
<view class="portal-content">
<view class="portal-card">
<!-- Tab 区域 -->
<view class="tab-row">
<view
class="tab-item"
:class="{ active: activeTab === 'register' }"
@tap="switchTab('register')"
>
<text class="tab-text">手机号注册</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'login' }"
@tap="switchTab('login')"
>
<text class="tab-text">账号密码登录</text>
</view>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 其他登录方式 -->
<view class="other-login">
<text class="other-login-text">其他登录方式</text>
<view class="third-party-icons">
<view class="third-party-item" @tap="handleWechatLogin">
<image
class="third-party-icon"
src="/static/assetDetail/share/ic_wechat_friend.png"
mode="aspectFit"
></image>
</view>
<view class="third-party-item" @tap="handleOtherLogin">
<image
class="third-party-icon"
src="/static/assetDetail/share/ic_qq.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
</view>
<!-- 底部协议 -->
<view class="agreement-wrapper">
<view
class="agreement-checkbox"
:class="{ checked: agreedToTerms }"
@tap="toggleAgreement"
>
<view v-if="agreedToTerms" class="checkbox-inner"></view>
</view>
<view class="agreement-text">
<text class="agreement-line"
>登陆代表您已阅读并接受统一认证服务条款以及</text
>
<text class="agreement-line agreement-link" @tap="showAgreementModal"
>隐私政策和用户服务协议</text
>
</view>
</view>
</view>
<!-- 提示弹窗 -->
<view v-if="showTipDialog" class="tip-dialog-mask" @tap="closeTipDialog">
<view class="tip-dialog" @tap.stop>
<view class="tip-dialog-header">
<text class="tip-dialog-title">提示</text>
<text class="tip-dialog-close" @tap="closeTipDialog">×</text>
</view>
<view class="tip-dialog-content">
<text class="tip-text">阅读并同意以下条款</text>
<text class="tip-agreement-link" @tap="openAgreementFromTip"
>Topfans用户服务协议</text
>
</view>
</view>
</view>
<!-- 协议全文弹窗 -->
<view
v-if="showAgreementDialog"
class="agreement-dialog-mask"
@tap="closeAgreementDialog"
>
<view class="agreement-dialog" @tap.stop>
<view class="agreement-dialog-header">
<text class="agreement-dialog-title">Topfans用户服务协议</text>
<text class="agreement-dialog-close" @tap="closeAgreementDialog"
>×</text
>
</view>
<scroll-view class="agreement-dialog-content" scroll-y>
<text class="agreement-dialog-text">{{
agreementContent || "协议内容加载中..."
}}</text>
</scroll-view>
<view class="agreement-dialog-footer">
<button class="agreement-confirm-btn" @tap="agreeAndClose">
我同意
</button>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import { ref } from "vue";
import LoginBackground from "@/components/LoginBackground.vue";
import { AGREEMENT_CONTENT } from "@/utils/agreement";
// tabregister=, login=
const activeTab = ref("register");
//
const agreedToTerms = ref(false);
//
const agreementContent = ref("");
const showAgreementDialog = ref(false);
const showTipDialog = ref(false);
//
const requireAgreement = () => {
if (!agreedToTerms.value) {
showTipDialog.value = true;
return false;
}
return true;
};
// tab
const switchTab = (tab) => {
if (!requireAgreement()) return;
activeTab.value = tab;
if (tab === "login") {
uni.reLaunch({ url: "/pages/login/login" });
} else if (tab === "register") {
uni.reLaunch({ url: "/pages/register/register" });
}
};
//
const handleWechatLogin = () => {
if (!requireAgreement()) return;
uni.showToast({ title: "微信登录开发中", icon: "none" });
};
//
const handleOtherLogin = () => {
if (!requireAgreement()) return;
uni.showToast({ title: "该登录方式开发中", icon: "none" });
};
//
const toggleAgreement = () => {
agreedToTerms.value = !agreedToTerms.value;
};
//
const showAgreementModal = () => {
if (!agreementContent.value) {
agreementContent.value = AGREEMENT_CONTENT;
}
showAgreementDialog.value = true;
};
//
const closeAgreementDialog = () => {
showAgreementDialog.value = false;
};
//
const agreeAndClose = () => {
agreedToTerms.value = true;
showAgreementDialog.value = false;
};
//
const closeTipDialog = () => {
showTipDialog.value = false;
};
//
const openAgreementFromTip = () => {
showTipDialog.value = false;
setTimeout(() => {
showAgreementModal();
}, 300);
};
</script>
<style scoped>
.portal-content {
position: relative;
z-index: 10;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* 卡片 */
.portal-card {
width: 606rpx;
height: 388rpx;
margin-top: 660rpx;
padding: 20rpx 50rpx;
box-sizing: border-box;
border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0);
box-shadow: 0 0 20.5px 0 rgba(0, 0, 0, 0.31);
display: flex;
flex-direction: column;
align-items: center;
}
/* Tab 按钮行 */
.tab-row {
width: 324rpx;
display: flex;
flex-direction: column;
gap: 18rpx;
margin-top: 14rpx;
}
.tab-item {
width: 324rpx;
height: 70rpx;
border-radius: 58rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.03) 0%,
rgba(252, 100, 102, 0.06) 64%,
rgba(244, 88, 104, 0.06) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
transition: all 0.2s ease;
}
.tab-item.active {
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.41) 0%,
rgba(252, 100, 102, 0.83) 64%,
rgba(244, 88, 104, 0.83) 100%
);
opacity: 0.96;
}
.tab-text {
font-size: 30rpx;
color: #ffffff;
text-shadow: 4rpx 4rpx 15rpx rgba(42, 36, 36, 0.85);
font-weight: normal;
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 分割线 */
.divider {
width: 534rpx;
height: 2rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 22rpx 0 22rpx;
}
/* 其他登录方式 */
.other-login {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.other-login-text {
font-size: 24rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
margin-bottom: 10rpx;
}
.third-party-icons {
display: flex;
align-items: center;
justify-content: center;
gap: 56rpx;
}
.third-party-item {
width: 48rpx;
height: 46rpx;
display: flex;
align-items: center;
justify-content: center;
filter: drop-shadow(0 4rpx 17rpx rgba(198, 27, 27, 0.49));
cursor: pointer;
}
.third-party-icon {
width: 48rpx;
height: 46rpx;
}
/* 底部协议 */
.agreement-wrapper {
position: fixed;
bottom: 40rpx;
z-index: 10;
display: flex;
gap: 0.5rem;
align-items: center;
}
.agreement-checkbox {
width: 44rpx;
height: 44rpx;
border-radius: 58rpx;
background: rgba(59, 58, 56, 0.19);
box-shadow: 4rpx 4rpx 18rpx 0 rgba(242, 21, 21, 0.27);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.96;
}
.checkbox-inner {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: #ffffff;
}
.agreement-text {
flex: 1;
margin-left: 6rpx;
display: flex;
flex-direction: column;
}
.agreement-line {
color: #fff;
text-shadow: 0 1px 6.7px #000;
font-family: "Abhaya Libre Medium";
font-size: 26rpx;
font-style: normal;
font-weight: 500;
line-height: normal;
}
/* 协议链接 */
.agreement-link {
text-decoration: underline;
cursor: pointer;
}
/* 协议弹窗样式 */
.agreement-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.agreement-dialog {
width: 100%;
max-width: 700rpx;
max-height: 80vh;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.agreement-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.agreement-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.agreement-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.agreement-dialog-content {
flex: 1;
padding: 40rpx;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
}
.agreement-dialog-text {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
width: 100%;
box-sizing: border-box;
display: block;
}
.agreement-dialog-footer {
padding: 30rpx 40rpx;
border-top: 1rpx solid #e5e5e5;
}
.agreement-confirm-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(
135deg,
rgba(255, 222, 8, 0.41) 0%,
rgba(252, 100, 102, 0.83) 64%,
rgba(244, 88, 104, 0.83) 100%
);
color: #e6e6e6;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.agreement-confirm-btn::after {
border: none;
}
/* 提示弹窗样式 */
.tip-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.tip-dialog {
width: 100%;
max-width: 600rpx;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tip-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.tip-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.tip-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.tip-dialog-content {
padding: 40rpx;
text-align: center;
}
.tip-text {
font-size: 32rpx;
color: #333333;
line-height: 1.6;
display: block;
margin-bottom: 20rpx;
}
.tip-agreement-link {
font-size: 32rpx;
color: #007aff;
text-decoration: underline;
cursor: pointer;
display: block;
}
</style>

View File

@ -0,0 +1,613 @@
<template>
<LoginBackground :show-logo="true" :showBack="false">
<view class="quicklogin-content">
<view class="quicklogin-card">
<!-- 标题 -->
<view class="card-title-wrapper">
<text class="card-title">欢迎回来</text>
</view>
<!-- 头像 -->
<view class="avatar-wrapper">
<view class="avatar-select-box">
<image
class="avatar-select-bg"
src="/static/login/portal/setnickname-avatar-box.png"
mode="aspectFit"
></image>
<view class="avatar-select-inner">
<image
v-if="userAvatar"
class="avatar-inner-img"
:src="userAvatar"
mode="aspectFill"
></image>
</view>
</view>
</view>
<!-- 手机号 -->
<view class="phone-row">
<text class="phone-text">{{ maskedPhone }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 一键登录按钮 -->
<view class="primary-btn-wrapper">
<view class="primary-btn" @tap="handleQuickLogin">
<text class="primary-btn-text">一键登录</text>
</view>
</view>
<!-- 其他账号登录 -->
<view class="secondary-btn-wrapper">
<view class="secondary-btn" @tap="handleOtherLogin">
<text class="secondary-btn-text">其他账号登录</text>
</view>
</view>
<!-- 底部协议 -->
<view class="agreement-wrapper">
<view
class="agreement-checkbox"
:class="{ checked: agreedToTerms }"
@tap="toggleAgreement"
></view>
<view class="agreement-text">
<text class="agreement-line"
>登陆代表您已阅读并接受统一认证服务条款以及</text
>
<text
class="agreement-line agreement-link"
@tap="showAgreementModal"
>隐私政策和用户服务协议</text
>
</view>
</view>
</view>
</view>
<!-- 提示弹窗 -->
<view v-if="showTipDialog" class="tip-dialog-mask" @tap="closeTipDialog">
<view class="tip-dialog" @tap.stop>
<view class="tip-dialog-header">
<text class="tip-dialog-title">提示</text>
<text class="tip-dialog-close" @tap="closeTipDialog">×</text>
</view>
<view class="tip-dialog-content">
<text class="tip-text">阅读并同意以下条款</text>
<text class="tip-agreement-link" @tap="openAgreementFromTip"
>Topfans用户服务协议</text
>
</view>
</view>
</view>
<!-- 协议全文弹窗 -->
<view
v-if="showAgreementDialog"
class="agreement-dialog-mask"
@tap="closeAgreementDialog"
>
<view class="agreement-dialog" @tap.stop>
<view class="agreement-dialog-header">
<text class="agreement-dialog-title">Topfans用户服务协议</text>
<text class="agreement-dialog-close" @tap="closeAgreementDialog"
>×</text
>
</view>
<scroll-view class="agreement-dialog-content" scroll-y>
<text class="agreement-dialog-text">{{
agreementContent || "协议内容加载中..."
}}</text>
</scroll-view>
<view class="agreement-dialog-footer">
<button class="agreement-confirm-btn" @tap="agreeAndClose">
我同意
</button>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import { ref } from "vue";
import LoginBackground from "@/components/LoginBackground.vue";
import { AGREEMENT_CONTENT } from "@/utils/agreement";
// user
const userInfo = JSON.parse(uni.getStorageSync("user")) || {};
//
const maskPhone = (phone) => {
if (!phone) return "";
const s = String(phone);
if (s.length < 7) return s;
if (s.includes("*")) return s; //
return `${s.slice(0, 3)}****${s.slice(-4)}`;
};
const maskedPhone = ref(
userInfo.mobile_masked || maskPhone(userInfo.mobile) || "174****2223",
);
const userAvatar = ref(
userInfo.avatar_url || "/static/login/portal/quicklogin-avatar.png",
);
const agreedToTerms = ref(false);
//
const agreementContent = ref("");
const showAgreementDialog = ref(false);
const showTipDialog = ref(false);
const toggleAgreement = () => {
agreedToTerms.value = !agreedToTerms.value;
};
//
const handleQuickLogin = () => {
if (!agreedToTerms.value) {
showTipDialog.value = true;
return;
}
uni.showLoading({ title: "登录中..." });
setTimeout(() => {
uni.hideLoading();
// TopfansWelcome
uni.setStorageSync("needs_welcome", true);
uni.reLaunch({ url: "/pages/square/square" });
}, 800);
};
//
const handleOtherLogin = () => {
uni.reLaunch({ url: "/pages/login/portal" });
};
//
const showAgreementModal = () => {
if (!agreementContent.value) {
agreementContent.value = AGREEMENT_CONTENT;
}
showAgreementDialog.value = true;
};
//
const closeAgreementDialog = () => {
showAgreementDialog.value = false;
};
//
const agreeAndClose = () => {
agreedToTerms.value = true;
showAgreementDialog.value = false;
};
//
const closeTipDialog = () => {
showTipDialog.value = false;
};
//
const openAgreementFromTip = () => {
showTipDialog.value = false;
setTimeout(() => {
showAgreementModal();
}, 300);
};
</script>
<style scoped>
.quicklogin-content {
position: absolute;
z-index: 10;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
top: 160rpx;
}
/* 卡片 */
.quicklogin-card {
width: 710rpx;
padding: 30rpx 40rpx 50rpx;
box-sizing: border-box;
border-radius: 56rpx;
border: 2rpx solid rgba(255, 255, 255, 0.15);
background: rgba(241, 163, 163, 0.17);
backdrop-filter: blur(64rpx);
-webkit-backdrop-filter: blur(64rpx);
box-shadow: 0 0 41rpx 0 rgba(0, 0, 0, 0.31);
display: flex;
flex-direction: column;
align-items: center;
}
/* 标题 */
.card-title-wrapper {
width: 270rpx;
height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
background:
linear-gradient(240deg, #cdaffa 9.77%, rgba(205, 175, 250, 0) 62.44%),
linear-gradient(108deg, #f7cbbf 13.91%, rgba(247, 203, 191, 0) 51.34%),
linear-gradient(297deg, #81a9f8 31.28%, rgba(129, 169, 248, 0) 90.74%),
linear-gradient(80deg, #69f4e9 39.86%, rgba(164, 252, 245, 0) 66.88%);
border-radius: 84rpx;
margin-top: 88rpx;
margin-bottom: 30rpx;
}
.card-title {
font-size: 40rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 头像 */
.avatar-wrapper {
position: relative;
width: 270rpx;
height: 270rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 48rpx 0;
}
.avatar-select-box {
position: relative;
width: 270rpx;
height: 270rpx;
}
.avatar-select-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
filter: drop-shadow(0 0 23rpx rgba(135, 16, 16, 0.67));
}
.avatar-select-inner {
position: absolute;
top: 18rpx;
left: 18rpx;
right: 18rpx;
bottom: 18rpx;
border-radius: 50%;
background: linear-gradient(
147deg,
rgba(223, 223, 146, 0.87) 17%,
rgba(135, 197, 216, 0.87) 39%,
rgba(194, 123, 209, 0.87) 99%
);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.avatar-inner-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
/* 手机号 */
.phone-row {
margin-bottom: 56rpx;
}
.phone-text {
font-size: 60rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Acme", sans-serif;
letter-spacing: 4rpx;
}
/* 分割线 */
.divider {
width: 584rpx;
height: 2rpx;
background: rgba(255, 228, 228, 0.46);
border-radius: 20rpx;
margin: 0 0 62rpx;
}
/* 一键登录按钮 */
.primary-btn-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 32rpx;
}
.primary-btn {
width: 394rpx;
height: 104rpx;
border-radius: 58rpx;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.41) 0%,
rgba(252, 100, 102, 0.83) 64%,
rgba(244, 88, 104, 0.83) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
}
.primary-btn-text {
font-size: 42rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 其他账号登录 */
.secondary-btn-wrapper {
width: 100%;
display: flex;
justify-content: center;
}
.secondary-btn {
width: 394rpx;
height: 104rpx;
border-radius: 58rpx;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.06) 0%,
rgba(252, 100, 102, 0.12) 64%,
rgba(244, 88, 104, 0.12) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
}
.secondary-btn-text {
font-size: 42rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 底部协议 */
.agreement-wrapper {
margin-top: 200rpx;
width: 100%;
display: flex;
align-items: center;
padding: 0 8rpx;
}
.agreement-checkbox {
width: 44rpx;
height: 44rpx;
border-radius: 58rpx;
background: rgba(59, 58, 56, 0.19);
box-shadow: 4rpx 4rpx 18rpx 0 rgba(242, 21, 21, 0.27);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.96;
}
.agreement-checkbox.checked {
background: #fff;
}
.agreement-text {
margin-left: 12rpx;
display: flex;
flex-direction: column;
}
.agreement-line {
font-size: 24rpx;
color: #fff;
text-shadow: 0 2rpx 13rpx rgba(0, 0, 0, 1);
font-family: "Abhaya Libre Medium", sans-serif;
line-height: 1.4;
}
.agreement-link {
text-decoration: underline;
cursor: pointer;
}
/* 提示弹窗样式 */
.tip-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.tip-dialog {
width: 100%;
max-width: 600rpx;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tip-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.tip-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.tip-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.tip-dialog-content {
padding: 40rpx;
text-align: center;
}
.tip-text {
font-size: 32rpx;
color: #333333;
line-height: 1.6;
display: block;
margin-bottom: 20rpx;
}
.tip-agreement-link {
font-size: 32rpx;
color: #007aff;
text-decoration: underline;
cursor: pointer;
display: block;
}
/* 协议全文弹窗 */
.agreement-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.agreement-dialog {
width: 100%;
max-width: 700rpx;
max-height: 80vh;
background: #e6e6e6;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.agreement-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
}
.agreement-dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
flex: 1;
text-align: center;
}
.agreement-dialog-close {
font-size: 48rpx;
color: #999999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
}
.agreement-dialog-content {
flex: 1;
padding: 40rpx;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
}
.agreement-dialog-text {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
width: 100%;
box-sizing: border-box;
display: block;
}
.agreement-dialog-footer {
padding: 30rpx 40rpx;
border-top: 1rpx solid #e5e5e5;
}
.agreement-confirm-btn {
width: 100%;
height: 88rpx;
background: #000000;
color: #e6e6e6;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.agreement-confirm-btn::after {
border: none;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,8 @@
<template> <template>
<view class="square-container"> <!-- TOPFANS 欢迎页确认后才显示主内容 -->
<TopfansWelcome v-if="showWelcome" @enter="handleEnterTopfans" />
<view v-else class="square-container">
<!-- 背景图片 --> <!-- 背景图片 -->
<!-- <image <!-- <image
class="bg-wrapper" class="bg-wrapper"
@ -75,10 +78,7 @@
</view> </view>
<!-- 在线榜单区块 - 仅在 星榜 时显示 --> <!-- 在线榜单区块 - 仅在 星榜 时显示 -->
<view <view v-if="activeContentTab === 'xingbang'" class="hot-category-wrapper">
v-if="activeContentTab === 'xingbang'"
class="hot-category-wrapper"
>
<HotCategoryBlock @cardClick="handleCardClick" /> <HotCategoryBlock @cardClick="handleCardClick" />
</view> </view>
@ -105,6 +105,7 @@ import ContentTabs from "./components/ContentTabs.vue";
import HotCategoryBlock from "./components/HotCategoryBlock.vue"; import HotCategoryBlock from "./components/HotCategoryBlock.vue";
import CreationGrid from "./components/CreationGrid.vue"; import CreationGrid from "./components/CreationGrid.vue";
import StarGalaxy from "./components/StarGalaxy/index.vue"; import StarGalaxy from "./components/StarGalaxy/index.vue";
import TopfansWelcome from "@/components/TopfansWelcome.vue";
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js' // import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from "./composables/useBanner.js"; import { useBanner } from "./composables/useBanner.js";
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh.js"; import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh.js";
@ -117,6 +118,24 @@ const currentStarId = ref(uni.getStorageSync("star_id") || null);
// ========== UI State ========== // ========== UI State ==========
const activeContentTab = ref("xinghe"); const activeContentTab = ref("xinghe");
// TOPFANS
// 1) needs_welcome=truequickLogin /
// app /
// 2) !globalData.welcomeShownThisSession app
// App.vue onLaunch false""""
// onShow home
const showWelcome = ref(
!!uni.getStorageSync("needs_welcome") &&
!getApp().globalData.welcomeShownThisSession,
);
const handleEnterTopfans = () => {
showWelcome.value = false;
// square
// needs_welcome app
getApp().globalData.welcomeShownThisSession = true;
};
const navExpanded = ref(false); const navExpanded = ref(false);
const showRankingModal = ref(false); const showRankingModal = ref(false);
const creationGridRef = ref(null); const creationGridRef = ref(null);
@ -198,20 +217,22 @@ const handleBannerClick = (banner) => {
// banner urlroute + params(JSON query) // banner urlroute + params(JSON query)
const buildBannerUrl = (banner) => { const buildBannerUrl = (banner) => {
const base = banner.route || "" const base = banner.route || "";
if (!banner.params) return base if (!banner.params) return base;
try { try {
const obj = JSON.parse(banner.params) const obj = JSON.parse(banner.params);
const entries = Object.entries(obj) const entries = Object.entries(obj)
.filter(([, v]) => v !== undefined && v !== null) .filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .map(
if (entries.length === 0) return base ([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`,
const sep = base.includes("?") ? "&" : "?" );
return `${base}${sep}${entries.join("&")}` if (entries.length === 0) return base;
const sep = base.includes("?") ? "&" : "?";
return `${base}${sep}${entries.join("&")}`;
} catch (e) { } catch (e) {
// params route // params route
console.warn("[square] banner.params 解析失败, 忽略", banner.params, e) console.warn("[square] banner.params 解析失败, 忽略", banner.params, e);
return base return base;
} }
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -79,6 +79,18 @@ const mutations = {
// 清除每日登录打卡记录 // 清除每日登录打卡记录
const today = new Date().toISOString().split('T')[0] const today = new Date().toISOString().split('T')[0]
uni.removeStorageSync(`daily_login_completed_${today}`) uni.removeStorageSync(`daily_login_completed_${today}`)
// 重置 TopfansWelcome 相关标记让登出后再登录quickLogin / 注册)能再次展示欢迎页
// 同时清掉 storage 和内存标记,避免 password 登录误触发(见 store/modules/user.js 注释表)
uni.removeStorageSync('needs_welcome')
try {
const app = getApp()
if (app && app.globalData) {
app.globalData.welcomeShownThisSession = false
}
} catch (e) {
// getApp() 在非 App 上下文不可用,忽略即可
}
} }
} }