Ver Fonte

fix: 修复小程序登录流程和用户编辑页跳转

- 修复user-edit.vue跳转逻辑,根据角色进入对应首页
- 删除不存在的/pages/index/index路由
- 修复login.vue中isNewUser判断逻辑
- 更新api.js添加新的API端点
- 更新pages.json和App.vue配置
User há 5 meses atrás
pai
commit
01065a1031

+ 70 - 0
zxyj-frontend/App.vue

@@ -6,6 +6,9 @@ export default {
     const token = uni.getStorageSync('token')
     if (token) {
       this.$store.commit('setToken', token)
+    } else {
+      // 无token时尝试自动登录
+      this.tryAutoLogin()
     }
   },
   onShow: function() {
@@ -13,6 +16,73 @@ export default {
   },
   onHide: function() {
     console.log('App Hide')
+  },
+  methods: {
+    async tryAutoLogin() {
+      try {
+        // 1. 先尝试用缓存的openid自动登录
+        const cachedOpenid = uni.getStorageSync('openid')
+        if (cachedOpenid) {
+          const res = await this.request('/api/auth/auto-login', 'POST', { openid: cachedOpenid })
+          if (res && res.data) {
+            this.saveLoginInfo(res.data)
+            console.log('openid自动登录成功')
+            return
+          }
+        }
+
+        // 2. openid自动登录失败,尝试微信静默登录
+        const loginRes = await new Promise((resolve, reject) => {
+          uni.login({
+            provider: 'weixin',
+            success: resolve,
+            fail: reject
+          })
+        })
+
+        if (loginRes.code) {
+          const res = await this.request('/api/auth/silent-login', 'POST', { code: loginRes.code })
+          if (res && res.data) {
+            this.saveLoginInfo(res.data)
+            console.log('微信静默登录成功')
+          }
+        }
+      } catch (e) {
+        console.log('自动登录失败,需要手动登录', e)
+      }
+    },
+    saveLoginInfo(data) {
+      uni.setStorageSync('token', data.token)
+      uni.setStorageSync('userId', data.userId)
+      uni.setStorageSync('role', data.role)
+      uni.setStorageSync('familyId', data.familyId)
+      if (data.openid) {
+        uni.setStorageSync('openid', data.openid)
+      }
+      uni.setStorageSync('userInfo', {
+        nickname: data.nickname,
+        userId: data.userId
+      })
+      this.$store.commit('setToken', data.token)
+    },
+    request(url, method, data) {
+      return new Promise((resolve, reject) => {
+        uni.request({
+          url: 'http://localhost:8080' + url,
+          method: method,
+          data: data,
+          header: { 'Content-Type': 'application/json' },
+          success: (res) => {
+            if (res.data.code === 200) {
+              resolve(res.data)
+            } else {
+              reject(res.data)
+            }
+          },
+          fail: reject
+        })
+      })
+    }
   }
 }
 </script>

+ 38 - 8
zxyj-frontend/pages.json

@@ -25,12 +25,6 @@
         "navigationBarTitleText": "首页"
       }
     },
-    {
-      "path": "pages/index/index",
-      "style": {
-        "navigationBarTitleText": "心知家庭"
-      }
-    },
     {
       "path": "pages/tasks/tasks",
       "style": {
@@ -48,11 +42,47 @@
       "style": {
         "navigationBarTitleText": "我的"
       }
+    },
+    {
+      "path": "pages/teacher/index",
+      "style": {
+        "navigationBarTitleText": "指导师中心"
+      }
+    },
+    {
+      "path": "pages/membership/index",
+      "style": {
+        "navigationBarTitleText": "会员服务"
+      }
+    },
+    {
+      "path": "pages/profile/create-child",
+      "style": {
+        "navigationBarTitleText": "添加孩子"
+      }
+    },
+    {
+      "path": "pages/profile/children",
+      "style": {
+        "navigationBarTitleText": "孩子管理"
+      }
+    },
+    {
+      "path": "pages/tasks/create-task",
+      "style": {
+        "navigationBarTitleText": "添加任务"
+      }
+    },
+    {
+      "path": "pages/tasks/review",
+      "style": {
+        "navigationBarTitleText": "审核任务"
+      }
     }
   ],
   "globalStyle": {
     "navigationBarTextStyle": "black",
-    "navigationBarTitleText": "心知家庭",
+    "navigationBarTitleText": "知行益家",
     "navigationBarBackgroundColor": "#FFFFFF",
     "backgroundColor": "#F8F8F8"
   },
