|
@@ -2,142 +2,84 @@
|
|
|
<CustomNav title="登录" leftType="home" />
|
|
<CustomNav title="登录" leftType="home" />
|
|
|
<view class="login-container">
|
|
<view class="login-container">
|
|
|
<view class="spacer top" />
|
|
<view class="spacer top" />
|
|
|
|
|
+
|
|
|
<view class="login-card">
|
|
<view class="login-card">
|
|
|
<view class="title-section">
|
|
<view class="title-section">
|
|
|
<text class="page-title">欢迎使用慢病管理APP</text>
|
|
<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>
|
|
|
|
|
|
|
+ <text class="subtitle">请选择您的身份进行登录</text>
|
|
|
|
|
+ </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>
|
|
|
|
|
|
|
+ <view class="role-section">
|
|
|
|
|
+ <button class="role-btn" @click="onSelectRole(2)" :disabled="isLogging">患者</button>
|
|
|
|
|
+ <button class="role-btn" @click="onSelectRole(3)" :disabled="isLogging">患者家属</button>
|
|
|
|
|
+ <button class="role-btn" @click="onSelectRole(1)" :disabled="isLogging">医生</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
|
|
|
- <button form-type="submit" class="login-btn">提交</button>
|
|
|
|
|
- </form>
|
|
|
|
|
|
|
+ <view class="info-text">点击身份后会使用微信登录并向后端发送登录请求。控制台将打印后端返回的 token。</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
<view class="spacer bottom" />
|
|
<view class="spacer bottom" />
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref } from 'vue'
|
|
import { ref } from 'vue'
|
|
|
-import { onMounted } from 'vue'
|
|
|
|
|
-import PageTitle from '@/components/PageTitle.vue'
|
|
|
|
|
import CustomNav from '@/components/CustomNav.vue'
|
|
import CustomNav from '@/components/CustomNav.vue'
|
|
|
|
|
|
|
|
-// 页面标题,用于导航栏显示
|
|
|
|
|
-const title = ref('登录')
|
|
|
|
|
|
|
+const isLogging = ref(false)
|
|
|
|
|
|
|
|
-// 默认头像URL,用于未选择头像时的显示
|
|
|
|
|
-const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
|
|
|
|
-const avatarUrl = ref(defaultAvatarUrl)
|
|
|
|
|
-// 防止重复触发 chooseAvatar 的并发锁,避免多次调用导致错误
|
|
|
|
|
-const isChoosing = ref(false)
|
|
|
|
|
-const nickname = ref('')
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * role: 1=SYS_ADMIN(医生), 2=PATIENT(患者), 3=PATIENT_FAMILY(患者家属)
|
|
|
|
|
+ */
|
|
|
|
|
+async function onSelectRole(role: number) {
|
|
|
|
|
+ if (isLogging.value) return
|
|
|
|
|
+ isLogging.value = true
|
|
|
|
|
+ console.log('Selected role:', role)
|
|
|
|
|
|
|
|
-// 处理chooseAvatar事件回调(微信小程序)
|
|
|
|
|
-// 此函数在用户选择头像后被调用,用于更新头像URL和可能的昵称
|
|
|
|
|
-function onChooseAvatar(e: any) {
|
|
|
|
|
- console.log('onChooseAvatar called with event:', e) // 调试输出:记录事件对象
|
|
|
|
|
- // 在微信小程序中,e.detail 可能包含 avatarUrl(字符串)或更复杂结构
|
|
|
|
|
try {
|
|
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) // 调试输出:昵称更新
|
|
|
|
|
|
|
+ uni.showLoading({ title: '登录中...' })
|
|
|
|
|
+ const loginRes = await new Promise<any>((resolve, reject) => {
|
|
|
|
|
+ uni.login({
|
|
|
|
|
+ provider: 'weixin',
|
|
|
|
|
+ success: resolve,
|
|
|
|
|
+ fail: reject
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ console.log('uni.login success:', loginRes)
|
|
|
|
|
+ const code = loginRes.code
|
|
|
|
|
+ const response = await uni.request({
|
|
|
|
|
+ url: 'http://127.0.0.1:8080/get_openid',
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ header: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ data: { code: code, role: role }
|
|
|
|
|
+ })
|
|
|
|
|
+ console.log('Backend response:', response)
|
|
|
|
|
+ // 兼容后端返回格式,提取 token 并打印
|
|
|
|
|
+ const resp = response.data as any
|
|
|
|
|
+ let token: string | undefined
|
|
|
|
|
+ if (resp && resp.code === 200 && resp.data && resp.data.token) {
|
|
|
|
|
+ token = resp.data.token
|
|
|
|
|
+ } else if (resp && resp.token) {
|
|
|
|
|
+ token = resp.token
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error('Unexpected backend response shape')
|
|
|
}
|
|
}
|
|
|
|
|
+ console.log('Token from backend:', token)
|
|
|
|
|
+ // 保存 token 和 role 到本地存储
|
|
|
|
|
+ uni.setStorageSync('token', token)
|
|
|
|
|
+ uni.setStorageSync('role', role)
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '登录成功', icon: 'success' })
|
|
|
|
|
+ // 跳转到首页
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ uni.switchTab({ url: '/pages/index/index' })
|
|
|
|
|
+ }, 1500)
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error('Error in onChooseAvatar:', err) // 调试输出:捕获错误
|
|
|
|
|
- // ignore
|
|
|
|
|
|
|
+ console.error('Login error:', err)
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '登录失败', icon: 'error' })
|
|
|
} finally {
|
|
} 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 时进行安全检测,若不通过会清空输入,这里仅作默认处理
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 处理表单提交事件
|
|
|
|
|
-function onSubmit() {
|
|
|
|
|
- console.log('onSubmit called with avatarUrl:', avatarUrl.value, 'nickname:', nickname.value) // 调试输出:提交数据
|
|
|
|
|
- // 保存用户信息到本地存储(可以改为调用后端或 uni.login + 后端换取 session)
|
|
|
|
|
- const user = {
|
|
|
|
|
- avatar: avatarUrl.value,
|
|
|
|
|
- nickname: nickname.value || '微信用户'
|
|
|
|
|
- }
|
|
|
|
|
- console.log('User object to save:', user) // 调试输出:用户对象
|
|
|
|
|
- uni.setStorageSync('user_info', user)
|
|
|
|
|
-
|
|
|
|
|
- // 如果是在小程序端,使用 navigateBack 或 switchTab 返回个人中心
|
|
|
|
|
- // 直接尝试使用 switchTab 返回个人中心(若失败则 navigateTo)
|
|
|
|
|
- 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' })
|
|
|
|
|
|
|
+ isLogging.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
@@ -145,12 +87,9 @@ function onSubmit() {
|
|
|
<style>
|
|
<style>
|
|
|
.login-container {
|
|
.login-container {
|
|
|
min-height: 100vh;
|
|
min-height: 100vh;
|
|
|
- /* 为固定在顶部的 CustomNav 留出空间(状态栏 + 导航栏 44px) */
|
|
|
|
|
padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
|
|
padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
|
|
|
- /* 保留侧边与内部间距,使用 border-box 避免计算误差 */
|
|
|
|
|
padding-right: 40rpx;
|
|
padding-right: 40rpx;
|
|
|
padding-left: 40rpx;
|
|
padding-left: 40rpx;
|
|
|
- /* 底部安全区:使用项目中声明的 --window-bottom 或 fallback */
|
|
|
|
|
padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
|
|
padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
@@ -159,136 +98,39 @@ function onSubmit() {
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-/* 让卡片在可用空间内垂直居中(当内容较短时) */
|
|
|
|
|
-.spacer {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.spacer.top {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.spacer.bottom {
|
|
|
|
|
- flex: 2;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
|
|
+.spacer { width: 100%; }
|
|
|
|
|
+.spacer.top { flex: 1 }
|
|
|
|
|
+.spacer.bottom { flex: 2 }
|
|
|
.login-card {
|
|
.login-card {
|
|
|
- /* remove auto margins; vertical spacing controlled by spacers */
|
|
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 20rpx;
|
|
border-radius: 20rpx;
|
|
|
- box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
+ box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.1);
|
|
|
padding: 60rpx 30rpx 120rpx 30rpx;
|
|
padding: 60rpx 30rpx 120rpx 30rpx;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
max-width: 600rpx;
|
|
max-width: 600rpx;
|
|
|
- display: flex;
|
|
|
|
|
|
|
+ display:flex;
|
|
|
flex-direction: column;
|
|
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);
|
|
|
|
|
|
|
+ align-items: stretch; /* 让内部元素(如按钮)可以宽度撑满 */
|
|
|
|
|
+}
|
|
|
|
|
+.title-section { text-align:center; margin-bottom: 40rpx }
|
|
|
|
|
+.page-title { display:block; white-space: normal; font-size:48rpx; font-weight:bold; color:#333; margin-bottom:10rpx; line-height:1.2 }
|
|
|
|
|
+.subtitle { display:block; white-space: normal; font-size:28rpx; color:#666; margin-bottom:6rpx }
|
|
|
|
|
+.role-section { display:flex; flex-direction: column; gap:20rpx; margin-top:20rpx; width:100% }
|
|
|
|
|
+.role-btn {
|
|
|
|
|
+ display:inline-flex;
|
|
|
|
|
+ justify-content:center;
|
|
|
|
|
+ align-items:center;
|
|
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
|
|
+ background: linear-gradient(135deg,#07C160 0%, #00A854 100%);
|
|
|
|
|
+ color:#fff;
|
|
|
|
|
+ border-radius:12rpx;
|
|
|
|
|
+ font-size:32rpx;
|
|
|
|
|
+ border:none;
|
|
|
|
|
+ width:100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.login-btn:active {
|
|
|
|
|
- background: linear-gradient(135deg, #00A854 0%, #07C160 100%);
|
|
|
|
|
- transform: scale(0.98);
|
|
|
|
|
|
|
+.role-btn:disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
}
|
|
}
|
|
|
|
|
+.info-text { margin-top:20rpx; font-size:24rpx; color:#666; text-align:center; white-space: normal; word-break: break-word }
|
|
|
</style>
|
|
</style>
|