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";
export default {
// uni.storage needs_welcome
// TopfansWelcome onLaunch false =
// square handleEnterTopfans true
globalData: {
welcomeShownThisSession: false,
},
onLaunch: function () {
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
// getDeviceFingerprint setStorageSync
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",
"style": {

View File

@ -1,709 +1,365 @@
<template>
<view class="login-container">
<!-- 背景图片层 -->
<image class="background-image" src="/static/background/login-bg.png" mode="aspectFill"></image>
<!-- 女孩图片 -->
<image class="girl-image" src="/static/login/person-photo.png" mode="aspectFit"></image>
<!-- 粉色玩偶遮挡女孩脸的下半部分 -->
<image class="doll-image" src="/static/login/login-character.png" mode="aspectFit"></image>
<view class="background-overlay"></view>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- TOPFANS 标题 -->
<view class="title-wrapper">
<text class="title-text">TOPFANS</text>
</view>
<!-- 表单区域居中部分 -->
<view class="form-container">
<view class="form-wrapper">
<!-- 手机号输入框 -->
<view class="input-wrapper">
<input
class="input-field"
type="number"
v-model="form.phone"
placeholder="请输入手机号"
maxlength="11"
placeholder-class="input-placeholder"
/>
</view>
<!-- 密码输入框 -->
<view class="input-wrapper password-wrapper">
<input
class="input-field"
:type="showPassword ? 'text' : 'password'"
v-model="form.password"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
<view class="eye-icon" @click="togglePassword">
<text class="eye-text">{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
<!-- 登录按钮 -->
<button class="btn-primary" @click="handleLogin">登录</button>
<!-- 注册按钮 -->
<view class="register-link" @click="goToRegister">
<text>还没有账号去注册</text>
</view>
</view>
</view>
<!-- 底部服务条款 -->
<view class="footer-text">
<view class="agreement-wrapper">
<view class="agreement-checkbox" @click="toggleAgreement">
<view class="custom-checkbox" :class="{ 'checked': agreedToTerms }">
<view v-if="agreedToTerms" class="checkbox-inner"></view>
</view>
<text class="agreement-label">我已阅读并同意</text>
</view>
<text class="agreement-link" @click="showAgreementModal">Topfans用户服务协议</text>
</view>
</view>
</view>
<!-- 提示弹窗 -->
<view v-if="showTipDialog" class="tip-dialog-mask" @click="closeTipDialog">
<view class="tip-dialog" @click.stop>
<view class="tip-dialog-header">
<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>
<LoginBackground :show-logo="true">
<!-- 内容区域 -->
<view class="login-content">
<!-- 头像 -->
<view class="avatar-wrapper">
<image
class="avatar-image"
src="/static/login/portal/register-avatar.png"
mode="aspectFit"
></image>
</view>
<!-- 登录卡片 -->
<view class="login-card">
<!-- 标题 -->
<view class="card-title-wrapper">
<text class="card-title">账号密码登录</text>
</view>
<!-- 表单 -->
<view class="form-area">
<!-- 手机号 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input
class="input-field"
type="number"
v-model="form.phone"
placeholder="输入您的手机号"
placeholder-class="input-placeholder"
maxlength="11"
/>
</view>
</view>
</view>
<!-- 密码 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input
class="input-field"
:type="showPassword ? 'text' : 'password'"
v-model="form.password"
placeholder="输入您的密码"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
</view>
<!-- 忘记密码 -->
<view class="forgot-password-wrapper">
<text class="forgot-password" @tap="handleForgotPassword"
>忘记密码</text
>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 登录按钮 -->
<view class="login-btn-wrapper">
<view class="login-btn" @tap="handleLogin">
<text class="login-btn-text">登录</text>
</view>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { useStore } from 'vuex';
import { validatePhone, validatePassword } from '@/utils/validator';
import { AGREEMENT_CONTENT } from '@/utils/agreement';
import { resetAllGuides } from '@/utils/guideConfig';
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useStore } from "vuex";
import LoginBackground from "@/components/LoginBackground.vue";
import { validatePhone, validatePassword } from "@/utils/validator";
import { resetAllGuides } from "@/utils/guideConfig";
const store = useStore();
//
const form = ref({
phone: '',
password: ''
phone: "",
password: "",
});
const showPassword = ref(false);
const errorMessage = ref('');
const agreedToTerms = ref(false);
const agreementContent = ref('');
const showAgreementDialog = ref(false);
const showTipDialog = ref(false);
const errorMessage = ref("");
//
const getPageParams = () => {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.options && currentPage.options.error) {
errorMessage.value = decodeURIComponent(currentPage.options.error)
}
}
}
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
if (currentPage.options && currentPage.options.error) {
errorMessage.value = decodeURIComponent(currentPage.options.error);
}
}
};
//
onLoad(() => {
getPageParams()
})
//
const getAgreementContent = () => {
return AGREEMENT_CONTENT;
};
getPageParams();
});
// /
const togglePassword = () => {
showPassword.value = !showPassword.value;
showPassword.value = !showPassword.value;
};
//
const goToRegister = () => {
//
resetAllGuides();
uni.removeStorageSync('is_new_user');
uni.removeStorageSync('has_seen_welcome');
uni.navigateTo({
url: '/pages/register/register'
});
resetAllGuides();
uni.removeStorageSync("is_new_user");
uni.removeStorageSync("has_seen_welcome");
uni.reLaunch({ url: "/pages/register/register" });
};
//
const toggleAgreement = () => {
agreedToTerms.value = !agreedToTerms.value;
};
//
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 handleForgotPassword = () => {
uni.showToast({ title: "忘记密码功能开发中", icon: "none" });
};
//
const handleLogin = async () => {
//
if (!agreedToTerms.value) {
openTipDialog();
return;
}
//
const phoneValidation = validatePhone(form.value.phone);
if (!phoneValidation.valid) {
errorMessage.value = phoneValidation.message;
return;
}
const passwordValidation = validatePassword(form.value.password);
if (!passwordValidation.valid) {
errorMessage.value = passwordValidation.message;
return;
}
errorMessage.value = '';
try {
//
uni.showLoading({
title: '登录中...',
mask: true
});
await store.dispatch('user/login', {
mobile: form.value.phone,
password: form.value.password
});
uni.hideLoading();
//
uni.reLaunch({
url: '/pages/square/square'
});
} catch (error) {
uni.hideLoading();
// message
errorMessage.value = error.message || '登录失败,请重试';
uni.showToast({
title: errorMessage.value,
icon: 'none',
duration: 2000
});
}
const phoneValidation = validatePhone(form.value.phone);
if (!phoneValidation.valid) {
errorMessage.value = phoneValidation.message;
return;
}
const passwordValidation = validatePassword(form.value.password);
if (!passwordValidation.valid) {
errorMessage.value = passwordValidation.message;
return;
}
errorMessage.value = "";
try {
uni.showLoading({ title: "登录中...", mask: true });
await store.dispatch("user/login", {
mobile: form.value.phone,
password: form.value.password,
});
// needs_welcome=true TopfansWelcome
uni.setStorageSync("needs_welcome", true);
uni.hideLoading();
uni.reLaunch({ url: "/pages/square/square" });
} catch (error) {
uni.hideLoading();
errorMessage.value = error.message || "登录失败,请重试";
uni.showToast({
title: errorMessage.value,
icon: "none",
duration: 2000,
});
}
};
</script>
<style scoped>
.login-container {
position: relative;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
.login-content {
position: relative;
z-index: 10;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 80rpx;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
/* 头像 */
.avatar-wrapper {
width: 296rpx;
height: 274rpx;
margin-top: 130rpx;
filter: drop-shadow(2rpx 4rpx 98rpx rgba(0, 0, 0, 0.94));
z-index: 11;
}
.girl-image {
position: absolute;
top: 55%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
width: 180%;
height: 180%;
z-index: 1;
object-fit: contain;
object-position: center center;
.avatar-image {
width: 100%;
height: 100%;
}
.doll-image {
position: absolute;
bottom: -70%;
left: 45%;
transform: translateX(-50%);
width: 150%;
height: 150%;
z-index: 2;
object-fit: contain;
object-position: center bottom;
/* 卡片 */
.login-card {
width: 710rpx;
margin-top: -104rpx;
padding: 30rpx 104rpx 30rpx;
box-sizing: border-box;
border-radius: 56rpx;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(64rpx);
-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;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 3;
/* 标题 */
.card-title-wrapper {
position: relative;
width: 324rpx;
height: 84rpx;
display: flex;
align-items: center;
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 {
position: relative;
z-index: 10;
width: 100%;
flex: 1;
min-height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 60rpx 60rpx 60rpx;
box-sizing: border-box;
.card-title {
position: relative;
z-index: 1;
font-size: 44rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
.title-wrapper {
width: 100%;
padding-top: 120rpx;
margin-bottom: 80rpx;
text-align: center;
position: relative;
flex-shrink: 0;
/* 表单 */
.form-area {
width: 100%;
display: flex;
flex-direction: column;
gap: 32rpx;
padding: 0 14rpx;
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
.input-group {
width: 100%;
}
.title-text {
font-size: 150rpx;
font-weight: 600;
letter-spacing: 12rpx;
background: linear-gradient(to right, #B52920 0%, #86D9E0 80%, #56C1FF 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
line-height: 1.2;
text-transform: uppercase;
font-family: 'TheMiladiatorRegular', sans-serif !important;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.15));
display: flex;
justify-content: center;
.input-outer {
width: 100%;
height: 74rpx;
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;
padding: 4rpx;
box-sizing: border-box;
}
.form-wrapper {
width: 100%;
}
.input-wrapper {
position: relative;
width: 100%;
height: 100rpx;
margin-bottom: 40rpx;
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-inner {
width: 100%;
height: 60rpx;
border-radius: 58rpx;
background: rgba(255, 255, 255, 0.65);
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
}
.input-field {
flex: 1;
height: 100%;
font-size: 32rpx;
color: #333333;
width: 100%;
height: 100%;
font-size: 30rpx;
color: #333;
}
.input-placeholder {
color: #999999;
}
.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;
font-size: 30rpx;
color: #565656;
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 错误提示 */
.error-message {
width: 100%;
padding: 20rpx 0;
margin-bottom: 20rpx;
text-align: center;
width: 100%;
padding-left: 24rpx;
}
.error-message text {
font-size: 28rpx;
color: #ff4444;
font-size: 24rpx;
color: #ff6b6b;
}
.btn-primary {
width: 100%;
height: 100rpx;
margin-bottom: 30rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 50rpx;
border: none;
font-size: 36rpx;
font-weight: bold;
color: #e6e6e6;
display: flex;
align-items: center;
justify-content: center;
/* 忘记密码 */
.forgot-password-wrapper {
width: 100%;
display: flex;
justify-content: center;
/* padding: 0 18rpx; */
margin-top: 48rpx;
margin-bottom: 24rpx;
}
.btn-primary::after {
border: none;
.forgot-password {
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%;
margin-top: 40rpx;
text-align: center;
/* 分割线 */
.divider {
width: 612rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 28rpx 0 24rpx;
}
.register-link text {
font-size: 28rpx;
color: #e6e6e6;
text-decoration: underline;
/* 登录按钮 */
.login-btn-wrapper {
width: 100%;
display: flex;
justify-content: center;
padding: 64rpx 0;
}
.footer-text {
position: relative;
width: 100%;
margin-top: auto;
padding: 0rpx 0 80rpx 0;
text-align: center;
.login-btn {
width: 270rpx;
height: 90rpx;
border-radius: 58rpx;
background: linear-gradient(
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 {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
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;
.login-btn-text {
font-size: 36rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
</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>
<view class="square-container">
<!-- TOPFANS 欢迎页确认后才显示主内容 -->
<TopfansWelcome v-if="showWelcome" @enter="handleEnterTopfans" />
<view v-else class="square-container">
<!-- 背景图片 -->
<!-- <image
class="bg-wrapper"
@ -75,10 +78,7 @@
</view>
<!-- 在线榜单区块 - 仅在 星榜 时显示 -->
<view
v-if="activeContentTab === 'xingbang'"
class="hot-category-wrapper"
>
<view v-if="activeContentTab === 'xingbang'" class="hot-category-wrapper">
<HotCategoryBlock @cardClick="handleCardClick" />
</view>
@ -105,6 +105,7 @@ import ContentTabs from "./components/ContentTabs.vue";
import HotCategoryBlock from "./components/HotCategoryBlock.vue";
import CreationGrid from "./components/CreationGrid.vue";
import StarGalaxy from "./components/StarGalaxy/index.vue";
import TopfansWelcome from "@/components/TopfansWelcome.vue";
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from "./composables/useBanner.js";
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh.js";
@ -117,6 +118,24 @@ const currentStarId = ref(uni.getStorageSync("star_id") || null);
// ========== UI State ==========
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 showRankingModal = ref(false);
const creationGridRef = ref(null);
@ -198,20 +217,22 @@ const handleBannerClick = (banner) => {
// banner urlroute + params(JSON query)
const buildBannerUrl = (banner) => {
const base = banner.route || ""
if (!banner.params) return base
const base = banner.route || "";
if (!banner.params) return base;
try {
const obj = JSON.parse(banner.params)
const obj = JSON.parse(banner.params);
const entries = Object.entries(obj)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
if (entries.length === 0) return base
const sep = base.includes("?") ? "&" : "?"
return `${base}${sep}${entries.join("&")}`
.map(
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`,
);
if (entries.length === 0) return base;
const sep = base.includes("?") ? "&" : "?";
return `${base}${sep}${entries.join("&")}`;
} catch (e) {
// params route
console.warn("[square] banner.params 解析失败, 忽略", banner.params, e)
return base
console.warn("[square] banner.params 解析失败, 忽略", banner.params, e);
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]
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 上下文不可用,忽略即可
}
}
}