|
|
@@ -0,0 +1,508 @@
|
|
|
+<template>
|
|
|
+ <view class="login-container">
|
|
|
+ <!-- 返回按钮 -->
|
|
|
+ <view class="back-button" @click="handleBack">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
+ <text class="back-text">返回</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 背景装饰 -->
|
|
|
+ <view class="bg-decoration">
|
|
|
+ <view class="circle circle-1"></view>
|
|
|
+ <view class="circle circle-2"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Logo和标题 -->
|
|
|
+ <view class="header">
|
|
|
+ <image class="logo" src="/static/images/logo.png" mode="aspectFit"></image>
|
|
|
+ <text class="title">量化选股大师</text>
|
|
|
+ <text class="subtitle">专业的股票量化分析工具</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 登录按钮区域 -->
|
|
|
+ <view class="login-actions">
|
|
|
+ <!-- 老用户:微信一键登录 -->
|
|
|
+ <button
|
|
|
+ v-if="showOneClickLogin"
|
|
|
+ class="login-btn primary-btn"
|
|
|
+ @click="handleWxLogin"
|
|
|
+ >
|
|
|
+ <text class="btn-icon">📱</text>
|
|
|
+ <text class="btn-text">微信一键登录</text>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 新用户:授权手机号登录 -->
|
|
|
+ <button
|
|
|
+ v-else
|
|
|
+ class="login-btn primary-btn"
|
|
|
+ open-type="getPhoneNumber"
|
|
|
+ @getphonenumber="handleGetPhoneNumber"
|
|
|
+ >
|
|
|
+ <text class="btn-icon">🔐</text>
|
|
|
+ <text class="btn-text">授权手机号登录</text>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 用户协议 -->
|
|
|
+ <view class="agreement">
|
|
|
+ <checkbox-group @change="handleAgreementChange">
|
|
|
+ <label class="agreement-label">
|
|
|
+ <checkbox :checked="agreedToTerms" color="#5d55e8" />
|
|
|
+ <text class="agreement-text">
|
|
|
+ 我已阅读并同意
|
|
|
+ <text class="link" @click.stop="showAgreement('user')">《用户协议》</text>
|
|
|
+ 和
|
|
|
+ <text class="link" @click.stop="showAgreement('privacy')">《隐私政策》</text>
|
|
|
+ </text>
|
|
|
+ </label>
|
|
|
+ </checkbox-group>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 用户信息完善弹窗 -->
|
|
|
+ <user-info-popup
|
|
|
+ ref="userInfoPopup"
|
|
|
+ @confirm="handleUserInfoConfirm"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { wxSilentLogin, wxPhoneLogin, wxCompleteUserInfo } from '@/utils/auth.js'
|
|
|
+import UserInfoPopup from '@/components/UserInfoPopup.vue'
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: {
|
|
|
+ UserInfoPopup
|
|
|
+ },
|
|
|
+
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ showOneClickLogin: true, // 是否显示一键登录按钮
|
|
|
+ agreedToTerms: false, // 是否同意协议
|
|
|
+ loginCode: '', // 微信登录code
|
|
|
+ tempUserData: null // 临时存储的用户数据
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 返回上一页
|
|
|
+ */
|
|
|
+ handleBack() {
|
|
|
+ const pages = getCurrentPages()
|
|
|
+ if (pages.length > 1) {
|
|
|
+ // 有上一页,返回
|
|
|
+ uni.navigateBack()
|
|
|
+ } else {
|
|
|
+ // 没有上一页,跳转到首页
|
|
|
+ uni.switchTab({
|
|
|
+ url: '/pages/index/index'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 第一步:微信一键登录(老用户静默登录)
|
|
|
+ */
|
|
|
+ async handleWxLogin() {
|
|
|
+ // 检查是否同意协议
|
|
|
+ if (!this.agreedToTerms) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先阅读并同意用户协议',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '登录中...' })
|
|
|
+
|
|
|
+ // 调用 wx.login 获取 code
|
|
|
+ const loginRes = await this.wxLoginAsync()
|
|
|
+ this.loginCode = loginRes.code
|
|
|
+
|
|
|
+ console.log('[登录] 获取到微信code:', this.loginCode)
|
|
|
+
|
|
|
+ // 调用后端接口检查是否为老用户
|
|
|
+ const result = await wxSilentLogin(this.loginCode)
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ if (result && result.isSign === 'true') {
|
|
|
+ // 老用户,直接登录成功
|
|
|
+ console.log('[登录] 老用户登录成功')
|
|
|
+ this.handleLoginSuccess()
|
|
|
+ } else if (result && result.isSign === 'false') {
|
|
|
+ // 新用户,需要授权手机号
|
|
|
+ console.log('[登录] 新用户,需要授权手机号')
|
|
|
+ this.showOneClickLogin = false
|
|
|
+ } else if (result && result.code === 103) {
|
|
|
+ // 账号被禁用
|
|
|
+ uni.showModal({
|
|
|
+ title: '账号异常',
|
|
|
+ content: '您的账号已被禁用,如有疑问请联系客服',
|
|
|
+ showCancel: false
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 后端返回异常
|
|
|
+ throw new Error('登录接口返回数据异常')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('[登录] 微信登录失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '登录失败,请重试',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 第二步:获取手机号授权(新用户)
|
|
|
+ */
|
|
|
+ async handleGetPhoneNumber(e) {
|
|
|
+ console.log('[登录] 手机号授权回调:', e)
|
|
|
+
|
|
|
+ if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
|
|
+ // 检查是否是权限问题
|
|
|
+ if (e.detail.errMsg.includes('no permission')) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '权限不足',
|
|
|
+ content: '获取手机号功能需要:\n1. 小程序企业认证\n2. 开通"手机号快速验证组件"权限\n3. 在真机上测试',
|
|
|
+ showCancel: false
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '授权失败,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '验证中...' })
|
|
|
+
|
|
|
+ // 重新获取 code(微信要求)
|
|
|
+ const loginRes = await this.wxLoginAsync()
|
|
|
+ const newLoginCode = loginRes.code
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ const params = {
|
|
|
+ loginCode: newLoginCode,
|
|
|
+ phoneCode: e.detail.code,
|
|
|
+ encryptedData: e.detail.encryptedData,
|
|
|
+ iv: e.detail.iv
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[登录] 发送手机号验证请求')
|
|
|
+
|
|
|
+ // 调用后端接口验证手机号
|
|
|
+ const result = await wxPhoneLogin(params)
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ if (result && result.isSign === 'false') {
|
|
|
+ // 需要完善用户信息
|
|
|
+ console.log('[登录] 需要完善用户信息')
|
|
|
+ this.tempUserData = result
|
|
|
+ this.$refs.userInfoPopup.open(result)
|
|
|
+ } else if (result && result.isSign === 'true') {
|
|
|
+ // 已注册,直接登录成功
|
|
|
+ console.log('[登录] 已注册用户,登录成功')
|
|
|
+ this.handleLoginSuccess()
|
|
|
+ } else {
|
|
|
+ // 后端返回异常
|
|
|
+ throw new Error('验证接口返回数据异常')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('[登录] 手机号验证失败:', error)
|
|
|
+
|
|
|
+ // 根据错误信息提供友好提示
|
|
|
+ let errorMsg = '验证失败,请重试'
|
|
|
+
|
|
|
+ if (error.message) {
|
|
|
+ if (error.message.includes('48001') || error.message.includes('未开通手机号快速验证组件')) {
|
|
|
+ // 未开通权限
|
|
|
+ uni.showModal({
|
|
|
+ title: '权限未开通',
|
|
|
+ content: '小程序未开通"手机号快速验证组件"权限\n\n请前往微信公众平台:\n开发 → 开发管理 → 接口设置\n开通"手机号快速验证组件"',
|
|
|
+ showCancel: false
|
|
|
+ })
|
|
|
+ return
|
|
|
+ } else if (error.message.includes('40029') || error.message.includes('code无效')) {
|
|
|
+ errorMsg = 'phoneCode已失效,请重新授权'
|
|
|
+ } else if (error.message.includes('40001') || error.message.includes('access_token')) {
|
|
|
+ errorMsg = 'access_token无效,请重试'
|
|
|
+ } else if (error.message.includes('45011')) {
|
|
|
+ errorMsg = '操作过于频繁,请稍后重试'
|
|
|
+ } else {
|
|
|
+ errorMsg = error.message
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: errorMsg,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 第三步:完善用户信息(首次登录)
|
|
|
+ */
|
|
|
+ async handleUserInfoConfirm(userInfo) {
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '注册中...' })
|
|
|
+
|
|
|
+ // 构建完整的用户信息
|
|
|
+ const completeInfo = {
|
|
|
+ openid: this.tempUserData.openid,
|
|
|
+ unionid: this.tempUserData.unionid,
|
|
|
+ phoneNumber: this.tempUserData.phoneNumber,
|
|
|
+ nickname: userInfo.nickname,
|
|
|
+ avatarUrl: userInfo.avatarUrl
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[登录] 提交完整用户信息')
|
|
|
+
|
|
|
+ // 调用后端接口完成注册
|
|
|
+ await wxCompleteUserInfo(completeInfo)
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ console.log('[登录] 注册成功')
|
|
|
+ this.handleLoginSuccess()
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('[登录] 注册失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '注册失败,请重试',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 登录成功后的处理
|
|
|
+ */
|
|
|
+ handleLoginSuccess() {
|
|
|
+ uni.showToast({
|
|
|
+ title: '登录成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+
|
|
|
+ // 延迟跳转,让用户看到成功提示
|
|
|
+ setTimeout(() => {
|
|
|
+ // 跳转到首页或返回上一页
|
|
|
+ const pages = getCurrentPages()
|
|
|
+ if (pages.length > 1) {
|
|
|
+ uni.navigateBack()
|
|
|
+ } else {
|
|
|
+ uni.switchTab({
|
|
|
+ url: '/pages/index/index'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, 1500)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 协议勾选变化
|
|
|
+ */
|
|
|
+ handleAgreementChange(e) {
|
|
|
+ this.agreedToTerms = e.detail.value.length > 0
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 显示协议内容
|
|
|
+ */
|
|
|
+ showAgreement(type) {
|
|
|
+ const title = type === 'user' ? '用户协议' : '隐私政策'
|
|
|
+ uni.showModal({
|
|
|
+ title: title,
|
|
|
+ content: '这里显示协议内容...',
|
|
|
+ showCancel: false
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 封装 wx.login 为 Promise
|
|
|
+ */
|
|
|
+ wxLoginAsync() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.login({
|
|
|
+ provider: 'weixin',
|
|
|
+ success: (res) => {
|
|
|
+ resolve(res)
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 返回按钮 */
|
|
|
+.back-button {
|
|
|
+ position: absolute;
|
|
|
+ top: 40rpx;
|
|
|
+ left: 30rpx;
|
|
|
+ z-index: 10;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border-radius: 50rpx;
|
|
|
+ backdrop-filter: blur(10rpx);
|
|
|
+}
|
|
|
+
|
|
|
+.back-icon {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #fff;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.back-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.login-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40rpx;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 背景装饰 */
|
|
|
+.bg-decoration {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ z-index: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.circle {
|
|
|
+ position: absolute;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.circle-1 {
|
|
|
+ width: 400rpx;
|
|
|
+ height: 400rpx;
|
|
|
+ top: -100rpx;
|
|
|
+ right: -100rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.circle-2 {
|
|
|
+ width: 300rpx;
|
|
|
+ height: 300rpx;
|
|
|
+ bottom: -50rpx;
|
|
|
+ left: -50rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 头部 */
|
|
|
+.header {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 120rpx;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.logo {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ font-size: 48rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #fff;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.subtitle {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+/* 登录按钮区域 */
|
|
|
+.login-actions {
|
|
|
+ width: 100%;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.login-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 96rpx;
|
|
|
+ border-radius: 48rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.primary-btn {
|
|
|
+ background: #fff;
|
|
|
+ color: #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-icon {
|
|
|
+ font-size: 40rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户协议 */
|
|
|
+.agreement {
|
|
|
+ margin-top: 60rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ line-height: 1.6;
|
|
|
+ margin-left: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.link {
|
|
|
+ color: #fff;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+</style>
|