@@ -63,7 +93,7 @@
     "backgroundColor": "#FFFFFF",
     "list": [
       {
-        "pagePath": "pages/index/index",
+        "pagePath": "pages/index/parent-index",
         "text": "首页"
       },
       {

+ 74 - 98
zxyj-frontend/pages/login/login.vue

@@ -8,44 +8,6 @@
 
     <!-- 手机号登录 -->
     <view class="login-form">
-      <!-- 角色选择(新用户显示) -->
-      <view class="form-item" v-if="showRoleSelect">
-        <text class="label">我是</text>
-        <radio-group class="role-select" @change="onRoleChange">
-          <label class="role-option">
-            <radio value="parent" :checked="role === 'parent'" />
-            <text>家长</text>
-          </label>
-          <label class="role-option">
-            <radio value="child" :checked="role === 'child'" />
-            <text>孩子</text>
-          </label>
-        </radio-group>
-      </view>
-
-      <!-- 生日选择(新用户显示) -->
-      <view class="form-item" v-if="showRoleSelect">
-        <text class="label">生日</text>
-        <picker mode="date" :value="birthday" @change="onBirthdayChange">
-          <view class="picker-input">{{ birthday || '请选择生日' }}</view>
-        </picker>
-      </view>
-
-      <!-- 性别选择(新用户显示) -->
-      <view class="form-item" v-if="showRoleSelect">
-        <text class="label">性别</text>
-        <radio-group class="role-select" @change="onGenderChange">
-          <label class="role-option">
-            <radio value="male" :checked="gender === 'male'" />
-            <text>男</text>
-          </label>
-          <label class="role-option">
-            <radio value="female" :checked="gender === 'female'" />
-            <text>女</text>
-          </label>
-        </radio-group>
-      </view>
-
       <view class="form-item">
         <text class="label">手机号</text>
         <input 
@@ -259,66 +221,80 @@ export default {
 			this.loading = false
 		}
     },
-    async handleWechatPhoneLogin(e) {
-      if (e.detail.errMsg === 'getPhoneNumber:ok') {
-        if (!this.agreed) {
-          uni.showToast({ title: '请先同意用户协议', icon: 'none' })
-          return
-        }
-
-        this.loading = true
-        try {
-          // 获取code用于获取手机号
-          const loginRes = await new Promise((resolve, reject) => {
-            uni.login({
-              provider: 'weixin',
-              success: resolve,
-              fail: reject
-            })
-          })
-
-          const res = await wechatPhoneLogin({
-            code: loginRes.code,
-            encryptedData: e.detail.encryptedData,
-            iv: e.detail.iv,
-            nickname: this.nickname,
-            avatar: this.avatar
-          })
-
-          // 保存token和用户信息
-          uni.setStorageSync('token', res.data.token)
-          uni.setStorageSync('userId', res.data.userId)
-          uni.setStorageSync('role', res.data.role)
-          uni.setStorageSync('familyId', res.data.familyId)
-          uni.setStorageSync('userInfo', {
-            nickname: res.data.nickname,
-            userId: res.data.userId
-          })
-
-          uni.showToast({ title: '登录成功', icon: 'success' })
-
-          // 保存用户角色
-          const role = res.data.role || 'parent'
-
-          // 根据 isNewUser 决定跳转页面
-          setTimeout(() => {
-            if (res.data.isNewUser) {
-              // 新用户:跳转到信息编辑页面
-              uni.redirectTo({
-                url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}`
-              })
-            } else {
-              // 老用户:根据角色进入对应首页
-              this.navigateToHome(role)
-            }
-          }, 1000)
-        } catch (e) {
-          uni.showToast({ title: e.message || '登录失败', icon: 'none' })
-        } finally {
-          this.loading = false
-        }
-      }
-    },
+	async handleWechatPhoneLogin(e) {
+			if (e.detail.errMsg === 'getPhoneNumber:ok') {
+				if (!this.agreed) {
+					uni.showToast({ title: '请先同意用户协议', icon: 'none' })
+					return
+				}
+
+				this.loading = true
+				try {
+					// 新版微信小程序获取手机号:使用e.detail.code
+					const phoneCode = e.detail.code
+					if (!phoneCode) {
+						uni.showToast({ title: '获取手机号失败,请重试', icon: 'none' })
+						return
+					}
+
+					// 获取登录code用于获取openid
+					const loginRes = await new Promise((resolve, reject) => {
+						uni.login({
+							provider: 'weixin',
+							success: resolve,
+							fail: reject
+						})
+					})
+
+					// 调用后端微信手机号登录接口
+					const res = await wechatPhoneLogin({
+						code: loginRes.code,        // 用于获取openid
+						phoneCode: phoneCode,       // 用于获取手机号
+						nickname: this.nickname || '用户',
+						avatar: this.avatar || '',
+						role: this.role,
+						birthday: this.birthday,
+						gender: this.gender
+					})
+
+					// 保存token和用户信息
+					uni.setStorageSync('token', res.data.token)
+					uni.setStorageSync('userId', res.data.userId)
+					uni.setStorageSync('role', res.data.role)
+					uni.setStorageSync('familyId', res.data.familyId)
+					// 保存openid,用于后续自动登录
+					if (res.data.openid) {
+						uni.setStorageSync('openid', res.data.openid)
+					}
+					uni.setStorageSync('userInfo', {
+						nickname: res.data.nickname,
+						userId: res.data.userId
+					})
+
+					uni.showToast({ title: '登录成功', icon: 'success' })
+
+					// 保存用户角色
+					const role = res.data.role || 'parent'
+
+					// 根据 isNewUser 决定跳转页面
+					setTimeout(() => {
+						if (res.data.isNewUser) {
+							// 新用户:跳转到信息编辑页面
+							uni.redirectTo({
+								url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}`
+							})
+						} else {
+							// 老用户:根据角色进入对应首页
+							this.navigateToHome(role)
+						}
+					}, 1000)
+				} catch (e) {
+					uni.showToast({ title: e.message || '登录失败', icon: 'none' })
+				} finally {
+					this.loading = false
+				}
+			}
+		},
     navigateToHome(role) {
       if (role === 'parent') {
         uni.reLaunch({

+ 405 - 0
zxyj-frontend/pages/user-edit/user-edit.vue

@@ -0,0 +1,405 @@
+<template>
+	<view class="user-edit-container">
+		<view class="header">
+			<text class="title">完善个人信息</text>
+			<text class="subtitle">以下信息可帮助您更好地使用服务</text>
+		</view>
+
+		<view class="form-section">
+			<!-- 头像 -->
+			<view class="form-item avatar-item">
+				<text class="label">头像</text>
+				<view class="avatar-upload" @click="chooseAvatar">
+					<image v-if="form.avatar" :src="form.avatar" class="avatar-preview"></image>
+					<view v-else class="avatar-placeholder">
+						<text class="iconfont icon-plus">+</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 昵称 -->
+			<view class="form-item">
+				<text class="label">昵称 <text class="required">*</text></text>
+				<input type="text" v-model="form.nickname" placeholder="请输入昵称" class="input" maxlength="20" />
+			</view>
+
+			<!-- 真实姓名 -->
+			<view class="form-item">
+				<text class="label">真实姓名</text>
+				<input type="text" v-model="form.realName" placeholder="选填,用于家庭管理" class="input" maxlength="20" />
+			</view>
+
+			<!-- 角色选择 -->
+			<view class="form-item">
+				<text class="label">我是</text>
+				<radio-group class="role-select" @change="onRoleChange">
+					<label class="role-option">
+						<radio value="parent" :checked="form.role === 'parent'" />
+						<text>家长</text>
+					</label>
+					<label class="role-option">
+						<radio value="child" :checked="form.role === 'child'" />
+						<text>孩子</text>
+					</label>
+				</radio-group>
+			</view>
+
+			<!-- 性别选择 -->
+			<view class="form-item">
+				<text class="label">性别</text>
+				<radio-group class="role-select" @change="onGenderChange">
+					<label class="role-option">
+						<radio value="male" :checked="form.gender === 'male'" />
+						<text>男</text>
+					</label>
+					<label class="role-option">
+						<radio value="female" :checked="form.gender === 'female'" />
+						<text>女</text>
+					</label>
+				</radio-group>
+			</view>
+
+			<!-- 身份证号 -->
+			<view class="form-item">
+				<text class="label">身份证号</text>
+				<input type="idcard" v-model="form.idCard" placeholder="选填,填写后自动生成生日" class="input" maxlength="18" @input="onIdCardInput" />
+			</view>
+
+			<!-- 生日(可选,如果填了身份证则自动填充) -->
+			<view class="form-item">
+				<text class="label">生日</text>
+				<picker mode="date" :value="form.birthday" @change="onBirthdayChange">
+					<view class="picker-input">{{ form.birthday || '请选择生日' }}</view>
+				</picker>
+			</view>
+		</view>
+
+		<view class="family-section">
+			<view class="section-title">家庭绑定</view>
+
+			<!-- 邀请码输入 -->
+			<view class="form-item">
+				<text class="label">家庭邀请码</text>
+				<input type="text" v-model="form.inviteCode" placeholder="选填,如有邀请码请输入" class="input" maxlength="8" />
+			</view>
+
+			<view class="family-actions">
+				<button v-if="form.inviteCode" class="btn btn-primary" @click="joinFamilyAction" :loading="loading">
+					加入家庭
+				</button>
+				<button v-else class="btn btn-primary" @click="createFamilyAction" :loading="loading">
+					创建新家庭
+				</button>
+				<button class="btn btn-skip" @click="skip">
+					跳过
+				</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import { updateUserInfo, joinFamily, createFamily } from '../../utils/api.js'
+
+export default {
+	data() {
+		return {
+			form: {
+				avatar: '',
+				nickname: '',
+				realName: '',
+				role: 'parent',
+				gender: '',
+				idCard: '',
+				birthday: '',
+				inviteCode: ''
+			},
+			loading: false,
+			token: '',
+			userId: null
+		}
+	},
+	onLoad(options) {
+		// 从登录页面传递过来的参数
+		this.token = options.token || ''
+		this.userId = options.userId || null
+		// 从本地存储获取用户信息
+		const userInfo = uni.getStorageSync('userInfo')
+		if (userInfo) {
+			this.form.nickname = userInfo.nickname || ''
+			this.form.avatar = userInfo.avatar || ''
+		}
+	},
+	methods: {
+		// 选择头像
+		chooseAvatar() {
+			uni.chooseImage({
+				count: 1,
+				sizeType: ['compressed'],
+				sourceType: ['album', 'camera'],
+				success: (res) => {
+					this.form.avatar = res.tempFilePaths[0]
+				}
+			})
+		},
+
+		// 角色选择
+		onRoleChange(e) {
+			this.form.role = e.detail.value
+		},
+
+		// 性别选择
+		onGenderChange(e) {
+			this.form.gender = e.detail.value
+		},
+
+		// 生日选择
+		onBirthdayChange(e) {
+			this.form.birthday = e.detail.value
+		},
+
+		// 身份证输入 - 自动提取生日
+		onIdCardInput(e) {
+			const idCard = e.detail.value
+			if (idCard.length === 15) {
+				// 15位身份证:1985年07月08日 -> 19850708
+				const birthYear = '19' + idCard.substring(6, 8)
+				const birthMonth = idCard.substring(8, 10)
+				const birthDay = idCard.substring(10, 12)
+				this.form.birthday = `${birthYear}-${birthMonth}-${birthDay}`
+			} else if (idCard.length === 18) {
+				// 18位身份证:1985年07月08日 -> 19850708
+				const birthYear = idCard.substring(6, 10)
+				const birthMonth = idCard.substring(10, 12)
+				const birthDay = idCard.substring(12, 14)
+				this.form.birthday = `${birthYear}-${birthMonth}-${birthDay}`
+			}
+		},
+
+		// 加入现有家庭
+		async joinFamilyAction() {
+			if (!this.form.inviteCode) {
+				uni.showToast({ title: '请输入邀请码', icon: 'none' })
+				return
+			}
+			await this.saveUserInfo('join')
+		},
+
+		// 创建新家庭
+		async createFamilyAction() {
+			await this.saveUserInfo('create')
+		},
+
+		// 跳过
+		async skip() {
+			await this.saveUserInfo('skip')
+		},
+
+		// 保存用户信息
+		async saveUserInfo(action) {
+			if (!this.form.nickname) {
+				uni.showToast({ title: '请输入昵称', icon: 'none' })
+				return
+			}
+
+			this.loading = true
+
+			try {
+				// 1. 更新用户信息(包括新字段:role, gender, birthday, idCard)
+				await updateUserInfo({
+					nickname: this.form.nickname,
+					avatar: this.form.avatar,
+					realName: this.form.realName,
+					role: this.form.role,
+					gender: this.form.gender,
+					birthday: this.form.birthday,
+					idCard: this.form.idCard
+				})
+
+				// 2. 根据 action 处理家庭绑定
+				if (action === 'join' && this.form.inviteCode) {
+					await joinFamily(this.form.inviteCode)
+				} else if (action === 'create') {
+					await createFamily('我的家庭')
+				}
+				// action === 'skip' 时不做处理,使用自动创建的默认家庭
+
+				uni.showToast({ title: '保存成功', icon: 'success' })
+
+				// 更新本地存储的用户信息
+				uni.setStorageSync('userInfo', {
+					nickname: this.form.nickname,
+					avatar: this.form.avatar
+				})
+				uni.setStorageSync('role', this.form.role)
+
+  // 跳转到首页
+  setTimeout(() => {
+    // 根据角色跳转到对应的首页
+    const role = uni.getStorageSync('role') || 'parent'
+    if (role === 'parent') {
+      uni.reLaunch({
+        url: '/pages/index/parent-index'
+      })
+    } else {
+      uni.reLaunch({
+        url: '/pages/index/child-index'
+      })
+    }
+  }, 1000)
+			} catch (error) {
+				uni.showToast({ title: error.message || '保存失败', icon: 'none' })
+			} finally {
+				this.loading = false
+			}
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.user-edit-container {
+	min-height: 100vh;
+	background: #f8f8f8;
+	padding: 40rpx;
+}
+
+.header {
+	text-align: center;
+	margin-bottom: 60rpx;
+
+	.title {
+		display: block;
+		font-size: 48rpx;
+		font-weight: bold;
+		color: #333;
+		margin-bottom: 20rpx;
+	}
+
+	.subtitle {
+		display: block;
+		font-size: 28rpx;
+		color: #999;
+	}
+}
+
+.form-section {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 40rpx;
+	margin-bottom: 40rpx;
+}
+
+.form-item {
+	margin-bottom: 40rpx;
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+
+	.label {
+		display: block;
+		font-size: 28rpx;
+		color: #333;
+		margin-bottom: 20rpx;
+
+		.required {
+			color: #ff6b6b;
+		}
+	}
+
+	.input {
+		width: 100%;
+		height: 80rpx;
+		background: #f8f8f8;
+		border-radius: 12rpx;
+		padding: 0 30rpx;
+		font-size: 28rpx;
+	}
+
+	.picker-input {
+		width: 100%;
+		height: 80rpx;
+		background: #f8f8f8;
+		border-radius: 12rpx;
+		padding: 0 30rpx;
+		font-size: 28rpx;
+		line-height: 80rpx;
+		color: #333;
+	}
+}
+
+.role-select {
+	display: flex;
+	gap: 40rpx;
+	padding: 10rpx 0;
+}
+
+.role-option {
+	display: flex;
+	align-items: center;
+	gap: 10rpx;
+}
+
+.avatar-item {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+
+	.avatar-upload {
+		width: 120rpx;
+		height: 120rpx;
+		border-radius: 50%;
+		overflow: hidden;
+		background: #f8f8f8;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		.avatar-preview {
+			width: 100%;
+			height: 100%;
+		}
+
+		.avatar-placeholder {
+			font-size: 48rpx;
+			color: #ccc;
+		}
+	}
+}
+
+.family-section {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 40rpx;
+
+	.section-title {
+		font-size: 32rpx;
+		font-weight: bold;
+		color: #333;
+		margin-bottom: 30rpx;
+	}
+}
+
+.family-actions {
+	margin-top: 40rpx;
+
+	.btn {
+		width: 100%;
+		height: 88rpx;
+		border-radius: 44rpx;
+		font-size: 32rpx;
+		margin-bottom: 20rpx;
+	}
+
+	.btn-primary {
+		background: #ff6b6b;
+		color: #fff;
+	}
+
+	.btn-skip {
+		background: #f8f8f8;
+		color: #999;
+	}
+}
+</style>

+ 74 - 1
zxyj-frontend/utils/api.js

@@ -50,11 +50,21 @@ export const phoneLogin = (data) => {
   return request('/api/auth/phone-login', 'POST', data)
 }
 
-// 微信手机号登录
+// 微信手机号登录(新版)
 export const wechatPhoneLogin = (data) => {
   return request('/api/auth/wechat-phone-login', 'POST', data)
 }
 
+// 微信静默登录
+export const silentLogin = (code) => {
+  return request('/api/auth/silent-login', 'POST', { code })
+}
+
+// 通过openid自动登录
+export const autoLogin = (openid) => {
+  return request('/api/auth/auto-login', 'POST', { openid })
+}
+
 export const setPassword = (password) => {
   return request('/api/auth/set-password', 'POST', { password })
 }
@@ -92,6 +102,35 @@ export const switchBackToParent = () => {
 	return request('/api/user/switch-back-to-parent', 'POST', {})
 }
 
+export const getUserRoles = () => {
+	return request('/api/user/roles', 'GET')
+}
+
+export const switchRole = (role) => {
+	return request('/api/user/switch-role', 'POST', { role })
+}
+
+// 会员模块
+export const getMembershipLevels = () => {
+	return request('/api/membership/levels', 'GET')
+}
+
+export const getMyMembership = () => {
+	return request('/api/membership/my', 'GET')
+}
+
+export const getCurrentLevel = () => {
+	return request('/api/membership/current', 'GET')
+}
+
+export const createOrder = (levelCode, paymentType) => {
+	return request('/api/membership/orders', 'POST', { levelCode, paymentType })
+}
+
+export const getAuthorizations = () => {
+	return request('/api/parent/authorizations', 'GET')
+}
+
 export const getChildren = () => {
   return request('/api/user/children', 'GET')
 }
@@ -178,3 +217,37 @@ export const getRewardTemplates = () => {
 export const getExchangeHistory = (childId, page = 1, size = 10) => {
   return request('/api/rewards/exchange-history', 'GET', { childId, page, size })
 }
+
+// =============================================
+// 家长任务模块(孩子分配给家长的任务)
+// =============================================
+export const getTodayParentTasks = () => {
+  return request('/api/tasks/today-parent', 'GET')
+}
+
+export const completeParentTask = (taskId) => {
+  return request(`/api/tasks/${taskId}/complete-parent`, 'POST', {})
+}
+
+// =============================================
+// 家长心愿单模块
+// =============================================
+export const createParentWishlist = (data) => {
+  return request('/api/parent/wishlist', 'POST', data)
+}
+
+export const getParentWishlist = () => {
+  return request('/api/parent/wishlist', 'GET')
+}
+
+export const getFamilyWishlist = () => {
+  return request('/api/parent/wishlist/family', 'GET')
+}
+
+export const exchangeParentWishlist = (id) => {
+  return request(`/api/parent/wishlist/${id}/exchange`, 'POST', {})
+}
+
+export const approveParentWishlist = (id, approved) => {
+  return request(`/api/parent/wishlist/${id}/approve`, 'POST', { approved })
+}