|
@@ -1,333 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <CustomNav title="登录" leftType="home" />
|
|
|
|
|
- <view class="login-container">
|
|
|
|
|
- <view class="spacer top" />
|
|
|
|
|
- <view class="login-card">
|
|
|
|
|
- <view class="title-section">
|
|
|
|
|
- <text class="page-title">欢迎使用慢病管理APP</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <view class="avatar-area">
|
|
|
|
|
- <text class="section-label">请点击下方选择您的头像</text>
|
|
|
|
|
- <!-- only available in Weixin Mini Program: chooseAvatar open-type -->
|
|
|
|
|
- <!-- 使用 tap 事件在打开前设置锁,防止重复调用导致 chooseAvatar:fail 错误 -->
|
|
|
|
|
- <button
|
|
|
|
|
- class="avatar-wrapper"
|
|
|
|
|
- :class="{ disabled: isChoosing }"
|
|
|
|
|
- :disabled="isChoosing"
|
|
|
|
|
- open-type="chooseAvatar"
|
|
|
|
|
- @tap="startChooseAvatar"
|
|
|
|
|
- @chooseavatar="onChooseAvatar"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="avatar-frame">
|
|
|
|
|
- <image class="avatar-img" :src="avatarUrl" mode="aspectFill" />
|
|
|
|
|
- </view>
|
|
|
|
|
- </button>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <form @submit.prevent="onSubmit">
|
|
|
|
|
- <view class="input-section">
|
|
|
|
|
- <text class="section-label">请输入您的名字</text>
|
|
|
|
|
- <input class="nickname-input" type="nickname" placeholder="请点击此处进行输入" v-model="nickname" @blur="onNicknameBlur" />
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <button form-type="submit" class="login-btn">授权登录</button>
|
|
|
|
|
- </form>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="spacer bottom" />
|
|
|
|
|
- </view>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup lang="ts">
|
|
|
|
|
-import { ref } from 'vue'
|
|
|
|
|
-import { onMounted } from 'vue'
|
|
|
|
|
-import PageTitle from '@/components/PageTitle.vue'
|
|
|
|
|
-import CustomNav from '@/components/CustomNav.vue'
|
|
|
|
|
-
|
|
|
|
|
-// 页面标题,用于导航栏显示
|
|
|
|
|
-const title = ref('登录')
|
|
|
|
|
-
|
|
|
|
|
-// 默认头像URL,用于未选择头像时的显示
|
|
|
|
|
-const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
|
|
|
|
-const avatarUrl = ref(defaultAvatarUrl)
|
|
|
|
|
-// 防止重复触发 chooseAvatar 的并发锁,避免多次调用导致错误
|
|
|
|
|
-const isChoosing = ref(false)
|
|
|
|
|
-const nickname = ref('')
|
|
|
|
|
-
|
|
|
|
|
-// 处理chooseAvatar事件回调(微信小程序)
|
|
|
|
|
-// 此函数在用户选择头像后被调用,用于更新头像URL和可能的昵称
|
|
|
|
|
-function onChooseAvatar(e: any) {
|
|
|
|
|
- console.log('onChooseAvatar called with event:', e) // 调试输出:记录事件对象
|
|
|
|
|
- // 在微信小程序中,e.detail 可能包含 avatarUrl(字符串)或更复杂结构
|
|
|
|
|
- try {
|
|
|
|
|
- const detail = e?.detail
|
|
|
|
|
- console.log('Event detail:', detail) // 调试输出:检查detail内容
|
|
|
|
|
- let url = ''
|
|
|
|
|
- if (!detail) {
|
|
|
|
|
- // 兼容某些平台直接传回字符串
|
|
|
|
|
- url = typeof e === 'string' ? e : ''
|
|
|
|
|
- console.log('No detail, using event as string:', url) // 调试输出:无detail时的处理
|
|
|
|
|
- } else if (typeof detail === 'string') {
|
|
|
|
|
- url = detail
|
|
|
|
|
- console.log('Detail is string:', url) // 调试输出:detail为字符串
|
|
|
|
|
- } else if (detail.avatarUrl) {
|
|
|
|
|
- url = detail.avatarUrl
|
|
|
|
|
- console.log('Using detail.avatarUrl:', url) // 调试输出:使用avatarUrl字段
|
|
|
|
|
- } else if (Array.isArray(detail) && detail[0]) {
|
|
|
|
|
- url = detail[0]
|
|
|
|
|
- console.log('Using first element of array:', url) // 调试输出:数组第一个元素
|
|
|
|
|
- }
|
|
|
|
|
- if (url) {
|
|
|
|
|
- avatarUrl.value = url
|
|
|
|
|
- console.log('Avatar URL updated to:', avatarUrl.value) // 调试输出:头像URL更新
|
|
|
|
|
- }
|
|
|
|
|
- // 如果 detail 中包含昵称(某些平台可能提供),尝试填充
|
|
|
|
|
- if (detail && detail.nickName && !nickname.value) {
|
|
|
|
|
- nickname.value = detail.nickName
|
|
|
|
|
- console.log('Nickname updated from detail:', nickname.value) // 调试输出:昵称更新
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Error in onChooseAvatar:', err) // 调试输出:捕获错误
|
|
|
|
|
- // ignore
|
|
|
|
|
- } finally {
|
|
|
|
|
- // 无论成功或失败,都在回调后清理并发锁
|
|
|
|
|
- isChoosing.value = false
|
|
|
|
|
- console.log('isChoosing reset to false') // 调试输出:重置锁
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 在点击(tap)打开 chooseAvatar 之前设置锁,防止重复打开
|
|
|
|
|
-function startChooseAvatar() {
|
|
|
|
|
- console.log('startChooseAvatar called, current isChoosing:', isChoosing.value) // 调试输出:检查锁状态
|
|
|
|
|
- if (isChoosing.value) {
|
|
|
|
|
- // 如果已经在打开中,直接忽略后续点击
|
|
|
|
|
- console.log('Already choosing, ignoring') // 调试输出:忽略重复点击
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- isChoosing.value = true
|
|
|
|
|
- console.log('isChoosing set to true') // 调试输出:设置锁
|
|
|
|
|
- // 为了避免因回调丢失导致一直锁住,设置一个安全超时(3s)作为兜底
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- isChoosing.value = false
|
|
|
|
|
- console.log('Timeout: isChoosing reset to false') // 调试输出:超时重置锁
|
|
|
|
|
- }, 3000)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 处理昵称输入框失去焦点事件
|
|
|
|
|
-function onNicknameBlur() {
|
|
|
|
|
- console.log('onNicknameBlur called, current nickname:', nickname.value) // 调试输出:昵称输入
|
|
|
|
|
- // 微信会在 onBlur 时进行安全检测,若不通过会清空输入,这里仅作默认处理
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 处理表单提交事件
|
|
|
|
|
-async function onSubmit() {
|
|
|
|
|
- console.log('onSubmit called with avatarUrl:', avatarUrl.value, 'nickname:', nickname.value) // 调试输出:提交数据
|
|
|
|
|
-
|
|
|
|
|
- // 调用 uni.login 获取微信登录凭证
|
|
|
|
|
- uni.login({
|
|
|
|
|
- provider: 'weixin', // 指定微信登录
|
|
|
|
|
- success: async (res) => {
|
|
|
|
|
- console.log('uni.login success:', res) // 调试输出:登录成功
|
|
|
|
|
- const code = res.code // 获取 code
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- // 调用后端接口获取 openid(适配新版响应格式)
|
|
|
|
|
- const response = await uni.request({
|
|
|
|
|
- url: 'http://127.0.0.1:8080/get_openid',
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- data: { code: code },
|
|
|
|
|
- header: { 'Content-Type': 'application/json' }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- console.log('Backend response:', response) // 调试输出:后端响应
|
|
|
|
|
-
|
|
|
|
|
- // 适配新版后端格式 { code, message, data: { openid } }
|
|
|
|
|
- const resp = response.data as any
|
|
|
|
|
- if (resp && resp.code === 200 && resp.data && resp.data.openid) {
|
|
|
|
|
- // 获取到 openid,保存用户信息
|
|
|
|
|
- const user = {
|
|
|
|
|
- avatar: avatarUrl.value,
|
|
|
|
|
- nickname: nickname.value || '微信用户',
|
|
|
|
|
- code: code,
|
|
|
|
|
- openid: resp.data.openid
|
|
|
|
|
- }
|
|
|
|
|
- console.log('User object to save:', user) // 调试输出:用户对象
|
|
|
|
|
- uni.setStorageSync('user_info', user)
|
|
|
|
|
-
|
|
|
|
|
- // 导航到个人中心
|
|
|
|
|
- try {
|
|
|
|
|
- console.log('Attempting switchTab to profile') // 调试输出:尝试切换标签页
|
|
|
|
|
- uni.switchTab({ url: '/pages/profile/profile' })
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.log('switchTab failed, using navigateTo:', e) // 调试输出:切换失败,使用导航
|
|
|
|
|
- uni.navigateTo({ url: '/pages/profile/profile' })
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 处理后端错误
|
|
|
|
|
- const errorMsg = resp?.message || '获取openid失败'
|
|
|
|
|
- console.error('Backend error:', errorMsg) // 调试输出:后端错误
|
|
|
|
|
- uni.showToast({ title: errorMsg, icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- console.error('Request failed:', err) // 调试输出:请求失败
|
|
|
|
|
- uni.showToast({ title: '网络请求失败,请重试', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- fail: (err) => {
|
|
|
|
|
- console.error('uni.login failed:', err) // 调试输出:登录失败
|
|
|
|
|
- uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style>
|
|
|
|
|
-.login-container {
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- /* 为固定在顶部的 CustomNav 留出空间(状态栏 + 导航栏 44px) */
|
|
|
|
|
- padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
|
|
|
|
|
- /* 保留侧边与内部间距,使用 border-box 避免计算误差 */
|
|
|
|
|
- padding-right: 40rpx;
|
|
|
|
|
- padding-left: 40rpx;
|
|
|
|
|
- /* 底部安全区:使用项目中声明的 --window-bottom 或 fallback */
|
|
|
|
|
- padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
|
|
|
|
|
- box-sizing: border-box;
|
|
|
|
|
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 让卡片在可用空间内垂直居中(当内容较短时) */
|
|
|
|
|
-.spacer {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.spacer.top {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.spacer.bottom {
|
|
|
|
|
- flex: 2;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.login-card {
|
|
|
|
|
- /* remove auto margins; vertical spacing controlled by spacers */
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 20rpx;
|
|
|
|
|
- box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
- padding: 60rpx 30rpx 120rpx 30rpx;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- max-width: 600rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.title-section {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-bottom: 60rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.page-title {
|
|
|
|
|
- font-size: 48rpx;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
- display: block;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.subtitle {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #666;
|
|
|
|
|
- display: block;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.section-label {
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
- display: block;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.avatar-area {
|
|
|
|
|
- margin-bottom: 60rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.avatar-wrapper {
|
|
|
|
|
- width: 200rpx;
|
|
|
|
|
- height: 200rpx;
|
|
|
|
|
- border-radius: 200rpx;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- border: 4rpx solid #07C160;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.avatar-frame {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- border-radius: 100%;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.avatar-img {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- object-fit: cover;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.avatar-hint {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #666;
|
|
|
|
|
- margin-top: 20rpx;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-section {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- margin-bottom: 60rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.nickname-input {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- height: 100rpx;
|
|
|
|
|
- border: 2rpx solid #ddd;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- background: #f9f9f9;
|
|
|
|
|
- color: #333;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.login-btn {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100rpx;
|
|
|
|
|
- background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- font-size: 36rpx;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- line-height: 100rpx;
|
|
|
|
|
- border: none;
|
|
|
|
|
- box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.login-btn:active {
|
|
|
|
|
- background: linear-gradient(135deg, #00A854 0%, #07C160 100%);
|
|
|
|
|
- transform: scale(0.98);
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|