Files
spring-festival-greetings/components/LoginPopup/LoginPopup.vue
2026-02-09 23:42:06 +08:00

450 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view>
<uni-popup ref="popupRef" type="bottom" :safe-area="false">
<view class="popup-container">
<view class="drag-handle"></view>
<view class="popup-header">
<text class="popup-title">授权登录</text>
<view class="close-btn" @tap="close">
<uni-icons type="closeempty" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="avatar-section">
<button
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
class="avatar-selector custom-button"
>
<view class="avatar-wrapper">
<image v-if="avatarUrl" :src="avatarUrl" class="avatar-preview" />
<image
v-else
src="/static/images/default-avatar.png"
class="avatar-preview"
/>
<view class="camera-icon">
<uni-icons
type="camera-filled"
size="14"
color="#fff"
></uni-icons>
</view>
</view>
<text class="upload-tip">点击上传头像</text>
</button>
</view>
<view class="form-section">
<view v-if="isSinglePage" class="single-page-tip">
<uni-icons type="info" size="16" color="#ff4d4f"></uni-icons>
<text class="tip-text">当前处于预览模式请前往小程序登录</text>
</view>
<view class="input-group">
<text class="label">昵称</text>
<input
class="nickname-input"
type="nickname"
v-model="nickname"
placeholder="请输入或点击获取昵称"
placeholder-class="placeholder"
/>
<!-- <text class="get-nickname-btn">获取微信昵称</text> -->
</view>
</view>
<view class="action-section">
<button class="confirm-btn custom-button" @tap="confirmLogin">
{{ isSinglePage ? "前往小程序登录" : "一键登录" }}
</button>
<!-- <view class="agreement-row" @tap="toggleAgreement">
<view class="checkbox" :class="{ checked: isAgreed }">
<uni-icons
v-if="isAgreed"
type="checkmarkempty"
size="12"
color="#fff"
></uni-icons>
</view>
<view class="agreement-text">
登录即代表您同意
<text class="link" @tap.stop="openAgreement('user')"
>用户协议</text
>
<text class="link" @tap.stop="openAgreement('privacy')"
>隐私政策</text
>
</view>
</view> -->
</view>
</view>
</uni-popup>
<!-- 隐私协议弹窗 -->
<PrivacyPopup ref="privacyRef" @agree="onPrivacyAgree" />
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { useUserStore } from "@/stores/user";
import { getPlatformProvider, isSinglePageMode } from "@/utils/system";
import { uploadImage } from "@/utils/common";
import { apiLogin } from "@/api/auth.js";
import { wxLogin } from "@/utils/login.js";
import PrivacyPopup from "@/components/PrivacyPopup/PrivacyPopup.vue";
const popupRef = ref(null);
const privacyRef = ref(null);
const avatarUrl = ref("");
const nickname = ref("");
const userStore = useUserStore();
const props = defineProps({
shareToken: {
type: String,
default: "",
},
});
const emit = defineEmits(["logind"]);
// 是否处于单页模式(朋友圈打开)
const isSinglePage = computed(() => isSinglePageMode());
const festivalNames = [
"春意",
"福星",
"小福",
"新禧",
"瑞雪",
"花灯",
"喜乐",
"元宝",
"春芽",
"年年",
"花灯",
"月圆",
"灯影",
"小灯",
"星灯",
"彩灯",
"清风",
"微风",
"小晴",
"碧波",
"流泉",
"月光",
"玉轮",
"桂香",
"秋叶",
"星河",
"小月",
"露华",
"秋水",
"雪落",
"冰晶",
"暖阳",
"小雪",
"冬影",
"雪花",
"松影",
];
const getFestivalName = () => {
const idx = Math.floor(Math.random() * festivalNames.length);
return festivalNames[idx];
};
const open = async () => {
// #ifdef MP-WEIXIN
const isAgreed = await privacyRef.value.check();
if (isAgreed) {
popupRef.value.open();
}
// #endif
// #ifndef MP-WEIXIN
popupRef.value.open();
// #endif
};
const onPrivacyAgree = () => {
popupRef.value.open();
};
const close = () => {
popupRef.value.close();
};
const onChooseAvatar = (e) => {
avatarUrl.value = e.detail.avatarUrl;
};
const confirmLogin = async () => {
// if (!isAgreed.value) {
// uni.showToast({ title: "请先同意用户协议和隐私政策", icon: "none" });
// return;
// }
if (isSinglePage.value) {
uni.showModal({
title: "提示",
content: "请点击屏幕下方的“前往小程序”按钮,进入完整版体验登录功能",
showCancel: false,
confirmText: "我知道了",
});
return;
}
try {
const platform = getPlatformProvider();
if (platform === "mp-weixin") {
const code = await wxLogin();
const imageUrl = avatarUrl.value
? await uploadImage(avatarUrl.value)
: "";
const loginRes = await apiLogin({
code,
nickname: nickname.value || getFestivalName(),
avatarUrl: imageUrl,
platform: "wx",
shareToken: props.shareToken,
});
// 保存用户信息到store
userStore.setUserInfo({
nickName: loginRes?.user?.nickname || nickname.value,
avatarUrl: loginRes?.user?.avatar || imageUrl,
id: loginRes?.user?.id,
isVip: loginRes?.isVip || false,
vipExpireAt: loginRes?.vipExpireAt || null,
});
userStore.setToken(loginRes.token);
uni.showToast({ title: "登录成功", icon: "success" });
emit("logind");
popupRef.value.close();
// 重置临时变量
avatarUrl.value = "";
nickname.value = "";
}
} catch (err) {
uni.showToast({ title: "登录失败", icon: "none" });
console.error(err);
}
};
defineExpose({ open, close });
</script>
<style lang="scss">
.custom-button {
border: none;
outline: none;
background-color: transparent;
padding: 0;
margin: 0;
line-height: normal;
font-family: inherit;
}
.custom-button::after {
border: none;
}
.popup-container {
background-color: #fff;
padding: 20rpx 40rpx 60rpx;
border-top-left-radius: 48rpx;
border-top-right-radius: 48rpx;
position: relative;
.drag-handle {
width: 64rpx;
height: 8rpx;
background: #e5e5e5;
border-radius: 4rpx;
margin: 0 auto 30rpx;
}
.popup-header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 60rpx;
position: relative;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #1a1a1a;
}
.close-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
padding: 10rpx;
}
}
.avatar-section {
display: flex;
justify-content: center;
margin-bottom: 80rpx;
.avatar-selector {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-wrapper {
width: 200rpx;
height: 200rpx;
background: #f5f5f5;
border-radius: 50%;
position: relative;
margin-bottom: 20rpx;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.avatar-preview {
width: 100%;
height: 100%;
border-radius: 50%;
}
.camera-icon {
position: absolute;
right: 10rpx;
bottom: 10rpx;
width: 48rpx;
height: 48rpx;
background: #ff4d4f;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
}
}
.upload-tip {
font-size: 26rpx;
color: #999;
}
}
.form-section {
margin-bottom: 80rpx;
.single-page-tip {
display: flex;
align-items: center;
justify-content: center;
background: #fff1f0;
border: 1rpx solid #ffccc7;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 30rpx;
.tip-text {
font-size: 24rpx;
color: #ff4d4f;
margin-left: 8rpx;
}
}
.input-group {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
.label {
font-size: 30rpx;
color: #1a1a1a;
font-weight: 500;
width: 100rpx;
}
.nickname-input {
flex: 1;
font-size: 30rpx;
color: #1a1a1a;
margin-left: 20rpx;
}
.placeholder {
color: #ccc;
}
.get-nickname-btn {
font-size: 26rpx;
color: #576b95;
font-weight: 500;
}
}
}
.action-section {
.confirm-btn {
background: #ff4d4f;
color: #fff;
font-size: 34rpx;
font-weight: 600;
height: 100rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
box-shadow: 0 12rpx 24rpx rgba(255, 77, 79, 0.3);
&:active {
opacity: 0.8;
}
}
.agreement-row {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 20rpx;
.checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #ccc;
border-radius: 50%;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&.checked {
background: #ff4d4f;
border-color: #ff4d4f;
}
}
.agreement-text {
font-size: 24rpx;
color: #999;
.link {
color: #576b95;
}
}
}
}
}
</style>