feat:修改app注册,登录页面和流程
@ -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();
|
||||
|
||||
149
frontend/components/LoginBackground.vue
Normal 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>
|
||||
144
frontend/components/TopfansWelcome.vue
Normal 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>
|
||||
@ -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": {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
553
frontend/pages/login/portal.vue
Normal 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";
|
||||
|
||||
// 当前激活的 tab:register=手机号注册, 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>
|
||||
613
frontend/pages/login/quickLogin.vue
Normal 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>
|
||||
@ -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">支持JPG、PNG格式,大小不超过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>
|
||||
|
||||
@ -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=true:quickLogin / 首次注册时由上游页面写入
|
||||
// (持久化,只在 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 跳转 url:route + 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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
BIN
frontend/static/login/portal/bg.png
Normal file
|
After Width: | Height: | Size: 548 KiB |
BIN
frontend/static/login/portal/character1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/login/portal/character2.png
Normal file
|
After Width: | Height: | Size: 864 KiB |
BIN
frontend/static/login/portal/monster-large.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
frontend/static/login/portal/quicklogin-avatar.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
frontend/static/login/portal/register-avatar.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/static/login/portal/register-tab-bg.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
frontend/static/login/portal/selectrole-photo1.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
frontend/static/login/portal/selectrole-photo2.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
frontend/static/login/portal/setnickname-avatar-box.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
frontend/static/login/portal/setnickname-edit-icon.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
frontend/static/login/portal/topfans-logo.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
frontend/static/login/portal/welcome-bg.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
@ -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 上下文不可用,忽略即可
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||