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,45 +1,53 @@
<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>
<LoginBackground :show-logo="true">
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- TOPFANS 标题 -->
<view class="title-wrapper">
<text class="title-text">TOPFANS</text>
<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="form-container">
<view class="form-wrapper">
<!-- 手机号输入框 -->
<view class="input-wrapper">
<!-- 登录卡片 -->
<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="请输入手机号"
maxlength="11"
placeholder="输入您的手机号"
placeholder-class="input-placeholder"
maxlength="11"
/>
</view>
</view>
</view>
<!-- 密码输入框 -->
<view class="input-wrapper password-wrapper">
<!-- 密码 -->
<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="输入您的密码"
placeholder-class="input-placeholder"
/>
<view class="eye-icon" @click="togglePassword">
<text class="eye-text">{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
</view>
@ -47,105 +55,62 @@
<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>
<!-- 登录按钮 -->
<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 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()
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const currentPage = pages[pages.length - 1];
if (currentPage.options && currentPage.options.error) {
errorMessage.value = decodeURIComponent(currentPage.options.error)
}
errorMessage.value = decodeURIComponent(currentPage.options.error);
}
}
};
//
onLoad(() => {
getPageParams()
})
//
const getAgreementContent = () => {
return AGREEMENT_CONTENT;
};
getPageParams();
});
// /
const togglePassword = () => {
@ -154,70 +119,19 @@ const togglePassword = () => {
//
const goToRegister = () => {
//
resetAllGuides();
uni.removeStorageSync('is_new_user');
uni.removeStorageSync('has_seen_welcome');
uni.navigateTo({
url: '/pages/register/register'
});
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;
@ -230,480 +144,222 @@ const handleLogin = async () => {
return;
}
errorMessage.value = '';
errorMessage.value = "";
try {
//
uni.showLoading({
title: '登录中...',
mask: true
uni.showLoading({ title: "登录中...", mask: true });
await store.dispatch("user/login", {
mobile: form.value.phone,
password: form.value.password,
});
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'
});
uni.reLaunch({ url: "/pages/square/square" });
} catch (error) {
uni.hideLoading();
// message
errorMessage.value = error.message || '登录失败,请重试';
errorMessage.value = error.message || "登录失败,请重试";
uni.showToast({
title: errorMessage.value,
icon: 'none',
duration: 2000
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;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.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;
}
.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;
}
.background-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 3;
}
.content-wrapper {
.login-content {
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;
padding-bottom: 80rpx;
}
.title-wrapper {
/* 头像 */
.avatar-wrapper {
width: 296rpx;
height: 274rpx;
margin-top: 130rpx;
filter: drop-shadow(2rpx 4rpx 98rpx rgba(0, 0, 0, 0.94));
z-index: 11;
}
.avatar-image {
width: 100%;
padding-top: 120rpx;
margin-bottom: 80rpx;
text-align: center;
position: relative;
flex-shrink: 0;
height: 100%;
}
.form-container {
flex: 1;
/* 卡片 */
.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;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
z-index: 12;
}
.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;
}
.form-wrapper {
width: 100%;
}
.input-wrapper {
/* 标题 */
.card-title-wrapper {
position: relative;
width: 100%;
height: 100rpx;
margin-bottom: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50rpx;
width: 324rpx;
height: 84rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
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;
}
.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;
}
/* 表单 */
.form-area {
width: 100%;
display: flex;
flex-direction: column;
gap: 32rpx;
padding: 0 14rpx;
}
.input-group {
width: 100%;
}
.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;
}
.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;
width: 100%;
height: 100%;
font-size: 32rpx;
color: #333333;
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;
padding-left: 24rpx;
}
.error-message text {
font-size: 28rpx;
color: #ff4444;
}
.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;
}
.btn-primary::after {
border: none;
}
.register-link {
width: 100%;
margin-top: 40rpx;
text-align: center;
}
.register-link text {
font-size: 28rpx;
color: #e6e6e6;
text-decoration: underline;
}
.footer-text {
position: relative;
width: 100%;
margin-top: auto;
padding: 0rpx 0 80rpx 0;
text-align: center;
}
.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;
color: #ff6b6b;
}
.agreement-link {
font-size: 24rpx;
color: #e6e6e6;
text-decoration: underline;
cursor: pointer;
/* 忘记密码 */
.forgot-password-wrapper {
width: 100%;
display: flex;
justify-content: center;
/* padding: 0 18rpx; */
margin-top: 48rpx;
margin-bottom: 24rpx;
}
/* 协议弹窗样式 */
.agreement-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
.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;
}
/* 分割线 */
.divider {
width: 612rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 28rpx 0 24rpx;
}
/* 登录按钮 */
.login-btn-wrapper {
width: 100%;
display: flex;
justify-content: center;
padding: 64rpx 0;
}
.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;
padding: 40rpx;
box-sizing: border-box;
opacity: 0.96;
}
.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 {
.login-btn-text {
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;
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

View File

@ -1,120 +1,150 @@
<template>
<view class="set-nickname-container">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<view class="background-overlay"></view>
<LoginBackground :show-logo="true">
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">设置头像与昵称</text>
<view class="nickname-content">
<!-- 头像 -->
<view class="avatar-wrapper">
<image
class="avatar-image"
src="/static/login/portal/register-avatar.png"
mode="aspectFit"
></image>
</view>
<!-- 表单区域居中部分 -->
<view class="form-container">
<!-- 头像点击可上传 -->
<view class="avatar-wrapper" @tap="handleAvatarClick">
<Avatar :nickname="nickname" :size="200" :borderWidth="8" :avatarUrl="userAvatarUrl" />
<view class="avatar-edit-badge">
<text class="avatar-edit-icon"></text>
<!-- 设置卡片 -->
<view class="nickname-card">
<!-- 标题 -->
<view class="card-title-wrapper">
<text class="card-title">设置头像与昵称</text>
</view>
<!-- 头像选择区域 -->
<view class="avatar-select-wrapper" @tap="handleAvatarClick">
<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="userAvatarUrl"
class="avatar-inner-img"
:src="userAvatarUrl"
mode="aspectFill"
></image>
<image
v-else
class="avatar-edit-icon"
src="/static/login/portal/setnickname-edit-icon.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
<!-- 昵称输入框 -->
<view class="input-wrapper">
<!-- 表单 -->
<view class="form-area">
<!-- 昵称输入 -->
<view class="input-group">
<view class="input-outer">
<view class="input-inner">
<input
class="input-field"
type="text"
v-model="nickname"
placeholder="请输入昵称"
maxlength="20"
placeholder-class="input-placeholder"
maxlength="20"
/>
</view>
</view>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
</view>
<!-- 下一步按钮圆形箭头 -->
<view class="next-button-wrapper">
<button class="btn-next" @click="handleNext">
<text class="arrow-icon"></text>
</button>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 下一步按钮 -->
<view class="next-btn-wrapper">
<view class="next-btn" @tap="handleNext">
<text class="next-btn-text">下一步</text>
</view>
</view>
</view>
</view>
<!-- 头像上传弹窗 -->
<view class="avatar-modal" v-if="showAvatarModal" @tap="closeAvatarModal">
<view class="modal-content" @tap.stop>
<view
v-if="showAvatarModal"
class="avatar-modal-mask"
@tap="closeAvatarModal"
>
<view class="avatar-modal" @tap.stop>
<view class="modal-title">设置头像</view>
<!-- 头像预览 -->
<view class="avatar-preview">
<Avatar :nickname="nickname" :size="180" :borderWidth="6" :avatarUrl="userAvatarUrl" />
<image
v-if="userAvatarUrl"
class="avatar-preview-img"
:src="userAvatarUrl"
mode="aspectFill"
></image>
</view>
<!-- 上传按钮 -->
<button class="upload-avatar-btn" @tap="handleUploadAvatar" :disabled="uploadingAvatar">
{{ uploadingAvatar ? '上传中...' : '上传头像' }}
<button
class="upload-avatar-btn"
@tap="handleUploadAvatar"
:disabled="uploadingAvatar"
>
{{ uploadingAvatar ? "上传中..." : "上传头像" }}
</button>
<view class="upload-hint">支持JPGPNG格式大小不超过10MB</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeAvatarModal">取消</button>
</view>
<button class="modal-btn-cancel" @tap="closeAvatarModal">确认</button>
</view>
</view>
</view>
</LoginBackground>
</template>
<script setup>
import { ref } from 'vue';
import { useStore } from 'vuex';
import Avatar from '../components/Avatar.vue';
import { checkNicknameApi, getPublicOssSignatureApi } from '@/utils/api.js';
import { validateNickname } from '@/utils/validator.js';
import { ref } from "vue";
import { useStore } from "vuex";
import LoginBackground from "@/components/LoginBackground.vue";
import { checkNicknameApi, getPublicOssSignatureApi } from "@/utils/api.js";
import { validateNickname } from "@/utils/validator.js";
const store = useStore();
//
const nickname = ref('');
const errorMessage = ref('');
const nickname = ref("");
const errorMessage = ref("");
const isChecking = ref(false);
//
const userAvatarUrl = ref('');
const userAvatarUrl = ref("");
const showAvatarModal = ref(false);
const uploadingAvatar = ref(false);
const goToAuthPage = () => {
const hasRegisterDraft = Boolean(uni.getStorageSync('temp_register_mobile'));
const authPageUrl = hasRegisterDraft ? '/pages/register/register' : '/pages/login/login';
uni.reLaunch({
url: authPageUrl
});
const hasRegisterDraft = Boolean(uni.getStorageSync("temp_register_mobile"));
const authPageUrl = hasRegisterDraft
? "/pages/register/register"
: "/pages/login/login";
uni.reLaunch({ url: authPageUrl });
};
//
const goBack = () => {
const pages = getCurrentPages();
// /
if (pages.length > 1) {
uni.navigateBack({
fail: goToAuthPage
});
uni.navigateBack({ fail: goToAuthPage });
return;
}
goToAuthPage();
};
@ -133,35 +163,28 @@ const closeAvatarModal = () => {
const handleUploadAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
sizeType: ["compressed"],
sourceType: ["album", "camera"],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0];
uni.getFileInfo({
filePath: tempFilePath,
success: async (fileInfo) => {
if (fileInfo.size > 10 * 1024 * 1024) {
uni.showToast({
title: '图片大小不能超过10MB',
icon: 'none'
});
uni.showToast({ title: "图片大小不能超过10MB", icon: "none" });
return;
}
await uploadAvatarToOss(tempFilePath);
},
fail: (error) => {
console.error('获取文件信息失败:', error);
uni.showToast({
title: '获取文件信息失败',
icon: 'none'
});
}
console.error("获取文件信息失败:", error);
uni.showToast({ title: "获取文件信息失败", icon: "none" });
},
});
},
fail: (error) => {
console.error('选择图片失败:', error);
}
console.error("选择图片失败:", error);
},
});
};
@ -169,67 +192,58 @@ const handleUploadAvatar = () => {
const uploadAvatarToOss = async (filePath) => {
try {
uploadingAvatar.value = true;
uni.showLoading({ title: '上传中...', mask: true });
uni.showLoading({ title: "上传中...", mask: true });
// 1. 使便/
const mobile = uni.getStorageSync('temp_register_mobile') || 'anon';
const signRes = await getPublicOssSignatureApi('register', mobile);
const mobile = uni.getStorageSync("temp_register_mobile") || "anon";
const signRes = await getPublicOssSignatureApi("register", mobile);
if (signRes.code !== 0) {
throw new Error(signRes.message || '获取签名失败');
throw new Error(signRes.message || "获取签名失败");
}
// 2. OSS key /
const ossKey = signRes.data.key
const ossKey = signRes.data.key;
uni.uploadFile({
url: signRes.data.host,
filePath: filePath,
name: 'file',
name: "file",
formData: {
key: ossKey,
policy: signRes.data.policy,
success_action_status: '200',
'x-oss-credential': signRes.data.x_oss_credential,
'x-oss-date': signRes.data.x_oss_date,
'x-oss-security-token': signRes.data.security_token,
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
success_action_status: "200",
"x-oss-credential": signRes.data.x_oss_credential,
"x-oss-date": signRes.data.x_oss_date,
"x-oss-security-token": signRes.data.security_token,
"x-oss-signature": signRes.data.signature,
"x-oss-signature-version": signRes.data.x_oss_signature_version,
},
success: (uploadRes) => {
uni.hideLoading();
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
userAvatarUrl.value = `${signRes.data.host}/${ossKey}`;
uni.showToast({
title: '头像已选择',
icon: 'success'
});
uni.showToast({ title: "头像已选择", icon: "success" });
closeAvatarModal();
} else {
uni.showToast({
title: `上传失败,状态码: ${uploadRes.statusCode}`,
icon: 'none',
duration: 2000
icon: "none",
duration: 2000,
});
}
uploadingAvatar.value = false;
},
fail: (error) => {
console.error('OSS上传失败:', error);
console.error("OSS上传失败:", error);
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none',
duration: 2000
});
uni.showToast({ title: "上传失败", icon: "none", duration: 2000 });
uploadingAvatar.value = false;
}
},
});
} catch (error) {
console.error('上传头像失败:', error);
console.error("上传头像失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || '上传失败',
icon: 'none',
duration: 2000
title: error.message || "上传失败",
icon: "none",
duration: 2000,
});
uploadingAvatar.value = false;
}
@ -237,115 +251,88 @@ const uploadAvatarToOss = async (filePath) => {
//
const handleNext = async () => {
//
const trimmedNickname = nickname.value.trim();
const validation = validateNickname(trimmedNickname);
if (!validation.valid) {
errorMessage.value = validation.message;
uni.showToast({
title: validation.message,
icon: 'none'
});
uni.showToast({ title: validation.message, icon: "none" });
return;
}
errorMessage.value = '';
errorMessage.value = "";
//
if (isChecking.value) return;
isChecking.value = true;
try {
//
const res = await checkNicknameApi(trimmedNickname);
if (res.data.exists === true) {
errorMessage.value = '该昵称已被注册';
uni.showToast({
title: '该昵称已被注册',
icon: 'none'
});
errorMessage.value = "该昵称已被注册";
uni.showToast({ title: "该昵称已被注册", icon: "none" });
isChecking.value = false;
return;
}
//
uni.setStorageSync('temp_register_nickname', trimmedNickname);
uni.setStorageSync("temp_register_nickname", trimmedNickname);
//
const mobile = uni.getStorageSync('temp_register_mobile');
const password = uni.getStorageSync('temp_register_password');
const star_id = 87; //
const verify_token = uni.getStorageSync('temp_register_verify_token') || '';
const avatar_url = userAvatarUrl.value || '';
const mobile = uni.getStorageSync("temp_register_mobile");
const password = uni.getStorageSync("temp_register_password");
const star_id = 87;
const verify_token = uni.getStorageSync("temp_register_verify_token") || "";
const avatar_url = userAvatarUrl.value || "";
//
if (!mobile || !password || !trimmedNickname || !star_id) {
uni.showToast({
title: '注册信息不完整,请重新注册',
icon: 'none'
title: "注册信息不完整,请重新注册",
icon: "none",
});
uni.removeStorageSync('temp_register_mobile');
uni.removeStorageSync('temp_register_password');
uni.removeStorageSync('temp_register_nickname');
uni.removeStorageSync('temp_register_verify_token');
uni.removeStorageSync("temp_register_mobile");
uni.removeStorageSync("temp_register_password");
uni.removeStorageSync("temp_register_nickname");
uni.removeStorageSync("temp_register_verify_token");
setTimeout(() => {
uni.reLaunch({
url: '/pages/register/register'
});
uni.reLaunch({ url: "/pages/register/register" });
}, 1500);
isChecking.value = false;
return;
}
//
uni.showLoading({
title: '注册中...',
mask: true
});
uni.showLoading({ title: "注册中...", mask: true });
// API
await store.dispatch('user/register', {
await store.dispatch("user/register", {
mobile,
password,
star_id,
nickname: trimmedNickname,
verify_token,
avatar_url
avatar_url,
});
uni.hideLoading();
//
uni.removeStorageSync('temp_register_mobile');
uni.removeStorageSync('temp_register_password');
uni.removeStorageSync('temp_register_nickname');
uni.removeStorageSync('temp_register_verify_token');
//
uni.setStorageSync('is_new_user', true);
//
uni.reLaunch({
url: '/pages/profile/selectRole'
});
uni.removeStorageSync("temp_register_mobile");
uni.removeStorageSync("temp_register_password");
uni.removeStorageSync("temp_register_nickname");
uni.removeStorageSync("temp_register_verify_token");
uni.setStorageSync("is_new_user", true);
// square TopfansWelcome
uni.setStorageSync("needs_welcome", true);
uni.reLaunch({ url: "/pages/profile/selectRole" });
} catch (error) {
uni.hideLoading();
// 409
if (error.code === 3 || error.message.includes('昵称') || error.message.includes('已存在')) {
if (
error.code === 3 ||
error.message.includes("昵称") ||
error.message.includes("已存在")
) {
uni.showModal({
title: '昵称已存在',
content: '该昵称已被使用,请返回修改昵称',
title: "昵称已存在",
content: "该昵称已被使用,请返回修改昵称",
showCancel: false,
confirmText: '知道了'
confirmText: "知道了",
});
} else {
errorMessage.value = error.message || '注册失败,请重试';
uni.showToast({
title: errorMessage.value,
icon: 'none'
});
errorMessage.value = error.message || "注册失败,请重试";
uni.showToast({ title: errorMessage.value, icon: "none" });
}
} finally {
isChecking.value = false;
@ -354,196 +341,247 @@ const handleNext = async () => {
</script>
<style scoped>
.set-nickname-container {
.nickname-content {
position: relative;
z-index: 10;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 120rpx;
}
.background-image {
position: absolute;
top: 0;
left: 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;
}
.avatar-image {
width: 100%;
height: 100%;
transform: scale(1.1);
}
.background-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
/* 卡片 */
.nickname-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;
}
.content-wrapper {
/* 标题 */
.card-title-wrapper {
position: relative;
width: 324rpx;
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;
}
.card-title {
position: relative;
z-index: 1;
width: 100%;
flex: 1;
min-height: 100%;
display: flex;
flex-direction: column;
padding-top: 80rpx;
box-sizing: border-box;
font-size: 40rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
font-family: "Abhaya Libre Medium", sans-serif;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
padding: 0 40rpx 40rpx 40rpx;
/* 头像选择区域 */
.avatar-select-wrapper {
position: relative;
}
.nav-back {
width: 60rpx;
height: 60rpx;
width: 270rpx;
height: 270rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 48rpx 0;
}
.back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #000000;
margin-right: 60rpx;
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
margin: 0 auto;
padding: 0 60rpx;
}
.avatar-wrapper {
.avatar-select-box {
position: relative;
width: 100%;
margin-bottom: 80rpx;
display: flex;
justify-content: center;
align-items: center;
width: 270rpx;
height: 270rpx;
}
.avatar-edit-badge {
.avatar-select-bg {
position: absolute;
right: 30%;
bottom: 0;
width: 56rpx;
height: 56rpx;
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(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
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;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
border: 4rpx solid #fff;
overflow: hidden;
}
n.avatar-inner-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder-text {
font-size: 26rpx;
color: #fff9e7;
text-shadow: -2rpx 2rpx 8rpx rgba(0, 0, 0, 0.84);
}
.avatar-edit-icon {
color: #fff;
font-size: 28rpx;
font-weight: bold;
line-height: 1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 74rpx;
height: 74rpx;
opacity: 0.58;
}
.input-wrapper {
position: relative;
/* 表单 */
.form-area {
width: 100%;
height: 100rpx;
margin-bottom: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
padding: 0 14rpx;
}
.input-group {
width: 100%;
}
.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: 0 40rpx;
padding: 4rpx;
box-sizing: border-box;
}
.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;
width: 100%;
height: 100%;
font-size: 32rpx;
color: #333333;
font-size: 30rpx;
color: #333;
}
.input-placeholder {
color: #999999;
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;
padding-left: 24rpx;
}
.error-message text {
font-size: 28rpx;
color: #ff4444;
font-size: 24rpx;
color: #ff6b6b;
}
.next-button-wrapper {
/* 分割线 */
.divider {
width: 612rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 100rpx;
margin: 28rpx 0 24rpx;
}
/* 下一步按钮 */
.next-btn-wrapper {
width: 100%;
margin-top: 60rpx;
display: flex;
justify-content: center;
align-items: center;
padding: 64rpx 0;
}
.btn-next {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #000000;
border: none;
.next-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;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
padding: 0;
transition: transform 0.2s ease;
opacity: 0.96;
}
.btn-next:active {
transform: scale(1.15);
.next-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;
}
.btn-next::after {
border: none;
}
.arrow-icon {
font-size: 48rpx;
font-weight: 900;
color: #e6e6e6;
line-height: 1;
}
/* 头像上传弹窗 */
.avatar-modal {
/* ===== 头像上传弹窗 ===== */
.avatar-modal-mask {
position: fixed;
top: 0;
left: 0;
@ -556,12 +594,10 @@ const handleNext = async () => {
z-index: 1000;
}
.modal-content {
.avatar-modal {
width: 80%;
max-width: 600rpx;
background-image: url('/static/starbookcontent/beijing.png');
background-size: cover;
background-position: center bottom;
background: #e6e6e6;
border-radius: 30rpx;
padding: 60rpx 40rpx;
box-sizing: border-box;
@ -582,15 +618,22 @@ const handleNext = async () => {
margin-bottom: 40rpx;
}
.avatar-preview-img {
width: 360rpx;
height: 360rpx;
border-radius: 50%;
object-fit: cover;
}
.upload-avatar-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border: none;
background: #000000;
margin-bottom: 20rpx;
}
@ -629,8 +672,4 @@ const handleNext = async () => {
.modal-btn-cancel::after {
border: none;
}
.modal-btn-cancel:active {
background: #e0e0e0;
}
</style>

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 上下文不可用,忽略即可
}
}
}