Преглед изворни кода

feat: 完善前端页面和组件

- 新增MediaRecorder组件 - 支持拍照和录音
- 新增PlayfulButton和PlayfulCard组件 - 趣味化UI
- 新增指导师相关页面(套餐管理、绑定邀请等)
- 新增家长市场页面(套餐市场、任务计划)
- 新增任务创建和审核页面
- 新增教师端页面
- 新增会员中心页面
- 新增孩子管理页面
- 优化登录页面和首页UI
- 更新pages.json路由配置
- 更新全局样式uni.scss
User пре 7 месеци
родитељ
комит
c765325082
30 измењених фајлова са 3710 додато и 578 уклоњено
  1. 310 0
      zxyj-frontend/components/MediaRecorder.vue
  2. 55 0
      zxyj-frontend/components/PlayfulButton.vue
  3. 56 0
      zxyj-frontend/components/PlayfulCard.vue
  4. 1 12
      zxyj-frontend/package.json
  5. 54 0
      zxyj-frontend/pages.json
  6. 251 0
      zxyj-frontend/pages/guide/bind/invite.vue
  7. 192 0
      zxyj-frontend/pages/guide/packages/create.vue
  8. 244 0
      zxyj-frontend/pages/guide/packages/detail.vue
  9. 225 0
      zxyj-frontend/pages/guide/packages/index.vue
  10. 187 0
      zxyj-frontend/pages/guide/packages/share.vue
  11. 5 0
      zxyj-frontend/pages/index/child-index.vue
  12. 0 292
      zxyj-frontend/pages/index/index.vue
  13. 0 14
      zxyj-frontend/pages/index/parent-index.vue
  14. 5 2
      zxyj-frontend/pages/login/login.vue
  15. 363 0
      zxyj-frontend/pages/membership/index.vue
  16. 157 0
      zxyj-frontend/pages/parent/market/detail.vue
  17. 119 0
      zxyj-frontend/pages/parent/market/index.vue
  18. 56 0
      zxyj-frontend/pages/parent/plans/detail.vue
  19. 130 0
      zxyj-frontend/pages/parent/plans/index.vue
  20. 65 0
      zxyj-frontend/pages/profile/children.vue
  21. 80 0
      zxyj-frontend/pages/profile/create-child.vue
  22. 18 18
      zxyj-frontend/pages/profile/profile.vue
  23. 82 0
      zxyj-frontend/pages/register-child/index.vue
  24. 61 16
      zxyj-frontend/pages/rewards/rewards.vue
  25. 123 0
      zxyj-frontend/pages/tasks/create-task.vue
  26. 109 0
      zxyj-frontend/pages/tasks/review.vue
  27. 4 0
      zxyj-frontend/pages/tasks/tasks.vue
  28. 320 0
      zxyj-frontend/pages/teacher/index.vue
  29. 388 224
      zxyj-frontend/pages/user-edit/user-edit.vue
  30. 50 0
      zxyj-frontend/uni.scss

+ 310 - 0
zxyj-frontend/components/MediaRecorder.vue

@@ -0,0 +1,310 @@
+<template>
+	<view class="media-recorder">
+		<view class="toolbar">
+			<!-- 拍照按钮 -->
+			<button class="tool-btn" @click="chooseImage">
+				<text class="icon">📷</text>
+				<text>拍照</text>
+			</button>
+
+			<!-- 录像按钮 -->
+			<button class="tool-btn" @click="chooseVideo">
+				<text class="icon">📹</text>
+				<text>录像</text>
+			</button>
+
+			<!-- 录音按钮 -->
+			<button class="tool-btn" @click="toggleRecording">
+				<text class="icon">🎤</text>
+				<text>{{ isRecording ? '停止' : '录音' }}</text>
+			</button>
+
+			<!-- 文字按钮 -->
+			<button class="tool-btn" @click="showTextInput = !showTextInput">
+				<text class="icon">✏️</text>
+				<text>文字</text>
+			</button>
+		</view>
+
+		<!-- 文字输入框 -->
+		<view v-if="showTextInput" class="text-input-area">
+			<textarea
+				v-model="textContent"
+				placeholder="输入文字记录..."
+				class="text-input"
+				maxlength="500"
+			/>
+			<button class="btn-add-text" @click="addTextNote">添加</button>
+		</view>
+
+		<!-- 媒体预览列表 -->
+		<view class="media-list" v-if="mediaItems.length > 0">
+			<view class="media-item" v-for="(item, index) in mediaItems" :key="index">
+				<image v-if="item.type === 'image'" :src="item.url" class="media-preview" mode="aspectFill" />
+				<view v-else-if="item.type === 'video'" class="media-preview video-preview">
+					<text class="icon">📹</text>
+				</view>
+				<view v-else-if="item.type === 'audio'" class="media-preview audio-preview">
+					<text class="icon">🎤</text>
+					<text class="duration">{{ item.duration }}s</text>
+				</view>
+				<view v-else-if="item.type === 'text'" class="media-preview text-preview">
+					<text>{{ item.content }}</text>
+				</view>
+				<button class="btn-delete" @click="removeMedia(index)">×</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'MediaRecorder',
+	props: {
+		taskId: {
+			type: Number,
+			required: true
+		},
+		creatorId: {
+			type: Number,
+			required: true
+		}
+	},
+	data() {
+		return {
+			mediaItems: [],
+			textContent: '',
+			showTextInput: false,
+			isRecording: false,
+			recorderManager: null,
+			recordingStartTime: 0
+		}
+	},
+	created() {
+		// 初始化录音管理器
+		this.recorderManager = uni.getRecorderManager()
+		this.recorderManager.onStop((res) => {
+			const duration = (Date.now() - this.recordingStartTime) / 1000
+			this.uploadAudio(res.tempFilePath, duration)
+		})
+	},
+	methods: {
+		// 选择图片
+		chooseImage() {
+			uni.chooseImage({
+				count: 9,
+				sizeType: ['compressed'],
+				sourceType: ['album', 'camera'],
+				success: (res) => {
+					res.tempFilePaths.forEach(path => {
+						this.uploadFile(path, 'image')
+					})
+				}
+			})
+		},
+
+		// 选择视频
+		chooseVideo() {
+			uni.chooseVideo({
+				sourceType: ['album', 'camera'],
+				maxDuration: 60,
+				success: (res) => {
+					this.uploadFile(res.tempFilePath, 'video')
+				}
+			})
+		},
+
+		// 开始/停止录音
+		toggleRecording() {
+			if (this.isRecording) {
+				this.recorderManager.stop()
+				this.isRecording = false
+			} else {
+				this.recordingStartTime = Date.now()
+				this.recorderManager.start()
+				this.isRecording = true
+			}
+		},
+
+		// 添加文字记录
+		addTextNote() {
+			if (!this.textContent.trim()) {
+				uni.showToast({ title: '请输入内容', icon: 'none' })
+				return
+			}
+			this.mediaItems.push({
+				type: 'text',
+				content: this.textContent,
+				uploaded: false
+			})
+			this.textContent = ''
+			this.showTextInput = false
+			this.$emit('change', this.mediaItems)
+		},
+
+		// 上传文件
+		uploadFile(filePath, type) {
+			uni.showLoading({ title: '上传中...' })
+			uni.uploadFile({
+				url: 'http://localhost:8080/api/media/upload',
+				filePath: filePath,
+				name: 'file',
+				formData: {
+					taskId: this.taskId,
+					creatorId: this.creatorId,
+					type: type
+				},
+				header: {
+					'Authorization': 'Bearer ' + uni.getStorageSync('token')
+				},
+				success: (res) => {
+					const data = JSON.parse(res.data)
+					if (data.code === 200) {
+						this.mediaItems.push({
+							id: data.data.id,
+							type: type,
+							url: data.data.url,
+							uploaded: true
+						})
+						this.$emit('change', this.mediaItems)
+					} else {
+						uni.showToast({ title: data.message || '上传失败', icon: 'none' })
+					}
+				},
+				fail: () => {
+					uni.showToast({ title: '上传失败', icon: 'none' })
+				},
+				complete: () => {
+					uni.hideLoading()
+				}
+			})
+		},
+
+		// 上传音频
+		uploadAudio(filePath, duration) {
+			this.uploadFile(filePath, 'audio')
+			this.mediaItems[this.mediaItems.length - 1].duration = Math.round(duration)
+		},
+
+		// 删除媒体
+		removeMedia(index) {
+			this.mediaItems.splice(index, 1)
+			this.$emit('change', this.mediaItems)
+		},
+
+		// 获取所有媒体数据
+		getMediaItems() {
+			return this.mediaItems
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.media-recorder {
+	margin: 20rpx 0;
+}
+
+.toolbar {
+	display: flex;
+	gap: 20rpx;
+	justify-content: space-around;
+	padding: 20rpx;
+	background: #fff;
+	border-radius: 12rpx;
+}
+
+.tool-btn {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 20rpx;
+	background: #f8f8f8;
+	border-radius: 12rpx;
+	font-size: 24rpx;
+
+	.icon {
+		font-size: 48rpx;
+		margin-bottom: 10rpx;
+	}
+}
+
+.text-input-area {
+	margin-top: 20rpx;
+	background: #fff;
+	border-radius: 12rpx;
+	padding: 20rpx;
+}
+
+.text-input {
+	width: 100%;
+	min-height: 150rpx;
+	border: 1rpx solid #ddd;
+	border-radius: 8rpx;
+	padding: 20rpx;
+}
+
+.btn-add-text {
+	margin-top: 20rpx;
+	background: #FF6B6B;
+	color: #fff;
+}
+
+.media-list {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 20rpx;
+	margin-top: 20rpx;
+}
+
+.media-item {
+	position: relative;
+	width: 200rpx;
+	height: 200rpx;
+}
+
+.media-preview {
+	width: 100%;
+	height: 100%;
+	border-radius: 8rpx;
+	object-fit: cover;
+}
+
+.video-preview, .audio-preview, .text-preview {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	background: #f8f8f8;
+
+	.icon {
+		font-size: 64rpx;
+	}
+
+	.duration {
+		font-size: 24rpx;
+		color: #666;
+		margin-top: 10rpx;
+	}
+}
+
+.text-preview {
+	padding: 20rpx;
+	font-size: 24rpx;
+	text-align: center;
+}
+
+.btn-delete {
+	position: absolute;
+	top: -10rpx;
+	right: -10rpx;
+	width: 40rpx;
+	height: 40rpx;
+	border-radius: 50%;
+	background: #ff4444;
+	color: #fff;
+	font-size: 32rpx;
+	line-height: 40rpx;
+	padding: 0;
+}
+</style>

+ 55 - 0
zxyj-frontend/components/PlayfulButton.vue

@@ -0,0 +1,55 @@
+<template>
+  <button
+    class="playful-btn"
+    :class="['playful-btn--' + variant, 'playful-btn--' + size]"
+    :disabled="disabled"
+    @click="emitClick"
+    :aria-label="ariaLabel"
+  >
+    <slot></slot>
+  </button>
+</template>
+
+<script>
+export default {
+  name: 'PlayfulButton',
+  props: {
+    variant: { type: String, default: 'primary' }, // 'primary' | 'secondary'
+    size: { type: String, default: 'md' }, // 'sm' | 'md' | 'lg'
+    disabled: { type: Boolean, default: false },
+    ariaLabel: { type: String, default: '' }
+  },
+  methods: {
+    emitClick(e) {
+      if (!this.disabled) this.$emit('click', e);
+    }
+  }
+}
+</script>
+
+<style scoped>
+.playful-btn {
+  border: none;
+  border-radius: 999px;
+  padding: 0.6rem 1rem;
+  font-weight: 700;
+  cursor: pointer;
+  transition: transform .15s ease, box-shadow .15s ease;
+}
+.playful-btn--primary {
+  background: var(--color-primary, #0EA5E9);
+  color: #fff;
+  box-shadow: 0 2px 6px rgba(14,165,233,.3);
+}
+.playful-btn--secondary {
+  background: #f8fafc;
+  color: var(--text, #1f2937);
+  border: 1px solid var(--border, #e5e7eb);
+}
+.playful-btn--sm { font-size: 0.875rem; padding: 0.5rem 0.75rem; }
+.playful-btn--md { font-size: 1rem; padding: 0.6rem 1rem; }
+.playful-btn--lg { font-size: 1.125rem; padding: 0.75rem 1.25rem; }
+
+.playful-btn:active { transform: translateY(1px) scale(0.98); }
+.playful-btn:focus { outline: 2px solid rgba(14,165,233,.6); outline-offset: 2px; }
+</style>

+ 56 - 0
zxyj-frontend/components/PlayfulCard.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="playful-card" :style="cardStyle">
+    <div class="playful-card__header" v-if="$slots.header">
+      <slot name="header"></slot>
+    </div>
+    <div class="playful-card__content">
+      <slot></slot>
+    </div>
+    <div class="playful-card__actions" v-if="$slots.actions">
+      <slot name="actions"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PlayfulCard',
+  props: {
+    // simple elevation control (not strictly required with CSS vars)
+    elevation: { type: Number, default: 1 }
+  },
+  computed: {
+    cardStyle() {
+      // Optional per-card styling hook; kept simple here
+      return {};
+    }
+  }
+}
+</script>
+
+<style scoped>
+.playful-card {
+  background: var(--surface, #ffffff);
+  border: 1px solid var(--border, #e5e7eb);
+  border-radius: 20px;
+  padding: 16px;
+  box-shadow: 0 4px 14px rgba(14, 165, 233, 0.12);
+}
+.playful-card__header {
+  font-weight: 700;
+  font-family: var(--font-display, system-ui, -apple-system, "Noto Sans SC", sans-serif);
+  font-size: 1.05rem;
+  margin-bottom: 8px;
+}
+.playful-card__content {
+  color: var(--text, #1f2937);
+  line-height: 1.6;
+  font-family: var(--font-body, system-ui, -apple-system, "Inter", sans-serif);
+}
+.playful-card__actions {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+</style>

+ 1 - 12
zxyj-frontend/package.json

@@ -3,19 +3,8 @@
   "version": "1.0.0",
   "description": "心知家庭小程序前端",
   "main": "main.js",
-  "scripts": {
-    "dev:mp-weixin": "uni -p mp-weixin",
-    "build:mp-weixin": "uni build -p mp-weixin"
-  },
   "dependencies": {
-    "@dcloudio/uni-app": "^2.0.0",
-    "@dcloudio/uni-h5": "^2.0.0",
-    "@dcloudio/uni-mp-weixin": "^2.0.0",
+    "vue": "^2.6.14",
     "vuex": "^3.2.0"
-  },
-  "devDependencies": {
-    "@dcloudio/types": "^3.0.0",
-    "@dcloudio/uni-automator": "^2.0.0",
-    "@dcloudio/uni-cli-shared": "^2.0.0"
   }
 }

+ 54 - 0
zxyj-frontend/pages.json

@@ -78,6 +78,60 @@
       "style": {
         "navigationBarTitleText": "审核任务"
       }
+    },
+    {
+      "path": "pages/guide/packages/index",
+      "style": {
+        "navigationBarTitleText": "我的套餐"
+      }
+    },
+    {
+      "path": "pages/guide/packages/create",
+      "style": {
+        "navigationBarTitleText": "创建套餐"
+      }
+    },
+    {
+      "path": "pages/guide/packages/detail",
+      "style": {
+        "navigationBarTitleText": "套餐详情"
+      }
+    },
+    {
+      "path": "pages/parent/market/index",
+      "style": {
+        "navigationBarTitleText": "套餐市场"
+      }
+    },
+    {
+      "path": "pages/parent/market/detail",
+      "style": {
+        "navigationBarTitleText": "套餐详情"
+      }
+    },
+    {
+      "path": "pages/parent/plans/index",
+      "style": {
+        "navigationBarTitleText": "我的计划"
+      }
+    },
+    {
+      "path": "pages/parent/plans/detail",
+      "style": {
+        "navigationBarTitleText": "计划详情"
+      }
+    },
+    {
+      "path": "pages/guide/bind/invite",
+      "style": {
+        "navigationBarTitleText": "绑定指导师"
+      }
+    },
+    {
+      "path": "pages/guide/packages/share",
+      "style": {
+        "navigationBarTitleText": "分享套餐"
+      }
     }
   ],
   "globalStyle": {

+ 251 - 0
zxyj-frontend/pages/guide/bind/invite.vue

@@ -0,0 +1,251 @@
+<template>
+  <view class="container">
+    <!-- 邀请信息 -->
+    <view class="invite-card" v-if="guideInfo">
+      <view class="guide-avatar">{{ guideInfo.guideName ? guideInfo.guideName[0] : '师' }}</view>
+      <text class="guide-name">{{ guideInfo.guideName }}</text>
+      <text class="invite-desc">邀请您绑定家庭关系</text>
+      <text class="expire-text">邀请有效期至 {{ formatDate(guideInfo.expireTime) }}</text>
+    </view>
+
+    <!-- 绑定表单 -->
+    <view class="bind-form" v-if="!bound">
+      <view class="form-item">
+        <text class="label">选择家庭</text>
+        <picker :range="families" range-key="name" @change="onFamilyChange">
+          <view class="picker">
+            {{ selectedFamily ? selectedFamily.name : '请选择家庭' }}
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="label">服务类型</text>
+        <picker :range="serviceTypes" range-key="label" @change="onServiceTypeChange">
+          <view class="picker">
+            {{ serviceType.label }}
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="label">服务价格</text>
+        <input class="input" type="digit" v-model="servicePrice" placeholder="请输入服务价格" />
+      </view>
+
+      <button class="btn-confirm" @click="confirmBind">确认绑定</button>
+    </view>
+
+    <!-- 已绑定提示 -->
+    <view class="bound-card" v-else>
+      <text class="success-icon">✓</text>
+      <text class="success-text">绑定成功</text>
+      <text class="success-desc">您已成功绑定指导师</text>
+      <button class="btn-go" @click="goHome">返回首页</button>
+    </view>
+
+    <!-- 分享邀请 -->
+    <view class="share-section" v-if="isGuide">
+      <text class="section-title">邀请家长绑定</text>
+      <view class="invite-url">
+        <text selectable>{{ inviteUrl }}</text>
+      </view>
+      <button class="btn-copy" @click="copyUrl">复制邀请链接</button>
+      <button class="btn-qrcode" @click="showQrcode">生成二维码</button>
+    </view>
+
+    <!-- 二维码弹窗 -->
+    <uni-popup ref="qrPopup" type="center">
+      <view class="qr-popup">
+        <text class="qr-title">扫码绑定</text>
+        <image class="qr-image" :src="qrcodeUrl" mode="widthFix" />
+        <text class="qr-desc">微信扫一扫绑定</text>
+        <text class="close-btn" @click="closeQrcode">关闭</text>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { generateBindInvite } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      inviteToken: '',
+      guideId: null,
+      guideInfo: null,
+      families: [],
+      selectedFamily: null,
+      serviceTypes: [
+        { label: '咨询', value: 'consulting' },
+        { label: '月度服务', value: 'monthly' },
+        { label: '年度服务', value: 'yearly' }
+      ],
+      serviceType: { label: '咨询', value: 'consulting' },
+      servicePrice: 0,
+      bound: false,
+      isGuide: false,
+      inviteUrl: '',
+      qrcodeUrl: ''
+    }
+  },
+  onLoad(options) {
+    if (options.token) {
+      this.inviteToken = options.token
+      this.guideId = options.guideId
+      this.loadGuideInfo()
+    }
+    
+    // 判断是否为指导师
+    const roles = uni.getStorageSync('roles') || ''
+    this.isGuide = roles.includes('teacher')
+    
+    if (this.isGuide) {
+      this.generateInvite()
+    }
+    
+    this.loadFamilies()
+  },
+  methods: {
+    loadGuideInfo() {
+      // 从服务器获取指导师信息
+      uni.request({
+        url: `${getApp().globalData.baseUrl}/api/guide/bind/validate`,
+        data: { token: this.inviteToken },
+        success: (res) => {
+          if (res.data.code === 200) {
+            this.guideInfo = res.data.data
+          }
+        }
+      })
+    },
+    loadFamilies() {
+      // 获取家庭列表
+      uni.request({
+        url: `${getApp().globalData.baseUrl}/api/user/families`,
+        header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
+        success: (res) => {
+          if (res.data.code === 200) {
+            this.families = res.data.data || []
+          }
+        }
+      })
+    },
+    onFamilyChange(e) {
+      this.selectedFamily = this.families[e.detail.value]
+    },
+    onServiceTypeChange(e) {
+      this.serviceType = this.serviceTypes[e.detail.value]
+    },
+    async confirmBind() {
+      if (!this.selectedFamily) {
+        uni.showToast({ title: '请选择家庭', icon: 'none' })
+        return
+      }
+      
+      uni.showLoading({ title: '绑定中...' })
+      try {
+        await uni.request({
+          url: `${getApp().globalData.baseUrl}/api/bind/accept`,
+          method: 'POST',
+          header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
+          data: {
+            guideId: this.guideId,
+            bindToken: this.inviteToken,
+            familyId: this.selectedFamily.id,
+            serviceType: this.serviceType.value,
+            servicePrice: this.servicePrice
+          }
+        })
+        uni.showToast({ title: '绑定成功' })
+        this.bound = true
+      } catch (e) {
+        uni.showToast({ title: '绑定失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    },
+    async generateInvite() {
+      try {
+        const res = await generateBindInvite()
+        this.inviteUrl = res.data.inviteUrl
+        this.guideInfo = res.data
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    copyUrl() {
+      uni.setClipboardData({ data: this.inviteUrl })
+      uni.showToast({ title: '已复制' })
+    },
+    showQrcode() {
+      // 生成二维码(需要后端返回二维码图片)
+      this.qrcodeUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' + encodeURIComponent(this.inviteUrl)
+      this.$refs.qrPopup.open()
+    },
+    closeQrcode() {
+      this.$refs.qrPopup.close()
+    },
+    formatDate(date) {
+      if (!date) return ''
+      const d = new Date(date)
+      return `${d.getMonth() + 1}-${d.getDate()}`
+    },
+    goHome() {
+      uni.switchTab({ url: '/pages/index/parent-index' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
+
+.invite-card, .bound-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 60rpx 30rpx;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.guide-avatar {
+  width: 120rpx;
+  height: 120rpx;
+  background: #4CAF50;
+  border-radius: 50%;
+  margin: 0 auto 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 48rpx;
+  color: #fff;
+}
+
+.guide-name { font-size: 36rpx; font-weight: bold; display: block; margin-bottom: 10rpx; }
+.invite-desc { font-size: 28rpx; color: #666; display: block; margin-bottom: 20rpx; }
+.expire-text { font-size: 24rpx; color: #999; }
+
+.bind-form { background: #fff; border-radius: 16rpx; padding: 30rpx; }
+.form-item { margin-bottom: 30rpx; }
+.label { display: block; font-size: 28rpx; color: #333; margin-bottom: 16rpx; }
+.input, .picker { padding: 20rpx; background: #f5f5f5; border-radius: 12rpx; font-size: 28rpx; }
+.btn-confirm { background: #4CAF50; color: #fff; border-radius: 40rpx; margin-top: 40rpx; }
+
+.success-icon { font-size: 80rpx; color: #4CAF50; display: block; margin-bottom: 20rpx; }
+.success-text { font-size: 36rpx; font-weight: bold; display: block; margin-bottom: 10rpx; }
+.success-desc { font-size: 28rpx; color: #666; display: block; margin-bottom: 40rpx; }
+.btn-go { background: #4CAF50; color: #fff; border-radius: 40rpx; }
+
+.share-section { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-top: 30rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+.invite-url { background: #f5f5f5; padding: 20rpx; border-radius: 8rpx; margin-bottom: 20rpx; font-size: 24rpx; word-break: break-all; }
+.btn-copy, .btn-qrcode { background: #4CAF50; color: #fff; margin-bottom: 20rpx; }
+
+.qr-popup { background: #fff; border-radius: 16rpx; padding: 40rpx; text-align: center; width: 500rpx; }
+.qr-title { font-size: 32rpx; font-weight: bold; display: block; margin-bottom: 30rpx; }
+.qr-image { width: 300rpx; height: 300rpx; margin-bottom: 20rpx; }
+.qr-desc { font-size: 26rpx; color: #666; display: block; margin-bottom: 30rpx; }
+.close-btn { font-size: 28rpx; color: #999; }
+</style>

+ 192 - 0
zxyj-frontend/pages/guide/packages/create.vue

@@ -0,0 +1,192 @@
+<template>
+  <view class="container">
+    <view class="form">
+      <view class="form-item">
+        <text class="label">套餐名称</text>
+        <input class="input" v-model="form.name" placeholder="如:21天养成学习习惯" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">详细介绍</text>
+        <textarea class="textarea" v-model="form.description" placeholder="描述套餐的内容和目标" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">周期天数</text>
+        <input class="input" type="number" v-model="form.cycleDays" placeholder="如:21" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">分类</text>
+        <picker :range="categories" @change="onCategoryChange">
+          <view class="picker">
+            {{ form.category || '请选择分类' }}
+          </view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="label">价格 (元)</text>
+        <input class="input" type="digit" v-model="form.price" placeholder="0 表示免费" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">封面图</text>
+        <view class="cover-upload" @click="chooseImage">
+          <image v-if="form.coverImage" :src="form.coverImage" mode="aspectFill" class="cover-image"></image>
+          <text v-else class="upload-icon">+</text>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">是否公开</text>
+        <switch v-model="form.isPublic" :checked="form.isPublic" @change="onPublicChange" />
+        <text class="switch-label">{{ form.isPublic ? '公开到市场' : '仅分享链接' }}</text>
+      </view>
+
+      <button class="btn-submit" @click="submit">保存</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { createPackage } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      form: {
+        name: '',
+        description: '',
+        cycleDays: 21,
+        category: '',
+        price: 0,
+        isPublic: false,
+        coverImage: ''
+      },
+      categories: ['学习', '习惯', '健康', '情绪', '其他']
+    }
+  },
+  methods: {
+    onCategoryChange(e) {
+      this.form.category = this.categories[e.detail.value]
+    },
+    onPublicChange(e) {
+      this.form.isPublic = e.detail.value
+    },
+    chooseImage() {
+      uni.chooseImage({
+        count: 1,
+        success: (res) => {
+          this.form.coverImage = res.tempFilePaths[0]
+        }
+      })
+    },
+    async submit() {
+      if (!this.form.name) {
+        uni.showToast({ title: '请输入套餐名称', icon: 'none' })
+        return
+      }
+      if (!this.form.cycleDays) {
+        uni.showToast({ title: '请输入周期天数', icon: 'none' })
+        return
+      }
+
+      uni.showLoading({ title: '保存中...' })
+      try {
+        await createPackage({
+          ...this.form,
+          isPublic: this.form.isPublic ? 1 : 0
+        })
+        uni.showToast({ title: '创建成功' })
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1500)
+      } catch (e) {
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 30rpx;
+}
+
+.form {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+}
+
+.form-item {
+  margin-bottom: 30rpx;
+}
+
+.label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.input, .textarea {
+  width: 100%;
+  padding: 20rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+}
+
+.textarea {
+  height: 160rpx;
+}
+
+.picker {
+  padding: 20rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.cover-upload {
+  width: 200rpx;
+  height: 200rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.upload-icon {
+  font-size: 60rpx;
+  color: #ccc;
+}
+
+.cover-image {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 12rpx;
+}
+
+.switch-label {
+  margin-left: 20rpx;
+  font-size: 26rpx;
+  color: #666;
+}
+
+.btn-submit {
+  background: #4CAF50;
+  color: #fff;
+  border-radius: 40rpx;
+  margin-top: 40rpx;
+}
+</style>

+ 244 - 0
zxyj-frontend/pages/guide/packages/detail.vue

@@ -0,0 +1,244 @@
+<template>
+  <view class="container">
+    <!-- 套餐信息 -->
+    <view class="package-info">
+      <view class="info-header">
+        <text class="name">{{ pkg.name }}</text>
+        <text class="status" :class="pkg.status">{{ getStatusText(pkg.status) }}</text>
+      </view>
+      <view class="info-row">
+        <text>周期: {{ pkg.cycleDays }}天</text>
+        <text>价格: ¥{{ pkg.price }}</text>
+      </view>
+      <view class="info-desc">{{ pkg.description }}</view>
+      <view class="share-code" v-if="pkg.shareCode">
+        <text>分享码: {{ pkg.shareCode }}</text>
+        <text class="copy" @click="copyCode">复制</text>
+      </view>
+    </view>
+
+    <!-- 任务项列表 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">任务项</text>
+        <button class="btn-add" @click="showAddItem">添加</button>
+      </view>
+      
+      <view class="item-list">
+        <view 
+          class="item-card" 
+          v-for="item in items" 
+          :key="item.id"
+        >
+          <view class="item-header">
+            <text class="item-name">{{ item.taskName }}</text>
+            <text class="item-required" v-if="item.isRequired">必选</text>
+          </view>
+          <view class="item-info">
+            <text>积分: {{ item.points }}</text>
+            <text>频率: {{ item.frequency }}</text>
+            <text>时长: {{ item.duration }}分钟</text>
+          </view>
+          <view class="item-actions">
+            <text class="action-edit" @click="editItem(item)">编辑</text>
+            <text class="action-delete" @click="deleteItem(item.id)">删除</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="empty-items" v-if="items.length === 0">
+        <text>暂无任务项,请添加</text>
+      </view>
+    </view>
+
+    <!-- 添加/编辑任务项弹窗 -->
+    <uni-popup ref="itemPopup" type="bottom">
+      <view class="popup-content">
+        <view class="popup-header">
+          <text>{{ editingItem ? '编辑任务项' : '添加任务项' }}</text>
+          <text class="close" @click="closePopup">×</text>
+        </view>
+        <view class="popup-form">
+          <view class="form-item">
+            <text class="label">任务名称</text>
+            <input class="input" v-model="itemForm.taskName" placeholder="任务名称" />
+          </view>
+          <view class="form-item">
+            <text class="label">描述</text>
+            <textarea class="textarea" v-model="itemForm.description" placeholder="任务描述" />
+          </view>
+          <view class="form-item">
+            <text class="label">积分</text>
+            <input class="input" type="number" v-model="itemForm.points" placeholder="积分" />
+          </view>
+          <view class="form-item">
+            <text class="label">频率</text>
+            <picker :range="frequencies" @change="onFreqChange">
+              <view class="picker">{{ itemForm.frequency || '请选择' }}</view>
+            </picker>
+          </view>
+          <view class="form-item">
+            <text class="label">预计时长(分钟)</text>
+            <input class="input" type="number" v-model="itemForm.duration" />
+          </view>
+          <view class="form-item">
+            <text class="label">必选</text>
+            <switch v-model="itemForm.isRequired" />
+          </view>
+          <button class="btn-save" @click="saveItem">保存</button>
+        </view>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { 
+  getPackage, 
+  getPackageItems, 
+  addPackageItem, 
+  updatePackageItem, 
+  deletePackageItem 
+} from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      pkgId: null,
+      pkg: {},
+      items: [],
+      editingItem: null,
+      itemForm: {
+        taskName: '',
+        description: '',
+        points: 10,
+        frequency: 'daily',
+        duration: 30,
+        isRequired: false
+      },
+      frequencies: ['daily', 'weekly', 'custom']
+    }
+  },
+  onLoad(options) {
+    this.pkgId = options.id
+    this.loadPackage()
+    this.loadItems()
+  },
+  methods: {
+    async loadPackage() {
+      try {
+        const res = await getPackage(this.pkgId)
+        this.pkg = res.data || {}
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async loadItems() {
+      try {
+        const res = await getPackageItems(this.pkgId)
+        this.items = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    getStatusText(status) {
+      const map = { 'draft': '草稿', 'pending': '待审核', 'approved': '已发布', 'rejected': '已拒绝' }
+      return map[status] || status
+    },
+    copyCode() {
+      uni.setClipboardData({ data: this.pkg.shareCode })
+      uni.showToast({ title: '已复制' })
+    },
+    showAddItem() {
+      this.editingItem = null
+      this.itemForm = { taskName: '', description: '', points: 10, frequency: 'daily', duration: 30, isRequired: false }
+      this.$refs.itemPopup.open()
+    },
+    editItem(item) {
+      this.editingItem = item
+      this.itemForm = { ...item, isRequired: !!item.isRequired }
+      this.$refs.itemPopup.open()
+    },
+    onFreqChange(e) {
+      this.itemForm.frequency = this.frequencies[e.detail.value]
+    },
+    closePopup() {
+      this.$refs.itemPopup.close()
+    },
+    async saveItem() {
+      if (!this.itemForm.taskName) {
+        uni.showToast({ title: '请输入任务名称', icon: 'none' })
+        return
+      }
+      try {
+        const data = { ...this.itemForm, packageId: this.pkgId, isRequired: this.itemForm.isRequired ? 1 : 0 }
+        if (this.editingItem) {
+          await updatePackageItem(this.editingItem.id, data)
+        } else {
+          await addPackageItem(this.pkgId, data)
+        }
+        uni.showToast({ title: '保存成功' })
+        this.closePopup()
+        this.loadItems()
+      } catch (e) {
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      }
+    },
+    async deleteItem(id) {
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除这个任务项吗?',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              await deletePackageItem(id)
+              uni.showToast({ title: '已删除' })
+              this.loadItems()
+            } catch (e) {
+              uni.showToast({ title: '删除失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; }
+.package-info { background: #fff; padding: 30rpx; margin-bottom: 20rpx; }
+.info-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
+.name { font-size: 34rpx; font-weight: bold; color: #333; }
+.status { font-size: 24rpx; padding: 8rpx 16rpx; border-radius: 20rpx; background: #f0f0f0; color: #666; }
+.info-row { display: flex; gap: 30rpx; font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
+.info-desc { font-size: 26rpx; color: #999; margin-bottom: 16rpx; }
+.share-code { display: flex; justify-content: space-between; font-size: 26rpx; color: #4CAF50; }
+.copy { color: #4CAF50; }
+
+.section { background: #fff; padding: 30rpx; }
+.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; }
+.btn-add { background: #4CAF50; color: #fff; font-size: 24rpx; padding: 12rpx 24rpx; border-radius: 24rpx; }
+
+.item-list { display: flex; flex-direction: column; gap: 16rpx; }
+.item-card { background: #f9f9f9; padding: 20rpx; border-radius: 12rpx; }
+.item-header { display: flex; justify-content: space-between; margin-bottom: 12rpx; }
+.item-name { font-size: 28rpx; font-weight: bold; }
+.item-required { font-size: 22rpx; color: #ff9800; }
+.item-info { display: flex; gap: 20rpx; font-size: 24rpx; color: #666; margin-bottom: 12rpx; }
+.item-actions { display: flex; gap: 20rpx; justify-content: flex-end; }
+.action-edit { color: #4CAF50; font-size: 26rpx; }
+.action-delete { color: #f44336; font-size: 26rpx; }
+.empty-items { text-align: center; padding: 40rpx; color: #999; }
+
+.popup-content { background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 30rpx; max-height: 80vh; overflow-y: auto; }
+.popup-header { display: flex; justify-content: space-between; font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
+.close { font-size: 48rpx; color: #999; }
+.popup-form .form-item { margin-bottom: 24rpx; }
+.popup-form .label { display: block; font-size: 26rpx; color: #333; margin-bottom: 12rpx; }
+.popup-form .input, .popup-form .textarea { width: 100%; padding: 16rpx; background: #f5f5f5; border-radius: 8rpx; }
+.popup-form .textarea { height: 100rpx; }
+.popup-form .picker { padding: 16rpx; background: #f5f5f5; border-radius: 8rpx; }
+.btn-save { background: #4CAF50; color: #fff; border-radius: 40rpx; margin-top: 20rpx; }
+</style>

+ 225 - 0
zxyj-frontend/pages/guide/packages/index.vue

@@ -0,0 +1,225 @@
+<template>
+  <view class="container">
+    <!-- 头部 -->
+    <view class="header">
+      <text class="title">我的套餐</text>
+      <button class="btn-create" @click="goToCreate">创建套餐</button>
+    </view>
+
+    <!-- 套餐列表 -->
+    <view class="package-list" v-if="packages.length > 0">
+      <view 
+        class="package-card" 
+        v-for="pkg in packages" 
+        :key="pkg.id"
+        @click="goToDetail(pkg.id)"
+      >
+        <view class="package-header">
+          <text class="package-name">{{ pkg.name }}</text>
+          <text class="package-status" :class="pkg.status">{{ getStatusText(pkg.status) }}</text>
+        </view>
+        <view class="package-info">
+          <text class="info-item">周期: {{ pkg.cycleDays }}天</text>
+          <text class="info-item">价格: ¥{{ pkg.price }}</text>
+        </view>
+        <view class="package-desc">{{ pkg.description || '暂无描述' }}</view>
+        <view class="package-actions">
+          <button 
+            v-if="pkg.status === 'draft'" 
+            class="btn-action" 
+            @click.stop="submitReview(pkg.id)"
+          >提交审核</button>
+          <button 
+            v-if="pkg.status === 'approved'" 
+            class="btn-action" 
+            @click.stop="togglePublish(pkg)"
+          >{{ pkg.status === 'offline' ? '发布' : '下架' }}</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty" v-else>
+      <text class="empty-text">暂无套餐,快去创建一个吧</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyPackages, submitPackageForReview, publishPackage } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      packages: []
+    }
+  },
+  onShow() {
+    this.loadPackages()
+  },
+  methods: {
+    async loadPackages() {
+      try {
+        const res = await getMyPackages()
+        this.packages = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    getStatusText(status) {
+      const map = {
+        'draft': '草稿',
+        'pending': '待审核',
+        'approved': '已发布',
+        'rejected': '已拒绝',
+        'offline': '已下架'
+      }
+      return map[status] || status
+    },
+    goToCreate() {
+      uni.navigateTo({
+        url: '/pages/guide/packages/create'
+      })
+    },
+    goToDetail(id) {
+      uni.navigateTo({
+        url: `/pages/guide/packages/detail?id=${id}`
+      })
+    },
+    async submitReview(id) {
+      uni.showLoading({ title: '提交中...' })
+      try {
+        await submitPackageForReview(id)
+        uni.showToast({ title: '提交成功' })
+        this.loadPackages()
+      } catch (e) {
+        uni.showToast({ title: '提交失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    },
+    async togglePublish(pkg) {
+      const publish = pkg.status === 'offline'
+      uni.showLoading({ title: publish ? '发布中...' : '下架中...' })
+      try {
+        await publishPackage(pkg.id, publish)
+        uni.showToast({ title: publish ? '发布成功' : '已下架' })
+        this.loadPackages()
+      } catch (e) {
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 30rpx;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.btn-create {
+  background: #4CAF50;
+  color: #fff;
+  font-size: 28rpx;
+  padding: 16rpx 30rpx;
+  border-radius: 30rpx;
+}
+
+.package-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.package-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+
+.package-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.package-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.package-status {
+  font-size: 24rpx;
+  padding: 8rpx 16rpx;
+  border-radius: 20rpx;
+}
+
+.package-status.draft { background: #f0f0f0; color: #666; }
+.package-status.pending { background: #fff3e0; color: #ff9800; }
+.package-status.approved { background: #e8f5e9; color: #4CAF50; }
+.package-status.rejected { background: #ffebee; color: #f44336; }
+.package-status.offline { background: #f5f5f5; color: #999; }
+
+.package-info {
+  display: flex;
+  gap: 30rpx;
+  margin-bottom: 16rpx;
+}
+
+.info-item {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.package-desc {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 16rpx;
+}
+
+.package-actions {
+  display: flex;
+  gap: 20rpx;
+}
+
+.btn-action {
+  font-size: 26rpx;
+  padding: 12rpx 24rpx;
+  border-radius: 24rpx;
+  background: #f0f0f0;
+  color: #333;
+}
+
+.empty {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 400rpx;
+}
+
+.empty-text {
+  color: #999;
+  font-size: 28rpx;
+}
+</style>

+ 187 - 0
zxyj-frontend/pages/guide/packages/share.vue

@@ -0,0 +1,187 @@
+<template>
+  <view class="container">
+    <!-- 套餐信息卡片 -->
+    <view class="package-card">
+      <image class="cover" :src="pkg.coverImage || '/static/default-cover.png'" mode="aspectFill" />
+      <view class="package-info">
+        <text class="name">{{ pkg.name }}</text>
+        <text class="desc">{{ pkg.description }}</text>
+        <view class="meta">
+          <text>{{ pkg.cycleDays }}天</text>
+          <text class="price">¥{{ pkg.price }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 分享选项 -->
+    <view class="share-options">
+      <text class="section-title">分享给家长</text>
+      
+      <view class="option-item" @click="copyShareCode">
+        <text class="option-icon">📋</text>
+        <text class="option-text">复制分享码</text>
+      </view>
+      
+      <view class="option-item" @click="generatePoster">
+        <text class="option-icon">🖼️</text>
+        <text class="option-text">生成分享海报</text>
+      </view>
+      
+      <view class="option-item" @click="shareToFriend">
+        <text class="option-icon">📱</text>
+        <text class="option-text">发送给朋友</text>
+      </view>
+    </view>
+
+    <!-- 海报预览 -->
+    <uni-popup ref="posterPopup" type="center">
+      <view class="poster-popup">
+        <text class="poster-title">分享海报</text>
+        <canvas canvas-id="posterCanvas" class="poster-canvas"></canvas>
+        <view class="poster-actions">
+          <button class="btn-save" @click="savePoster">保存图片</button>
+          <button class="btn-share" open-type="share">转发给朋友</button>
+        </view>
+        <text class="close-btn" @click="closePoster">关闭</text>
+      </view>
+    </uni-popup>
+  </view>
+</template>
+
+<script>
+import { getPackage } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      pkgId: null,
+      pkg: {},
+      shareCode: ''
+    }
+  },
+  onLoad(options) {
+    this.pkgId = options.id
+    this.loadPackage()
+  },
+  methods: {
+    async loadPackage() {
+      try {
+        const res = await getPackage(this.pkgId)
+        this.pkg = res.data || {}
+        this.shareCode = this.pkg.shareCode || ''
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    copyShareCode() {
+      uni.setClipboardData({ data: this.shareCode })
+      uni.showToast({ title: '已复制分享码' })
+    },
+    generatePoster() {
+      const ctx = uni.createCanvasContext('posterCanvas')
+      
+      // 背景
+      ctx.setFillStyle('#ffffff')
+      ctx.fillRect(0, 0, 600, 900)
+      
+      // 封面图(简化处理,实际需要下载图片)
+      ctx.setFillStyle('#4CAF50')
+      ctx.fillRect(0, 0, 600, 300)
+      
+      // 标题
+      ctx.setFillStyle('#333333')
+      ctx.setFontSize(32)
+      ctx.fillText(this.pkg.name || '套餐名称', 30, 350)
+      
+      // 描述
+      ctx.setFillStyle('#666666')
+      ctx.setFontSize(24)
+      const desc = this.pkg.description || '暂无描述'
+      ctx.fillText(desc.substring(0, 30), 30, 400)
+      if (desc.length > 30) {
+        ctx.fillText(desc.substring(30, 60), 30, 430)
+      }
+      
+      // 周期和价格
+      ctx.setFillStyle('#333333')
+      ctx.setFontSize(28)
+      ctx.fillText(`${this.pkg.cycleDays}天`, 30, 480)
+      ctx.setFillStyle('#f44336')
+      ctx.setFontSize(36)
+      ctx.fillText(`¥${this.pkg.price}`, 30, 520)
+      
+      // 分享码
+      ctx.setFillStyle('#4CAF50')
+      ctx.setFontSize(24)
+      ctx.fillText('分享码: ' + this.shareCode, 30, 580)
+      
+      // 提示文字
+      ctx.setFillStyle('#999999')
+      ctx.setFontSize(20)
+      ctx.fillText('微信扫码或输入分享码查看套餐', 30, 640)
+      
+      // 品牌标识
+      ctx.setFillStyle('#cccccc')
+      ctx.setFontSize(16)
+      ctx.fillText('知行益家', 480, 880)
+      
+      ctx.draw()
+      
+      this.$refs.posterPopup.open()
+    },
+    savePoster() {
+      uni.canvasToTempFilePath({
+        canvasId: 'posterCanvas',
+        success: (res) => {
+          uni.saveImageToPhotosAlbum({
+            filePath: res.tempFilePath,
+            success: () => {
+              uni.showToast({ title: '已保存到相册' })
+            },
+            fail: () => {
+              uni.showToast({ title: '保存失败', icon: 'none' })
+            }
+          })
+        },
+        fail: () => {
+          uni.showToast({ title: '生成失败', icon: 'none' })
+        }
+      })
+    },
+    shareToFriend() {
+      // 触发分享
+    },
+    closePoster() {
+      this.$refs.posterPopup.close()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
+
+.package-card { background: #fff; border-radius: 16rpx; overflow: hidden; margin-bottom: 30rpx; }
+.cover { width: 100%; height: 300rpx; }
+.package-info { padding: 30rpx; }
+.name { font-size: 34rpx; font-weight: bold; color: #333; display: block; margin-bottom: 10rpx; }
+.desc { font-size: 26rpx; color: #666; display: block; margin-bottom: 20rpx; }
+.meta { display: flex; align-items: center; gap: 20rpx; font-size: 26rpx; }
+.price { font-size: 34rpx; color: #f44336; font-weight: bold; }
+
+.share-options { background: #fff; border-radius: 16rpx; padding: 30rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.option-item { display: flex; align-items: center; padding: 30rpx 0; border-bottom: 1rpx solid #f0f0f0; }
+.option-item:last-child { border-bottom: none; }
+.option-icon { font-size: 40rpx; margin-right: 20rpx; }
+.option-text { font-size: 28rpx; color: #333; }
+
+.poster-popup { background: #fff; border-radius: 16rpx; padding: 30rpx; width: 600rpx; }
+.poster-title { font-size: 32rpx; font-weight: bold; display: block; text-align: center; margin-bottom: 20rpx; }
+.poster-canvas { width: 600rpx; height: 900rpx; background: #f5f5f5; margin: 0 auto; }
+.poster-actions { display: flex; gap: 20rpx; margin-top: 20rpx; }
+.btn-save, .btn-share { flex: 1; background: #4CAF50; color: #fff; font-size: 28rpx; }
+.btn-share { background: #2196F3; }
+.close-btn { display: block; text-align: center; margin-top: 20rpx; color: #999; font-size: 28rpx; }
+</style>

+ 5 - 0
zxyj-frontend/pages/index/child-index.vue

@@ -1,5 +1,6 @@
 <template>
   <view class="container">
+    <PlayfulCard elevation="1">
     <!-- 欢迎区域 -->
     <view class="welcome-section">
       <view class="welcome-text">
@@ -58,12 +59,16 @@
       </view>
     </view>
   </view>
+  </PlayfulCard>
 </template>
 
 <script>
+import PlayfulCard from '@/components/PlayfulCard.vue'
+import PlayfulButton from '@/components/PlayfulButton.vue'
 import { getTodayTasks, getPointsBalance, getChildren, switchBackToParent } from '../../utils/api.js'
 
 export default {
+  components: { PlayfulCard, PlayfulButton },
   data() {
     return {
       nickname: '',

+ 0 - 292
zxyj-frontend/pages/index/index.vue

@@ -1,292 +0,0 @@
-<template>
-  <view class="container">
-    <!-- 欢迎区域 -->
-    <view class="welcome-section">
-      <view class="welcome-text">
-        <text class="hello">你好,{{ nickname || '小朋友' }}</text>
-        <text class="subtitle">今天也要加油完成任务哦!</text>
-      </view>
-      <view class="streak-badge" v-if="streakDays > 0">
-        <text class="fire">🔥</text>
-        <text class="days">{{ streakDays }}天</text>
-      </view>
-    </view>
-
-    <!-- 积分卡片 -->
-    <view class="points-card">
-      <view class="points-label">当前积分</view>
-      <view class="points-value">{{ totalPoints }}</view>
-      <view class="points-expire" v-if="expireDate">有效期至 {{ expireDate }}</view>
-    </view>
-
-    <!-- 今日任务概览 -->
-    <view class="section">
-      <view class="section-title">今日任务</view>
-      <view class="task-summary">
-        <view class="summary-item">
-          <text class="num">{{ completedCount }}</text>
-          <text class="label">已完成</text>
-        </view>
-        <view class="summary-item">
-          <text class="num">{{ pendingCount }}</text>
-          <text class="label">待完成</text>
-        </view>
-        <view class="summary-item">
-          <text class="num">{{ totalPoints }}</text>
-          <text class="label">今日获得</text>
-        </view>
-      </view>
-      <button class="btn-primary view-all-btn" @click="goToTasks">查看全部任务</button>
-    </view>
-
-    <!-- 快捷操作 -->
-    <view class="section">
-      <view class="section-title">快捷操作</view>
-      <view class="quick-actions">
-        <view class="action-item" @click="goToRewards">
-          <text class="icon">🎁</text>
-          <text class="label">心愿单</text>
-        </view>
-        <view class="action-item" @click="goToFocus">
-          <text class="icon">⏱️</text>
-          <text class="label">专注训练</text>
-        </view>
-        <view class="action-item" @click="switchMode">
-          <text class="icon">🔄</text>
-          <text class="label">切换模式</text>
-        </view>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getTodayTasks, getPointsBalance, getChildren } from '../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      nickname: '',
-      totalPoints: 0,
-      streakDays: 0,
-      expireDate: '',
-      completedCount: 0,
-      pendingCount: 0,
-      currentChildId: ''
-    }
-  },
-  onLoad() {
-    this.checkLogin()
-  },
-  onShow() {
-    if (uni.getStorageSync('token')) {
-      this.loadData()
-    }
-  },
-  methods: {
-    checkLogin() {
-      if (!uni.getStorageSync('token')) {
-        uni.reLaunch({
-          url: '/pages/login/login'
-        })
-      }
-    },
-    async loadData() {
-      try {
-        // 获取孩子列表
-        const childrenRes = await getChildren()
-        const children = childrenRes.data
-        if (children && children.length > 0) {
-          const child = children[0]
-          this.currentChildId = child.id
-          this.nickname = child.nickname
-          this.totalPoints = child.totalPoints
-          this.streakDays = child.streakDays
-          
-          // 获取积分余额
-          const balanceRes = await getPointsBalance(child.id)
-          this.totalPoints = balanceRes.data.balance
-          this.expireDate = this.formatDate(balanceRes.data.expireDate)
-          
-          // 获取今日任务
-          const tasksRes = await getTodayTasks(child.id)
-          const tasks = tasksRes.data
-          this.completedCount = tasks.filter(t => t.status === 'completed').length
-          this.pendingCount = tasks.filter(t => t.status === 'pending').length
-        }
-      } catch (e) {
-        console.error('加载数据失败', e)
-      }
-    },
-    formatDate(dateStr) {
-      if (!dateStr) return ''
-      const date = new Date(dateStr)
-      return `${date.getMonth() + 1}月${date.getDate()}日`
-    },
-    goToTasks() {
-      uni.switchTab({
-        url: '/pages/tasks/tasks'
-      })
-    },
-    goToRewards() {
-      uni.switchTab({
-        url: '/pages/rewards/rewards'
-      })
-    },
-    goToFocus() {
-      uni.navigateTo({
-        url: '/pages/focus/focus'
-      })
-    },
-    switchMode() {
-      uni.showModal({
-        title: '切换模式',
-        content: '确定要切换模式吗?',
-        success: async (res) => {
-          if (res.confirm) {
-            const currentRole = this.$store.state.role
-            const newRole = currentRole === 'parent' ? 'child' : 'parent'
-            this.$store.commit('switchRole', newRole)
-            uni.showToast({
-              title: `已切换为${newRole === 'parent' ? '家长' : '孩子'}模式`,
-              icon: 'success'
-            })
-          }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-}
-
-.welcome-section {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 30rpx;
-}
-
-.hello {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-}
-
-.subtitle {
-  font-size: 26rpx;
-  color: #666;
-}
-
-.streak-badge {
-  display: flex;
-  align-items: center;
-  background: linear-gradient(135deg, #FF6B6B, #FF8E53);
-  padding: 10rpx 20rpx;
-  border-radius: 30rpx;
-}
-
-.fire {
-  font-size: 32rpx;
-}
-
-.days {
-  color: #fff;
-  font-size: 24rpx;
-  margin-left: 8rpx;
-}
-
-.points-card {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  border-radius: 20rpx;
-  padding: 40rpx;
-  text-align: center;
-  margin-bottom: 30rpx;
-}
-
-.points-label {
-  color: rgba(255, 255, 255, 0.8);
-  font-size: 26rpx;
-}
-
-.points-value {
-  color: #fff;
-  font-size: 72rpx;
-  font-weight: bold;
-  margin: 20rpx 0;
-}
-
-.points-expire {
-  color: rgba(255, 255, 255, 0.6);
-  font-size: 22rpx;
-}
-
-.section {
-  margin-bottom: 30rpx;
-}
-
-.section-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 20rpx;
-}
-
-.task-summary {
-  display: flex;
-  justify-content: space-around;
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-}
-
-.summary-item {
-  text-align: center;
-}
-
-.summary-item .num {
-  font-size: 40rpx;
-  font-weight: bold;
-  color: #FF6B6B;
-  display: block;
-}
-
-.summary-item .label {
-  font-size: 24rpx;
-  color: #666;
-}
-
-.view-all-btn {
-  width: 100%;
-}
-
-.quick-actions {
-  display: flex;
-  justify-content: space-around;
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-}
-
-.action-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.action-item .icon {
-  font-size: 48rpx;
-  margin-bottom: 10rpx;
-}
-
-.action-item .label {
-  font-size: 24rpx;
-  color: #666;
-}
-</style>

+ 0 - 14
zxyj-frontend/pages/index/parent-index.vue

@@ -62,20 +62,6 @@
           <text class="icon">➕</text>
           <text class="label">添加任务</text>
         </view>
-        <view class="action-item" @click="goToReview">
-          <text class="icon">📋</text>
-          <text class="label">审核任务</text>
-        </view>
-        <view class="action-item" @click="addChild">
-          <text class="icon">👶</text>
-          <text class="label">添加孩子</text>
-        </view>
-        <view class="action-item" @click="switchMode">
-          <text class="icon">🔄</text>
-          <text class="label">切换模式</text>
-        </view>
-      </view>
-    </view>
         <view class="action-item" @click="goToReview">
           <text class="icon">📋</text>
           <text class="label">审核任务</text>

+ 5 - 2
zxyj-frontend/pages/login/login.vue

@@ -203,12 +203,15 @@ export default {
 			// 保存用户角色
 			const role = res.data.role || 'parent'
 
+			// 保存用户信息
+			uni.setStorageSync('roles', res.data.roles || role)
+
 			// 根据 isNewUser 决定跳转页面
 			setTimeout(() => {
 				if (res.data.isNewUser) {
-					// 新用户:跳转到信息编辑页面
+					// 新用户:跳转到信息编辑页面(先选择角色)
 					uni.redirectTo({
-						url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}`
+						url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}&needsRoleSelect=true`
 					})
 				} else {
 					// 老用户:根据角色进入对应首页

+ 363 - 0
zxyj-frontend/pages/membership/index.vue

@@ -0,0 +1,363 @@
+<template>
+  <view class="container">
+    <!-- 当前会员状态 -->
+    <view class="membership-card">
+      <view class="card-header">
+        <text class="card-title">我的会员</text>
+        <text class="card-status" :class="currentLevel.levelCode">{{ currentLevel.levelName }}</text>
+      </view>
+      <view class="card-body" v-if="membership">
+        <view class="expiry-info">
+          <text class="label">到期时间</text>
+          <text class="value">{{ formatDate(membership.endDate) }}</text>
+        </view>
+        <view class="days-info" v-if="membership.isActive">
+          <text class="remaining">{{ membership.daysRemaining }}天</text>
+          <text class="tip">剩余天数</text>
+        </view>
+        <view class="expired-tip" v-else>
+          <text class="tip">会员已过期,请续费</text>
+        </view>
+      </view>
+      <view class="card-body" v-else>
+        <text class="free-tip">您当前为免费版,升级享受更多服务</text>
+      </view>
+      <button class="btn-upgrade" @click="goToUpgrade">立即升级</button>
+    </view>
+
+    <!-- 会员权益 -->
+    <view class="benefits-section">
+      <text class="section-title">会员权益</text>
+      <view class="benefits-grid">
+        <view class="benefit-item" v-for="level in levels" :key="level.levelCode">
+          <view class="benefit-header">
+            <text class="level-name">{{ level.levelName }}</text>
+            <text class="level-price" v-if="level.priceMonthly > 0">¥{{ level.priceMonthly }}/月</text>
+            <text class="level-price free" v-else>免费</text>
+          </view>
+          <view class="benefit-features">
+            <text class="feature-tag" v-for="(feature, idx) in parseFeatures(level.features)" :key="idx">
+              {{ feature }}
+            </text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 老师咨询授权 -->
+    <view class="auth-section">
+      <text class="section-title">老师咨询授权</text>
+      <view class="auth-item" v-for="auth in authorizations" :key="auth.id">
+        <view class="auth-info">
+          <text class="teacher-name">{{ auth.teacherName }}</text>
+          <text class="auth-status" :class="auth.status">{{ auth.statusText }}</text>
+        </view>
+        <view class="auth-actions">
+          <button class="btn-revoke" v-if="auth.status === 'active'" @click="revokeAuth(auth.id)">撤销</button>
+        </view>
+      </view>
+      <view class="no-auth" v-if="authorizations.length === 0">
+        <text class="tip">暂无老师授权</text>
+        <text class="desc">联系老师获取授权后,可查看孩子任务执行情况</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMembershipLevels, getMyMembership, getAuthorizations } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      levels: [],
+      membership: null,
+      currentLevel: {},
+      authorizations: []
+    }
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      try {
+        // 获取会员等级
+        const levelsRes = await getMembershipLevels()
+        this.levels = levelsRes.data || []
+        
+        // 获取我的会员信息
+        const membershipRes = await getMyMembership()
+        this.membership = membershipRes.data
+        
+        // 确定当前等级
+        if (this.membership && this.membership.levelCode) {
+          this.currentLevel = this.levels.find(l => l.levelCode === this.membership.levelCode) || {}
+        } else {
+          this.currentLevel = this.levels.find(l => l.levelCode === 'FREE') || {}
+        }
+        
+        // 获取授权列表
+        const authRes = await getAuthorizations()
+        this.authorizations = (authRes.data || []).map(a => ({
+          ...a,
+          statusText: a.status === 'active' ? '已授权' : '已撤销'
+        }))
+      } catch (e) {
+        console.error('加载数据失败', e)
+      }
+    },
+    formatDate(date) {
+      if (!date) return ''
+      const d = new Date(date)
+      return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
+    },
+    parseFeatures(features) {
+      if (!features) return []
+      try {
+        return JSON.parse(features)
+      } catch {
+        return []
+      }
+    },
+    goToUpgrade() {
+      uni.navigateTo({
+        url: '/pages/membership/upgrade'
+      })
+    },
+    revokeAuth(authId) {
+      uni.showModal({
+        title: '撤销授权',
+        content: '确定要撤销该老师的查看权限吗?',
+        success: (res) => {
+          if (res.confirm) {
+            // TODO: 调用撤销API
+            uni.showToast({ title: '已撤销', icon: 'success' })
+            this.loadData()
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+}
+
+.membership-card {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 40rpx;
+  margin-bottom: 30rpx;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.card-title {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+.card-status {
+  font-size: 24rpx;
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+  background: rgba(255,255,255,0.2);
+  color: #fff;
+}
+
+.card-body {
+  margin-top: 30rpx;
+}
+
+.expiry-info {
+  display: flex;
+  justify-content: space-between;
+}
+
+.expiry-info .label {
+  font-size: 26rpx;
+  color: rgba(255,255,255,0.8);
+}
+
+.expiry-info .value {
+  font-size: 26rpx;
+  color: #fff;
+}
+
+.days-info {
+  text-align: center;
+  margin-top: 20rpx;
+}
+
+.remaining {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #FFD700;
+}
+
+.days-info .tip {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.7);
+  display: block;
+}
+
+.expired-tip {
+  text-align: center;
+  margin-top: 20rpx;
+}
+
+.expired-tip .tip {
+  color: #FFD700;
+  font-size: 26rpx;
+}
+
+.free-tip {
+  font-size: 26rpx;
+  color: rgba(255,255,255,0.8);
+  text-align: center;
+}
+
+.btn-upgrade {
+  margin-top: 30rpx;
+  background: #FFD700;
+  color: #333;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+
+.benefits-section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.benefits-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.benefit-item {
+  border: 1rpx solid #eee;
+  border-radius: 10rpx;
+  padding: 20rpx;
+}
+
+.benefit-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.level-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.level-price {
+  font-size: 26rpx;
+  color: #FF6B6B;
+}
+
+.level-price.free {
+  color: #52C41A;
+}
+
+.benefit-features {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10rpx;
+  margin-top: 15rpx;
+}
+
+.feature-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  background: #f5f5f5;
+  border-radius: 20rpx;
+  color: #666;
+}
+
+.auth-section {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+
+.auth-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.auth-item:last-child {
+  border-bottom: none;
+}
+
+.auth-info {
+  flex: 1;
+}
+
+.teacher-name {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+}
+
+.auth-status {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+.auth-status.active {
+  color: #52C41A;
+}
+
+.btn-revoke {
+  font-size: 24rpx;
+  padding: 10rpx 20rpx;
+  background: #FF4D4F;
+  color: #fff;
+  border-radius: 10rpx;
+}
+
+.no-auth {
+  text-align: center;
+  padding: 40rpx;
+}
+
+.no-auth .tip {
+  font-size: 28rpx;
+  color: #999;
+  display: block;
+}
+
+.no-auth .desc {
+  font-size: 24rpx;
+  color: #ccc;
+  margin-top: 10rpx;
+  display: block;
+}
+</style>

+ 157 - 0
zxyj-frontend/pages/parent/market/detail.vue

@@ -0,0 +1,157 @@
+<template>
+  <view class="container">
+    <!-- 套餐信息 -->
+    <view class="package-header">
+      <image class="cover" :src="pkg.coverImage || '/static/default-cover.png'" mode="aspectFill" />
+      <view class="header-content">
+        <text class="name">{{ pkg.name }}</text>
+        <view class="meta">
+          <text>{{ pkg.cycleDays }}天</text>
+          <text>{{ pkg.category }}</text>
+        </view>
+        <text class="price">¥{{ pkg.price }}</text>
+      </view>
+    </view>
+
+    <!-- 描述 -->
+    <view class="section">
+      <text class="section-title">套餐介绍</text>
+      <text class="description">{{ pkg.description || '暂无介绍' }}</text>
+    </view>
+
+    <!-- 任务项选择 -->
+    <view class="section">
+      <text class="section-title">选择任务项</text>
+      <view class="item-list">
+        <view 
+          class="item-card" 
+          v-for="item in items" 
+          :key="item.id"
+          :class="{ disabled: item.isRequired }"
+          @click="toggleItem(item)"
+        >
+          <view class="item-check">
+            <checkbox :checked="selectedItems.includes(item.id)" :disabled="item.isRequired" />
+          </view>
+          <view class="item-content">
+            <text class="item-name">{{ item.taskName }}</text>
+            <text class="item-desc">{{ item.description }}</text>
+            <view class="item-meta">
+              <text>积分: {{ item.points }}</text>
+              <text>频率: {{ item.frequency }}</text>
+              <text v-if="item.isRequired" class="required-tag">必选</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部操作 -->
+    <view class="footer">
+      <button class="btn-apply" @click="applyPackage">立即应用</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getPackage, getPackageItems, purchasePackage } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      pkgId: null,
+      pkg: {},
+      items: [],
+      selectedItems: []
+    }
+  },
+  onLoad(options) {
+    this.pkgId = options.id
+    this.loadPackage()
+    this.loadItems()
+  },
+  methods: {
+    async loadPackage() {
+      try {
+        const res = await getPackage(this.pkgId)
+        this.pkg = res.data || {}
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async loadItems() {
+      try {
+        const res = await getPackageItems(this.pkgId)
+        this.items = res.data || []
+        // 自动选中必选项
+        this.items.forEach(item => {
+          if (item.isRequired) {
+            this.selectedItems.push(item.id)
+          }
+        })
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    toggleItem(item) {
+      if (item.isRequired) return
+      const idx = this.selectedItems.indexOf(item.id)
+      if (idx > -1) {
+        this.selectedItems.splice(idx, 1)
+      } else {
+        this.selectedItems.push(item.id)
+      }
+    },
+    async applyPackage() {
+      // 获取孩子列表,选择一个孩子
+      uni.showActionSheet({
+        itemList: ['选择孩子'],
+        success: async () => {
+          // 这里应该先获取孩子列表,让用户选择
+          // 简化处理,直接使用当前家庭的孩子
+          try {
+            const res = await purchasePackage(this.pkgId, {
+              selectedItemIds: this.selectedItems,
+              childId: 1 // TODO: 实际应该让用户选择
+            })
+            uni.showToast({ title: '应用成功' })
+            setTimeout(() => {
+              uni.navigateTo({ url: '/pages/parent/plans/index' })
+            }, 1500)
+          } catch (e) {
+            uni.showToast({ title: '应用失败', icon: 'none' })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
+
+.package-header { position: relative; }
+.cover { width: 100%; height: 400rpx; }
+.header-content { position: absolute; bottom: 0; left: 0; right: 0; padding: 30rpx; background: linear-gradient(transparent, rgba(0,0,0,0.7)); color: #fff; }
+.name { font-size: 36rpx; font-weight: bold; display: block; margin-bottom: 10rpx; }
+.meta { display: flex; gap: 20rpx; font-size: 24rpx; opacity: 0.8; margin-bottom: 10rpx; }
+.price { font-size: 40rpx; font-weight: bold; }
+
+.section { background: #fff; padding: 30rpx; margin-bottom: 10rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+.description { font-size: 28rpx; color: #666; line-height: 1.6; }
+
+.item-list { display: flex; flex-direction: column; gap: 16rpx; }
+.item-card { display: flex; padding: 20rpx; background: #f9f9f9; border-radius: 12rpx; }
+.item-card.disabled { opacity: 0.7; }
+.item-check { margin-right: 16rpx; }
+.item-content { flex: 1; }
+.item-name { font-size: 28rpx; font-weight: bold; display: block; margin-bottom: 8rpx; }
+.item-desc { font-size: 24rpx; color: #999; display: block; margin-bottom: 8rpx; }
+.item-meta { display: flex; gap: 16rpx; font-size: 22rpx; color: #666; }
+.required-tag { color: #ff9800; }
+
+.footer { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 30rpx; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); }
+.btn-apply { background: #4CAF50; color: #fff; border-radius: 40rpx; }
+</style>

+ 119 - 0
zxyj-frontend/pages/parent/market/index.vue

@@ -0,0 +1,119 @@
+<template>
+  <view class="container">
+    <!-- 搜索/分类 -->
+    <view class="filter-bar">
+      <view class="search-box">
+        <input class="search-input" v-model="keyword" placeholder="搜索套餐" @confirm="search" />
+      </view>
+      <view class="categories">
+        <text 
+          class="cat-item" 
+          :class="{ active: !currentCategory }"
+          @click="selectCategory('')"
+        >全部</text>
+        <text 
+          class="cat-item" 
+          v-for="cat in categories" 
+          :key="cat"
+          :class="{ active: currentCategory === cat }"
+          @click="selectCategory(cat)"
+        >{{ cat }}</text>
+      </view>
+    </view>
+
+    <!-- 套餐列表 -->
+    <view class="package-list">
+      <view 
+        class="package-card" 
+        v-for="pkg in packages" 
+        :key="pkg.id"
+        @click="goToDetail(pkg.id)"
+      >
+        <image class="package-cover" :src="pkg.coverImage || '/static/default-cover.png'" mode="aspectFill" />
+        <view class="package-content">
+          <text class="package-name">{{ pkg.name }}</text>
+          <text class="package-desc">{{ pkg.description }}</text>
+          <view class="package-meta">
+            <text class="meta-item">{{ pkg.cycleDays }}天</text>
+            <text class="meta-item">{{ pkg.category }}</text>
+            <text class="price">¥{{ pkg.price }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty" v-if="packages.length === 0">
+      <text>暂无套餐</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getPublicPackages } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      packages: [],
+      keyword: '',
+      currentCategory: '',
+      categories: ['学习', '习惯', '健康', '情绪', '其他']
+    }
+  },
+  onShow() {
+    this.loadPackages()
+  },
+  methods: {
+    async loadPackages() {
+      try {
+        const res = await getPublicPackages(this.currentCategory)
+        this.packages = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    selectCategory(cat) {
+      this.currentCategory = cat
+      this.loadPackages()
+    },
+    search() {
+      // 简单筛选
+      if (this.keyword) {
+        this.packages = this.packages.filter(p => p.name.includes(this.keyword))
+      } else {
+        this.loadPackages()
+      }
+    },
+    goToDetail(id) {
+      uni.navigateTo({
+        url: `/pages/parent/market/detail?id=${id}`
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; }
+
+.filter-bar { background: #fff; padding: 20rpx; margin-bottom: 10rpx; }
+.search-box { margin-bottom: 20rpx; }
+.search-input { padding: 16rpx 20rpx; background: #f5f5f5; border-radius: 30rpx; font-size: 28rpx; }
+
+.categories { display: flex; gap: 20rpx; overflow-x: auto; }
+.cat-item { font-size: 26rpx; color: #666; padding: 10rpx 20rpx; white-space: nowrap; }
+.cat-item.active { color: #4CAF50; font-weight: bold; }
+
+.package-list { padding: 20rpx; display: flex; flex-direction: column; gap: 20rpx; }
+.package-card { background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); }
+.package-cover { width: 100%; height: 300rpx; }
+.package-content { padding: 20rpx; }
+.package-name { font-size: 32rpx; font-weight: bold; color: #333; display: block; margin-bottom: 10rpx; }
+.package-desc { font-size: 26rpx; color: #999; display: block; margin-bottom: 16rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.package-meta { display: flex; align-items: center; gap: 20rpx; }
+.meta-item { font-size: 24rpx; color: #666; }
+.price { font-size: 32rpx; color: #f44336; font-weight: bold; margin-left: auto; }
+
+.empty { display: flex; justify-content: center; padding: 100rpx; color: #999; }
+</style>

+ 56 - 0
zxyj-frontend/pages/parent/plans/detail.vue

@@ -0,0 +1,56 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">计划详情</text>
+    </view>
+    
+    <view class="content">
+      <text class="placeholder">计划详情页面开发中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {}
+  },
+  onLoad(options) {
+    // 获取计划ID
+    const planId = options.id
+    console.log('计划ID:', planId)
+    this.loadPlanDetail(planId)
+  },
+  methods: {
+    async loadPlanDetail(planId) {
+      // TODO: 加载计划详情
+      console.log('加载计划详情:', planId)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+}
+
+.header {
+  padding: 20rpx 0;
+}
+
+.title {
+  font-size: 36rpx;
+  font-weight: bold;
+}
+
+.content {
+  padding: 40rpx;
+  text-align: center;
+}
+
+.placeholder {
+  color: #999;
+  font-size: 28rpx;
+}
+</style>

+ 130 - 0
zxyj-frontend/pages/parent/plans/index.vue

@@ -0,0 +1,130 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">我的计划</text>
+    </view>
+
+    <!-- 计划列表 -->
+    <view class="plan-list" v-if="plans.length > 0">
+      <view 
+        class="plan-card" 
+        v-for="plan in plans" 
+        :key="plan.id"
+        @click="goToDetail(plan.id)"
+      >
+        <view class="plan-header">
+          <text class="plan-name">{{ plan.name }}</text>
+          <text class="plan-status" :class="plan.status">{{ getStatusText(plan.status) }}</text>
+        </view>
+        <view class="plan-progress">
+          <progress :percent="getProgress(plan)" :stroke-width="8" backgroundColor="#e0e0e0" activeColor="#4CAF50" />
+          <text class="progress-text">{{ plan.completedDays }} / {{ plan.totalDays }} 天</text>
+        </view>
+        <view class="plan-dates">
+          <text>{{ formatDate(plan.startDate) }} - {{ formatDate(plan.endDate) }}</text>
+        </view>
+        <view class="plan-actions" v-if="plan.status === 'active'">
+          <button class="btn-cancel" @click.stop="cancelPlan(plan.id)">取消</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty" v-else>
+      <text class="empty-text">暂无计划,快去应用套餐吧</text>
+      <button class="btn-go" @click="goToMarket">去市场</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyPlans, cancelPlan as cancelPlanApi } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      plans: []
+    }
+  },
+  onShow() {
+    this.loadPlans()
+  },
+  methods: {
+    async loadPlans() {
+      try {
+        const res = await getMyPlans()
+        this.plans = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    getStatusText(status) {
+      const map = { 'active': '进行中', 'completed': '已完成', 'cancelled': '已取消' }
+      return map[status] || status
+    },
+    getProgress(plan) {
+      if (!plan.totalDays) return 0
+      return Math.round(plan.completedDays / plan.totalDays * 100)
+    },
+    formatDate(date) {
+      if (!date) return ''
+      const d = new Date(date)
+      return `${d.getMonth() + 1}-${d.getDate()}`
+    },
+    goToDetail(id) {
+      uni.navigateTo({
+        url: `/pages/parent/plans/detail?id=${id}`
+      })
+    },
+    goToMarket() {
+      uni.navigateTo({
+        url: '/pages/parent/market/index'
+      })
+    },
+    async cancelPlan(id) {
+      uni.showModal({
+        title: '确认取消',
+        content: '确定要取消这个计划吗?',
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              await cancelPlanApi(id)
+              uni.showToast({ title: '已取消' })
+              this.loadPlans()
+            } catch (e) {
+              uni.showToast({ title: '取消失败', icon: 'none' })
+            }
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
+.header { margin-bottom: 30rpx; }
+.title { font-size: 36rpx; font-weight: bold; color: #333; }
+
+.plan-list { display: flex; flex-direction: column; gap: 20rpx; }
+.plan-card { background: #fff; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); }
+.plan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.plan-name { font-size: 32rpx; font-weight: bold; color: #333; }
+.plan-status { font-size: 24rpx; padding: 8rpx 16rpx; border-radius: 20rpx; }
+.plan-status.active { background: #e8f5e9; color: #4CAF50; }
+.plan-status.completed { background: #e3f2fd; color: #2196F3; }
+.plan-status.cancelled { background: #f5f5f5; color: #999; }
+
+.plan-progress { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
+.plan-progress progress { flex: 1; }
+.progress-text { font-size: 24rpx; color: #666; }
+
+.plan-dates { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
+.plan-actions { display: flex; justify-content: flex-end; }
+.btn-cancel { background: #f5f5f5; color: #666; font-size: 24rpx; padding: 12rpx 24rpx; border-radius: 24rpx; }
+
+.empty { display: flex; flex-direction: column; align-items: center; padding: 100rpx; }
+.empty-text { color: #999; font-size: 28rpx; margin-bottom: 30rpx; }
+.btn-go { background: #4CAF50; color: #fff; }
+</style>

+ 65 - 0
zxyj-frontend/pages/profile/children.vue

@@ -0,0 +1,65 @@
+<template>
+  <view class="container">
+    <view class="children-list">
+      <view class="child-item" v-for="child in children" :key="child.id">
+        <view class="child-avatar">{{ child.nickname ? child.nickname[0] : '?' }}</view>
+        <view class="child-info">
+          <text class="child-name">{{ child.nickname }}</text>
+          <text class="child-detail">年龄: {{ child.age }} | 积分: {{ child.totalPoints }}</text>
+          <text class="child-dan">DAN: {{ child.danLevel }}</text>
+        </view>
+        <view class="child-actions">
+          <text class="btn-edit" @click="editChild(child)">编辑</text>
+        </view>
+      </view>
+    </view>
+    <view class="empty" v-if="children.length === 0">
+      <text>暂无孩子,请添加</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getChildren, updateChild } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      children: []
+    }
+  },
+  onShow() {
+    this.loadChildren()
+  },
+  methods: {
+    async loadChildren() {
+      try {
+        const res = await getChildren()
+        this.children = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    editChild(child) {
+      uni.showModal({
+        title: '编辑孩子',
+        content: `编辑 ${child.nickname}`,
+        showCancel: false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 30rpx; }
+.children-list { background: #fff; border-radius: 20rpx; }
+.child-item { display: flex; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
+.child-avatar { width: 100rpx; height: 100rpx; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 40rpx; font-weight: bold; margin-right: 20rpx; }
+.child-info { flex: 1; }
+.child-name { font-size: 32rpx; font-weight: bold; color: #333; display: block; }
+.child-detail { font-size: 26rpx; color: #666; display: block; }
+.child-dan { font-size: 24rpx; color: #999; }
+.btn-edit { color: #FF6B6B; font-size: 28rpx; }
+.empty { text-align: center; padding: 100rpx; color: #999; }
+</style>

+ 80 - 0
zxyj-frontend/pages/profile/create-child.vue

@@ -0,0 +1,80 @@
+<template>
+  <view class="container">
+    <view class="form-container">
+      <view class="form-item">
+        <text class="label">孩子昵称</text>
+        <input class="input" v-model="form.nickname" placeholder="请输入孩子昵称" />
+      </view>
+      
+      <view class="form-item">
+        <text class="label">年龄</text>
+        <input class="input" type="number" v-model="form.age" placeholder="请输入年龄" />
+      </view>
+      
+      <view class="form-item">
+        <text class="label">DAN等级</text>
+        <picker :range="danLevels" @change="onDanLevelChange">
+          <view class="picker">
+            {{ form.danLevel || '请选择DAN等级' }}
+          </view>
+        </picker>
+      </view>
+      
+      <button class="btn-primary" @click="submit">保存</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { createChild } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      form: {
+        nickname: '',
+        age: '',
+        danLevel: ''
+      },
+      danLevels: ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6']
+    }
+  },
+  methods: {
+    onDanLevelChange(e) {
+      this.form.danLevel = this.danLevels[e.detail.value]
+    },
+    async submit() {
+      if (!this.form.nickname) {
+        uni.showToast({ title: '请输入孩子昵称', icon: 'none' })
+        return
+      }
+      if (!this.form.age) {
+        uni.showToast({ title: '请输入年龄', icon: 'none' })
+        return
+      }
+      
+      try {
+        await createChild({
+          nickname: this.form.nickname,
+          age: parseInt(this.form.age),
+          danLevel: this.form.danLevel || 'B1'
+        })
+        uni.showToast({ title: '添加成功', icon: 'success' })
+        setTimeout(() => uni.navigateBack(), 1500)
+      } catch (e) {
+        uni.showToast({ title: '添加失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 30rpx; }
+.form-container { background: #fff; border-radius: 20rpx; padding: 30rpx; }
+.form-item { margin-bottom: 30rpx; }
+.label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
+.input { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; }
+.picker { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; color: #666; }
+.btn-primary { background: #FF6B6B; color: #fff; border-radius: 20rpx; margin-top: 40rpx; }
+</style>

+ 18 - 18
zxyj-frontend/pages/profile/profile.vue

@@ -11,24 +11,24 @@
       </button>
     </view>
 
-    <view class="menu-list">
-      <view class="menu-item" @click="goToChildren">
-        <text>👶 我的孩子</text>
-        <text class="arrow">›</text>
-      </view>
-      <view class="menu-item" @click="goToPointsLogs">
-        <text>📊 积分记录</text>
-        <text class="arrow">›</text>
-      </view>
-      <view class="menu-item" @click="showSetPassword">
-        <text>🔐 设置密码</text>
-        <text class="arrow">›</text>
-      </view>
-      <view class="menu-item" @click="logout">
-        <text>🚪 退出登录</text>
-        <text class="arrow">›</text>
-      </view>
-    </view>
+	<view class="menu-list">
+		<view class="menu-item" v-if="role === 'parent'" @click="goToChildren">
+			<text>👶 我的孩子</text>
+			<text class="arrow">›</text>
+		</view>
+		<view class="menu-item" @click="goToPointsLogs">
+			<text>📊 积分记录</text>
+			<text class="arrow">›</text>
+		</view>
+		<view class="menu-item" @click="showSetPassword">
+			<text>🔐 设置密码</text>
+			<text class="arrow">›</text>
+		</view>
+		<view class="menu-item" @click="logout">
+			<text>🚪 退出登录</text>
+			<text class="arrow">›</text>
+		</view>
+	</view>
 
     <!-- 设置密码弹窗 -->
     <view class="modal-mask" v-if="showPasswordModal" @click="showPasswordModal = false">

+ 82 - 0
zxyj-frontend/pages/register-child/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <view class="register-child-page">
+    <PlayfulCard elevation="1">
+      <template slot="header">
+        <view class="playful-card-header">
+          <text class="playful-title">为孩子创建账户</text>
+        </view>
+      </template>
+
+      <view v-if="step === 1" class="form-step">
+        <text>请输入孩子的姓名</text>
+        <input v-model="childName" placeholder="姓名" class="input" />
+        <text>出生日期</text>
+        <input v-model="birth" type="date" class="input" />
+      </view>
+
+      <view v-else-if="step === 2" class="form-step">
+        <text>选择头像</text>
+        <view class="avatar-grid">
+          <image v-for="n in 5" :key="n" :src="avatarPlaceholder" class="avatar" @click="selectAvatar(n)" />
+        </view>
+      </view>
+
+      <view v-else class="form-step">
+        <text>确认信息</text>
+        <text>Name: {{ childName }}</text>
+        <text>Birth: {{ birth }}</text>
+        <text>Avatar: {{ selectedAvatar || '默认' }}</text>
+      </view>
+
+      <template slot="actions">
+        <PlayfulButton v-if="step < 3" variant="primary" size="md" @click="nextStep">
+          下一步
+        </PlayfulButton>
+        <PlayfulButton v-else variant="primary" size="md" @click="finishRegistration">
+          完成
+        </PlayfulButton>
+      </template>
+    </PlayfulCard>
+  </view>
+</template>
+
+<script>
+import PlayfulCard from '@/components/PlayfulCard.vue';
+import PlayfulButton from '@/components/PlayfulButton.vue';
+
+export default {
+  name: 'RegisterChild',
+  components: { PlayfulCard, PlayfulButton },
+  data() {
+    return {
+      step: 1,
+      childName: '',
+      birth: '',
+      selectedAvatar: null,
+      avatarPlaceholder: '/static/avatar-placeholder.png'
+    }
+  },
+  methods: {
+    nextStep() {
+      if (this.step < 3) this.step++;
+      else this.finishRegistration();
+    },
+    selectAvatar(n) { this.selectedAvatar = n; },
+    finishRegistration() {
+      // Here you would typically POST the data to your backend
+      uni.showToast({ title: '注册完成!', icon: 'success' });
+      // Navigate to child dashboard or home
+      uni.redirectTo({ url: '/pages/index/index' });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.register-child-page { padding: 16px; }
+.input { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid var(--border); margin-bottom: 12px; font-size: 1rem; }
+.playful-card-header { font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; }
+.playful-title { font-family: var(--font-display); }
+.avatar-grid { display: flex; gap: 10px; flex-wrap: wrap; }
+.avatar { width: 72px; height: 72px; border-radius: 50%; border: 2px solid #eee; cursor: pointer; }
+</style>

+ 61 - 16
zxyj-frontend/pages/rewards/rewards.vue

@@ -52,7 +52,7 @@
 </template>
 
 <script>
-import { getWishlist, exchangeReward, createReward, getPointsBalance, getChildren } from '../../utils/api.js'
+import { getWishlist, exchangeReward, createReward, getPointsBalance, getChildren, getParentWishlist, createParentWishlist, exchangeParentWishlist } from '../../utils/api.js'
 
 export default {
   data() {
@@ -60,6 +60,7 @@ export default {
       rewards: [],
       currentPoints: 0,
       currentChildId: '',
+      currentRole: 'parent',  // 当前角色: parent/child
       showAddModal: false,
       newReward: {
         title: '',
@@ -68,26 +69,57 @@ export default {
     }
   },
   onShow() {
+    // 获取当前角色
+    this.currentRole = uni.getStorageSync('role') || 'parent'
     this.loadRewards()
   },
   methods: {
     async loadRewards() {
       try {
-        const childrenRes = await getChildren()
-        const children = childrenRes.data
-        if (children && children.length > 0) {
-          this.currentChildId = children[0].id
-          
-          const balanceRes = await getPointsBalance(this.currentChildId)
-          this.currentPoints = balanceRes.data.balance
-          
-          const res = await getWishlist(this.currentChildId)
+        if (this.currentRole === 'parent') {
+          // 家长模式:使用家长心愿单API
+          const res = await getParentWishlist()
           this.rewards = res.data || []
+          // 家长积分从用户信息获取
+          const userInfo = await this.getUserInfo()
+          this.currentPoints = userInfo.totalPoints || 0
+        } else {
+          // 孩子模式:使用孩子心愿单API
+          const childrenRes = await getChildren()
+          const children = childrenRes.data
+          if (children && children.length > 0) {
+            this.currentChildId = children[0].id
+            
+            const balanceRes = await getPointsBalance(this.currentChildId)
+            this.currentPoints = balanceRes.data.balance
+            
+            const res = await getWishlist(this.currentChildId)
+            this.rewards = res.data || []
+          }
         }
       } catch (e) {
         console.error('加载心愿单失败', e)
       }
     },
+    async getUserInfo() {
+      return new Promise((resolve, reject) => {
+        uni.request({
+          url: 'http://localhost:8080/api/user/info',
+          method: 'GET',
+          header: {
+            'Authorization': 'Bearer ' + uni.getStorageSync('token')
+          },
+          success: (res) => {
+            if (res.data.code === 200) {
+              resolve(res.data.data)
+            } else {
+              reject(res.data)
+            }
+          },
+          fail: reject
+        })
+      })
+    },
     async exchangeReward(rewardId) {
       uni.showModal({
         title: '确认兑换',
@@ -95,7 +127,11 @@ export default {
         success: async (res) => {
           if (res.confirm) {
             try {
-              await exchangeReward(rewardId, this.currentChildId)
+              if (this.currentRole === 'parent') {
+                await exchangeParentWishlist(rewardId)
+              } else {
+                await exchangeReward(rewardId, this.currentChildId)
+              }
               uni.showToast({ title: '兑换成功', icon: 'success' })
               this.loadRewards()
             } catch (e) {
@@ -112,11 +148,20 @@ export default {
       }
       
       try {
-        await createReward({
-          title: this.newReward.title,
-          pointsRequired: parseInt(this.newReward.pointsRequired),
-          childId: this.currentChildId
-        })
+        if (this.currentRole === 'parent') {
+          // 家长模式:创建家长心愿
+          await createParentWishlist({
+            title: this.newReward.title,
+            pointsRequired: parseInt(this.newReward.pointsRequired)
+          })
+        } else {
+          // 孩子模式:创建孩子心愿
+          await createReward({
+            title: this.newReward.title,
+            pointsRequired: parseInt(this.newReward.pointsRequired),
+            childId: this.currentChildId
+          })
+        }
         uni.showToast({ title: '添加成功', icon: 'success' })
         this.showAddModal = false
         this.newReward = { title: '', pointsRequired: '' }

+ 123 - 0
zxyj-frontend/pages/tasks/create-task.vue

@@ -0,0 +1,123 @@
+<template>
+  <view class="container">
+    <view class="form-container">
+      <view class="form-item">
+        <text class="label">任务名称</text>
+        <input class="input" v-model="form.title" placeholder="请输入任务名称" />
+      </view>
+      
+      <view class="form-item">
+        <text class="label">任务描述</text>
+        <textarea class="textarea" v-model="form.description" placeholder="请输入任务描述" />
+      </view>
+      
+      <view class="form-item">
+        <text class="label">选择孩子</text>
+        <picker :range="children" range-key="nickname" @change="onChildChange">
+          <view class="picker">
+            {{ selectedChild ? selectedChild.nickname : '请选择孩子' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item">
+        <text class="label">积分</text>
+        <input class="input" type="number" v-model="form.points" placeholder="请输入积分" />
+      </view>
+      
+      <view class="form-item">
+        <text class="label">分类</text>
+        <picker :range="categories" @change="onCategoryChange">
+          <view class="picker">
+            {{ form.category || '请选择分类' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item">
+        <text class="label">需要审核</text>
+        <switch :checked="form.needReview" @change="form.needReview = !form.needReview" />
+      </view>
+      
+      <button class="btn-primary" @click="submit">创建任务</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getChildren, createTask } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      form: {
+        title: '',
+        description: '',
+        childId: '',
+        points: 2,
+        category: '',
+        needReview: false
+      },
+      children: [],
+      selectedChild: null,
+      categories: ['学习', '生活', '运动', '其他']
+    }
+  },
+  onLoad() {
+    this.loadChildren()
+  },
+  methods: {
+    async loadChildren() {
+      try {
+        const res = await getChildren()
+        this.children = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    onChildChange(e) {
+      this.selectedChild = this.children[e.detail.value]
+      this.form.childId = this.selectedChild.id
+    },
+    onCategoryChange(e) {
+      this.form.category = this.categories[e.detail.value]
+    },
+    async submit() {
+      if (!this.form.title) {
+        uni.showToast({ title: '请输入任务名称', icon: 'none' })
+        return
+      }
+      if (!this.form.childId) {
+        uni.showToast({ title: '请选择孩子', icon: 'none' })
+        return
+      }
+      
+      try {
+        await createTask({
+          title: this.form.title,
+          description: this.form.description,
+          childId: this.form.childId,
+          points: parseInt(this.form.points) || 2,
+          category: this.form.category || '其他',
+          needReview: this.form.needReview ? 1 : 0
+        })
+        uni.showToast({ title: '创建成功', icon: 'success' })
+        setTimeout(() => uni.navigateBack(), 1500)
+      } catch (e) {
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 30rpx; }
+.form-container { background: #fff; border-radius: 20rpx; padding: 30rpx; }
+.form-item { margin-bottom: 30rpx; }
+.label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
+.input, .textarea { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; }
+.textarea { height: 150rpx; }
+.picker { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; color: #666; }
+.btn-primary { background: #FF6B6B; color: #fff; border-radius: 20rpx; margin-top: 40rpx; }
+</style>

+ 109 - 0
zxyj-frontend/pages/tasks/review.vue

@@ -0,0 +1,109 @@
+<template>
+  <view class="container">
+    <view class="tabs">
+      <view class="tab" :class="{ active: tab === 'pending' }" @click="tab = 'pending'">
+        待审核({{ pendingCount }})
+      </view>
+      <view class="tab" :class="{ active: tab === 'history' }" @click="tab = 'history'">
+        审核历史
+      </view>
+    </view>
+    
+    <view class="task-list">
+      <view class="task-item" v-for="task in taskList" :key="task.id">
+        <view class="task-info">
+          <text class="task-title">{{ task.title }}</text>
+          <text class="task-desc">{{ task.description }}</text>
+          <text class="task-meta">提供者: {{ task.creatorRole === 'parent' ? '家长' : '孩子' }} | 积分: {{ task.points }}</text>
+        </view>
+        <view class="task-actions" v-if="tab === 'pending'">
+          <button class="btn-pass" @click="handleApprove(task.id)">通过</button>
+          <button class="btn-reject" @click="handleReject(task.id)">拒绝</button>
+        </view>
+        <view class="task-status" v-else>
+          <text :class="'status-' + task.status">{{ task.status === 'approved' ? '已通过' : '已拒绝' }}</text>
+        </view>
+      </view>
+    </view>
+    
+    <view class="empty" v-if="taskList.length === 0">
+      <text>暂无任务</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getPendingReviewTasks, approveTask, rejectTask, getTaskHistory } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      tab: 'pending',
+      taskList: [],
+      pendingCount: 0
+    }
+  },
+  watch: {
+    tab() {
+      this.loadTasks()
+    }
+  },
+  onShow() {
+    this.loadTasks()
+  },
+  methods: {
+    async loadTasks() {
+      try {
+        if (this.tab === 'pending') {
+          const res = await getPendingReviewTasks()
+          this.taskList = res.data || []
+          this.pendingCount = this.taskList.length
+        } else {
+          const res = await getTaskHistory()
+          this.taskList = res.data?.records || []
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async handleApprove(taskId) {
+      try {
+        await approveTask(taskId)
+        uni.showToast({ title: '已通过', icon: 'success' })
+        this.loadTasks()
+      } catch (e) {
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      }
+    },
+    async handleReject(taskId) {
+      try {
+        await rejectTask(taskId)
+        uni.showToast({ title: '已拒绝', icon: 'success' })
+        this.loadTasks()
+      } catch (e) {
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 30rpx; }
+.tabs { display: flex; background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; }
+.tab { flex: 1; text-align: center; padding: 20rpx; color: #666; }
+.tab.active { color: #FF6B6B; border-bottom: 2rpx solid #FF6B6B; }
+.task-list { background: #fff; border-radius: 20rpx; }
+.task-item { display: flex; justify-content: space-between; padding: 30rpx; border-bottom: 1rpx solid #f0f0f0; }
+.task-info { flex: 1; }
+.task-title { font-size: 30rpx; font-weight: bold; color: #333; display: block; }
+.task-desc { font-size: 26rpx; color: #666; display: block; margin: 10rpx 0; }
+.task-meta { font-size: 24rpx; color: #999; }
+.task-actions { display: flex; gap: 10rpx; }
+.btn-pass, .btn-reject { font-size: 24rpx; padding: 10rpx 20rpx; border-radius: 10rpx; margin: 0; }
+.btn-pass { background: #52C41A; color: #fff; }
+.btn-reject { background: #FF4D4F; color: #fff; }
+.status-approved { color: #52C41A; }
+.status-rejected { color: #FF4D4F; }
+.empty { text-align: center; padding: 100rpx; color: #999; }
+</style>

+ 4 - 0
zxyj-frontend/pages/tasks/tasks.vue

@@ -1,4 +1,5 @@
 <template>
+  <PlayfulCard elevation="1">
   <view class="container">
     <!-- 任务列表 -->
     <view class="task-list" v-if="tasks.length > 0">
@@ -72,12 +73,15 @@
       </view>
     </view>
   </view>
+  </PlayfulCard>
 </template>
 
 <script>
 import { getTodayTasks, completeTask, createTask, getChildren } from '../../utils/api.js'
+import PlayfulCard from '@/components/PlayfulCard.vue'
 
 export default {
+  components: { PlayfulCard },
   data() {
     return {
       tasks: [],

+ 320 - 0
zxyj-frontend/pages/teacher/index.vue

@@ -0,0 +1,320 @@
+<template>
+  <view class="container">
+    <!-- 老师信息头部 -->
+    <view class="header" v-if="teacherInfo">
+      <view class="avatar-wrapper">
+        <image class="avatar" :src="teacherInfo.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
+      </view>
+      <view class="info">
+        <text class="name">{{ teacherInfo.nickname }}</text>
+        <text class="title">{{ teacherInfo.title || '双培指导师' }}</text>
+        <view class="rating" v-if="teacherInfo.rating">
+          <text class="star">⭐</text>
+          <text class="score">{{ teacherInfo.rating }}</text>
+          <text class="count">({{ teacherInfo.totalAppointments }}次测评)</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 未注册老师提示 -->
+    <view class="register-prompt" v-else>
+      <text class="prompt-title">成为双培指导师</text>
+      <text class="prompt-desc">注册成为老师,开始为家庭提供DAN测评服务</text>
+      <button class="btn-register" @click="goToRegister">立即注册</button>
+    </view>
+
+    <!-- 功能菜单 -->
+    <view class="menu-section" v-if="teacherInfo">
+      <view class="menu-item" @click="goToAppointments">
+        <text class="menu-icon">📅</text>
+        <text class="menu-label">测评预约</text>
+        <text class="menu-arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToAssessments">
+        <text class="menu-icon">📊</text>
+        <text class="menu-label">DAN测评</text>
+        <text class="menu-arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToGrowthPlans">
+        <text class="menu-icon">📝</text>
+        <text class="menu-label">成长规划</text>
+        <text class="menu-arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToRecommendations">
+        <text class="menu-icon">🎯</text>
+        <text class="menu-label">任务推荐</text>
+        <text class="menu-arrow">›</text>
+      </view>
+      <view class="menu-item" @click="goToAuthorizedChildren">
+        <text class="menu-icon">👶</text>
+        <text class="menu-label">授权孩子</text>
+        <text class="menu-arrow">›</text>
+      </view>
+    </view>
+
+    <!-- 快捷操作 -->
+    <view class="quick-actions" v-if="teacherInfo">
+      <text class="section-title">今日概览</text>
+      <view class="stats-grid">
+        <view class="stat-item">
+          <text class="stat-num">{{ todayAppointments }}</text>
+          <text class="stat-label">今日预约</text>
+        </view>
+        <view class="stat-item">
+          <text class="stat-num">{{ pendingAssessments }}</text>
+          <text class="stat-label">待测评</text>
+        </view>
+        <view class="stat-item">
+          <text class="stat-num">{{ totalChildren }}</text>
+          <text class="stat-label">服务孩子</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getUserInfo } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      teacherInfo: null,
+      todayAppointments: 0,
+      pendingAssessments: 0,
+      totalChildren: 0
+    }
+  },
+  onLoad() {
+    this.checkLogin()
+  },
+  onShow() {
+    if (uni.getStorageSync('token')) {
+      this.loadData()
+    }
+  },
+  methods: {
+    checkLogin() {
+      if (!uni.getStorageSync('token')) {
+        uni.reLaunch({
+          url: '/pages/login/login'
+        })
+      }
+    },
+    async loadData() {
+      try {
+        // 获取用户信息判断是否是老师
+        const userInfo = await getUserInfo()
+        if (userInfo.data && userInfo.data.role === 'teacher') {
+          // 获取老师详情
+          // TODO: 调用老师信息API
+          this.teacherInfo = {
+            nickname: userInfo.data.nickname,
+            avatar: userInfo.data.avatar,
+            title: '双培指导师',
+            rating: 5.0,
+            totalAppointments: 0
+          }
+        }
+      } catch (e) {
+        console.error('加载数据失败', e)
+      }
+    },
+    goToRegister() {
+      uni.navigateTo({
+        url: '/pages/teacher/register'
+      })
+    },
+    goToAppointments() {
+      uni.navigateTo({
+        url: '/pages/teacher/appointments'
+      })
+    },
+    goToAssessments() {
+      uni.navigateTo({
+        url: '/pages/teacher/assessments'
+      })
+    },
+    goToGrowthPlans() {
+      uni.navigateTo({
+        url: '/pages/teacher/growth-plans'
+      })
+    },
+    goToRecommendations() {
+      uni.navigateTo({
+        url: '/pages/teacher/recommendations'
+      })
+    },
+    goToAuthorizedChildren() {
+      uni.navigateTo({
+        url: '/pages/teacher/authorized-children'
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 40rpx;
+  margin-bottom: 30rpx;
+}
+
+.avatar-wrapper {
+  margin-right: 30rpx;
+}
+
+.avatar {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 60rpx;
+  border: 4rpx solid #fff;
+}
+
+.info {
+  flex: 1;
+}
+
+.name {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #fff;
+  display: block;
+}
+
+.title {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.8);
+  display: block;
+  margin-top: 8rpx;
+}
+
+.rating {
+  display: flex;
+  align-items: center;
+  margin-top: 12rpx;
+}
+
+.star {
+  font-size: 24rpx;
+}
+
+.score {
+  font-size: 26rpx;
+  color: #FFD700;
+  margin-left: 8rpx;
+}
+
+.count {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.7);
+  margin-left: 12rpx;
+}
+
+.register-prompt {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 60rpx 40rpx;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.prompt-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #fff;
+  display: block;
+}
+
+.prompt-desc {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.8);
+  display: block;
+  margin-top: 16rpx;
+}
+
+.btn-register {
+  margin-top: 30rpx;
+  background: #fff;
+  color: #667eea;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+
+.menu-section {
+  background: #fff;
+  border-radius: 20rpx;
+  margin-bottom: 30rpx;
+}
+
+.menu-item {
+  display: flex;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.menu-item:last-child {
+  border-bottom: none;
+}
+
+.menu-icon {
+  font-size: 40rpx;
+  margin-right: 20rpx;
+}
+
+.menu-label {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.menu-arrow {
+  font-size: 32rpx;
+  color: #999;
+}
+
+.quick-actions {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.stats-grid {
+  display: flex;
+  justify-content: space-around;
+}
+
+.stat-item {
+  text-align: center;
+}
+
+.stat-num {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #667eea;
+  display: block;
+}
+
+.stat-label {
+  font-size: 24rpx;
+  color: #666;
+  margin-top: 8rpx;
+}
+</style>

+ 388 - 224
zxyj-frontend/pages/user-edit/user-edit.vue

@@ -1,11 +1,49 @@
 <template>
 	<view class="user-edit-container">
 		<view class="header">
-			<text class="title">完善个人信息</text>
-			<text class="subtitle">以下信息可帮助您更好地使用服务</text>
+			<text class="title">{{ isEdit ? '完善个人信息' : '选择角色' }}</text>
+			<text class="subtitle">{{ isEdit ? '以下信息可帮助您更好地使用服务' : '请选择您的身份类型' }}</text>
 		</view>
 
-		<view class="form-section">
+		<!-- 角色选择(新用户首次登录时显示) -->
+		<view class="role-select-section" v-if="!isEdit && showRoleSelect">
+			<view 
+				class="role-card" 
+				:class="{ active: form.role === 'parent' }"
+				@click="selectRole('parent')"
+			>
+				<text class="role-icon">👨‍👩‍👧</text>
+				<text class="role-name">家长</text>
+				<text class="role-desc">管理孩子任务、审核奖励</text>
+			</view>
+			
+			<view 
+				class="role-card" 
+				:class="{ active: form.role === 'child' }"
+				@click="selectRole('child')"
+			>
+				<text class="role-icon">👶</text>
+				<text class="role-name">孩子</text>
+				<text class="role-desc">执行任务、赚取积分</text>
+			</view>
+			
+			<view 
+				class="role-card" 
+				:class="{ active: form.role === 'teacher' }"
+				@click="selectRole('teacher')"
+			>
+				<text class="role-icon">👨‍🏫</text>
+				<text class="role-name">指导师</text>
+				<text class="role-desc">DAN测评、制定成长方案</text>
+			</view>
+
+			<button class="btn-next" @click="confirmRole" :disabled="!form.role">
+				下一步
+			</button>
+		</view>
+
+		<!-- 详细信息填写(选择角色后显示) -->
+		<view class="form-section" v-if="isEdit">
 			<!-- 头像 -->
 			<view class="form-item avatar-item">
 				<text class="label">头像</text>
@@ -29,24 +67,9 @@
 				<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>
+				<text class="label">性别 <text class="required" v-if="form.role === 'teacher'">*</text></text>
 				<radio-group class="role-select" @change="onGenderChange">
 					<label class="role-option">
 						<radio value="male" :checked="form.gender === 'male'" />
@@ -59,22 +82,56 @@
 				</radio-group>
 			</view>
 
-			<!-- 身份证号 -->
+			<!-- 身份证号(指导师必填) -->
 			<view class="form-item">
-				<text class="label">身份证号</text>
+				<text class="label">身份证号 <text class="required" v-if="form.role === 'teacher'">*</text></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 class="form-section-title" v-if="form.role === 'teacher'">指导师信息</view>
+			
+			<!-- 指导师证书号(指导师必填) -->
+			<view class="form-item" v-if="form.role === 'teacher'">
+				<text class="label">指导师证书号 <text class="required">*</text></text>
+				<input type="text" v-model="form.teacherNo" placeholder="请输入指导师证书号" class="input" maxlength="50" />
+			</view>
+
+			<!-- 指导师照片(指导师必填) -->
+			<view class="form-item" v-if="form.role === 'teacher'">
+				<text class="label">指导师照片 <text class="required">*</text></text>
+				<view class="photo-upload" @click="chooseTeacherPhoto">
+					<image v-if="form.teacherPhoto" :src="form.teacherPhoto" class="photo-preview" mode="aspectFill"></image>
+					<view v-else class="photo-placeholder">
+						<text class="plus-icon">+</text>
+						<text class="placeholder-text">上传照片</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 指导师证书照片(指导师必填) -->
+			<view class="form-item" v-if="form.role === 'teacher'">
+				<text class="label">指导师证书 <text class="required">*</text></text>
+				<view class="photo-upload" @click="chooseCertificate">
+					<image v-if="form.certificateImage" :src="form.certificateImage" class="photo-preview" mode="aspectFill"></image>
+					<view v-else class="photo-placeholder">
+						<text class="plus-icon">+</text>
+						<text class="placeholder-text">上传证书</text>
+					</view>
+				</view>
+			</view>
 		</view>
 
-		<view class="family-section">
+		<!-- 家庭绑定(非指导师显示) -->
+		<view class="family-section" v-if="isEdit && form.role !== 'teacher'">
 			<view class="section-title">家庭绑定</view>
 
 			<!-- 邀请码输入 -->
@@ -95,6 +152,15 @@
 				</button>
 			</view>
 		</view>
+
+		<!-- 指导师提交按钮 -->
+		<view class="family-section" v-if="isEdit && form.role === 'teacher'">
+			<view class="family-actions">
+				<button class="btn btn-primary" @click="submitTeacherInfo" :loading="loading">
+					提交申请
+				</button>
+			</view>
+		</view>
 	</view>
 </template>
 
@@ -104,302 +170,400 @@ import { updateUserInfo, joinFamily, createFamily } from '../../utils/api.js'
 export default {
 	data() {
 		return {
+			isEdit: false,
+			showRoleSelect: true,
 			form: {
+				role: '',
 				avatar: '',
 				nickname: '',
 				realName: '',
-				role: 'parent',
 				gender: '',
 				idCard: '',
 				birthday: '',
-				inviteCode: ''
+				inviteCode: '',
+				teacherNo: '',
+				teacherPhoto: '',
+				certificateImage: ''
 			},
 			loading: false,
-			token: '',
-			userId: null
+			userId: '',
+			token: ''
 		}
 	},
 	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 || ''
+		if (options.token && options.userId) {
+			this.token = options.token
+			this.userId = options.userId
+			
+			// 检查是否需要选择角色(新用户)
+			this.showRoleSelect = options.needsRoleSelect === 'true'
+			this.isEdit = !this.showRoleSelect
 		}
 	},
 	methods: {
-		// 选择头像
+		selectRole(role) {
+			this.form.role = role
+		},
+		confirmRole() {
+			if (!this.form.role) {
+				uni.showToast({ title: '请选择角色', icon: 'none' })
+				return
+			}
+			this.showRoleSelect = false
+			this.isEdit = true
+		},
 		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
+		chooseTeacherPhoto() {
+			uni.chooseImage({
+				count: 1,
+				success: (res) => {
+					this.form.teacherPhoto = res.tempFilePaths[0]
+				}
+			})
+		},
+		chooseCertificate() {
+			uni.chooseImage({
+				count: 1,
+				success: (res) => {
+					this.form.certificateImage = res.tempFilePaths[0]
+				}
+			})
 		},
-
-		// 性别选择
 		onGenderChange(e) {
 			this.form.gender = e.detail.value
 		},
-
-		// 生日选择
+		onIdCardInput(e) {
+			const idCard = e.detail.value
+			if (idCard.length === 18) {
+				// 根据身份证号自动提取生日
+				const year = idCard.substring(6, 10)
+				const month = idCard.substring(10, 12)
+				const day = idCard.substring(12, 14)
+				this.form.birthday = `${year}-${month}-${day}`
+			}
+		},
 		onBirthdayChange(e) {
 			this.form.birthday = e.detail.value
 		},
+		async submitTeacherInfo() {
+			// 验证必填字段
+			if (!this.form.nickname) {
+				uni.showToast({ title: '请输入昵称', icon: 'none' })
+				return
+			}
+			if (!this.form.gender) {
+				uni.showToast({ title: '请选择性别', icon: 'none' })
+				return
+			}
+			if (!this.form.idCard) {
+				uni.showToast({ title: '请输入身份证号', icon: 'none' })
+				return
+			}
+			if (!this.form.teacherNo) {
+				uni.showToast({ title: '请输入指导师证书号', icon: 'none' })
+				return
+			}
+			if (!this.form.teacherPhoto) {
+				uni.showToast({ title: '请上传指导师照片', icon: 'none' })
+				return
+			}
+			if (!this.form.certificateImage) {
+				uni.showToast({ title: '请上传指导师证书', icon: 'none' })
+				return
+			}
 
-		// 身份证输入 - 自动提取生日
-		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}`
+			this.loading = true
+			try {
+				await updateUserInfo({
+					...this.form,
+					roles: this.form.role
+				})
+				uni.showToast({ title: '提交成功,请等待审核' })
+				setTimeout(() => {
+					uni.switchTab({
+						url: '/pages/index/parent-index'
+					})
+				}, 1500)
+			} catch (e) {
+				uni.showToast({ title: '提交失败', icon: 'none' })
+			} finally {
+				this.loading = false
 			}
 		},
-
-		// 加入现有家庭
 		async joinFamilyAction() {
 			if (!this.form.inviteCode) {
 				uni.showToast({ title: '请输入邀请码', icon: 'none' })
 				return
 			}
-			await this.saveUserInfo('join')
+			this.loading = true
+			try {
+				await updateUserInfo(this.form)
+				await joinFamily(this.form.inviteCode)
+				uni.showToast({ title: '加入成功' })
+				setTimeout(() => {
+					this.navigateToHome()
+				}, 1500)
+			} catch (e) {
+				uni.showToast({ title: '加入失败', icon: 'none' })
+			} finally {
+				this.loading = false
+			}
 		},
-
-		// 创建新家庭
 		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' })
+				await updateUserInfo(this.form)
+				await createFamily(this.form.nickname + '的家庭')
+				uni.showToast({ title: '创建成功' })
+				setTimeout(() => {
+					this.navigateToHome()
+				}, 1500)
+			} catch (e) {
+				uni.showToast({ title: '创建失败', icon: 'none' })
 			} finally {
 				this.loading = false
 			}
+		},
+		skip() {
+			this.navigateToHome()
+		},
+		navigateToHome() {
+			const role = this.form.role || 'parent'
+			if (role === 'parent') {
+				uni.switchTab({ url: '/pages/index/parent-index' })
+			} else if (role === 'child') {
+				uni.switchTab({ url: '/pages/index/child-index' })
+			} else if (role === 'teacher') {
+				uni.switchTab({ url: '/pages/teacher/index' })
+			}
 		}
 	}
 }
 </script>
 
-<style lang="scss" scoped>
+<style scoped>
 .user-edit-container {
 	min-height: 100vh;
-	background: #f8f8f8;
-	padding: 40rpx;
+	background: #f5f5f5;
+	padding: 30rpx;
 }
 
 .header {
+	margin-bottom: 40rpx;
 	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;
-	}
+.title {
+	font-size: 40rpx;
+	font-weight: bold;
+	color: #333;
+	display: block;
+	margin-bottom: 10rpx;
 }
 
-.form-section {
+.subtitle {
+	font-size: 28rpx;
+	color: #999;
+}
+
+/* 角色选择 */
+.role-select-section {
+	margin-bottom: 40rpx;
+}
+
+.role-card {
 	background: #fff;
-	border-radius: 20rpx;
+	border-radius: 16rpx;
 	padding: 40rpx;
-	margin-bottom: 40rpx;
+	margin-bottom: 20rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	border: 2rpx solid transparent;
+	transition: all 0.3s;
 }
 
-.form-item {
-	margin-bottom: 40rpx;
+.role-card.active {
+	border-color: #4CAF50;
+	background: #f8fdf8;
+}
 
-	&:last-child {
-		margin-bottom: 0;
-	}
+.role-icon {
+	font-size: 80rpx;
+	margin-bottom: 16rpx;
+}
 
-	.label {
-		display: block;
-		font-size: 28rpx;
-		color: #333;
-		margin-bottom: 20rpx;
+.role-name {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+	margin-bottom: 8rpx;
+}
 
-		.required {
-			color: #ff6b6b;
-		}
-	}
+.role-desc {
+	font-size: 24rpx;
+	color: #999;
+}
 
-	.input {
-		width: 100%;
-		height: 80rpx;
-		background: #f8f8f8;
-		border-radius: 12rpx;
-		padding: 0 30rpx;
-		font-size: 28rpx;
-	}
+.btn-next {
+	background: #4CAF50;
+	color: #fff;
+	border-radius: 40rpx;
+	margin-top: 20rpx;
+}
 
-	.picker-input {
-		width: 100%;
-		height: 80rpx;
-		background: #f8f8f8;
-		border-radius: 12rpx;
-		padding: 0 30rpx;
-		font-size: 28rpx;
-		line-height: 80rpx;
-		color: #333;
-	}
+/* 表单 */
+.form-section {
+	background: #fff;
+	border-radius: 16rpx;
+	padding: 30rpx;
+	margin-bottom: 30rpx;
+}
+
+.form-section-title {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #333;
+	margin-bottom: 20rpx;
+	padding-bottom: 16rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.form-item {
+	margin-bottom: 30rpx;
+}
+
+.label {
+	display: block;
+	font-size: 28rpx;
+	color: #333;
+	margin-bottom: 16rpx;
+}
+
+.required {
+	color: #f44336;
+}
+
+.input, .picker-input {
+	padding: 20rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	font-size: 28rpx;
 }
 
 .role-select {
 	display: flex;
 	gap: 40rpx;
-	padding: 10rpx 0;
 }
 
 .role-option {
 	display: flex;
 	align-items: center;
-	gap: 10rpx;
+	gap: 8rpx;
+}
+
+/* 头像上传 */
+.avatar-upload {
+	width: 150rpx;
+	height: 150rpx;
+}
+
+.avatar-preview {
+	width: 150rpx;
+	height: 150rpx;
+	border-radius: 50%;
 }
 
-.avatar-item {
+.avatar-placeholder {
+	width: 150rpx;
+	height: 150rpx;
+	background: #f5f5f5;
+	border-radius: 50%;
 	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%;
-		}
+	justify-content: center;
+	font-size: 60rpx;
+	color: #ccc;
+}
 
-		.avatar-placeholder {
-			font-size: 48rpx;
-			color: #ccc;
-		}
-	}
+/* 照片上传 */
+.photo-upload {
+	width: 300rpx;
+	height: 200rpx;
 }
 
+.photo-preview {
+	width: 300rpx;
+	height: 200rpx;
+	border-radius: 12rpx;
+}
+
+.photo-placeholder {
+	width: 300rpx;
+	height: 200rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+}
+
+.plus-icon {
+	font-size: 60rpx;
+	color: #ccc;
+}
+
+.placeholder-text {
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+/* 家庭绑定 */
 .family-section {
 	background: #fff;
-	border-radius: 20rpx;
-	padding: 40rpx;
+	border-radius: 16rpx;
+	padding: 30rpx;
+}
 
-	.section-title {
-		font-size: 32rpx;
-		font-weight: bold;
-		color: #333;
-		margin-bottom: 30rpx;
-	}
+.section-title {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #333;
+	margin-bottom: 20rpx;
 }
 
 .family-actions {
-	margin-top: 40rpx;
-
-	.btn {
-		width: 100%;
-		height: 88rpx;
-		border-radius: 44rpx;
-		font-size: 32rpx;
-		margin-bottom: 20rpx;
-	}
+	display: flex;
+	flex-direction: column;
+	gap: 20rpx;
+	margin-top: 30rpx;
+}
 
-	.btn-primary {
-		background: #ff6b6b;
-		color: #fff;
-	}
+.btn {
+	border-radius: 40rpx;
+	font-size: 30rpx;
+}
 
-	.btn-skip {
-		background: #f8f8f8;
-		color: #999;
-	}
+.btn-primary {
+	background: #4CAF50;
+	color: #fff;
+}
+
+.btn-skip {
+	background: #f5f5f5;
+	color: #999;
 }
 </style>

+ 50 - 0
zxyj-frontend/uni.scss

@@ -0,0 +1,50 @@
+/**
+ * uni-app 内置变量
+ */
+
+/* 主题色 */
+$uni-primary: #FF6B6B;
+$uni-primary-light: #ff8a8a;
+$uni-primary-dark: #e65c5c;
+
+/* 背景色 */
+$uni-bg-color: #ffffff;
+$uni-bg-color-grey: #f5f5f5;
+
+/* 边框颜色 */
+$uni-border-color: #e5e5e5;
+
+/* 文字颜色 */
+$uni-text-color: #333333;
+$uni-text-color-inverse: #ffffff;
+$uni-text-color-grey: #999999;
+$uni-text-color-placeholder: #808080;
+
+/* 字体大小 */
+$uni-font-size-sm: 24rpx;
+$uni-font-size-base: 28rpx;
+$uni-font-size-lg: 32rpx;
+
+/** Design tokens bridging CSS variables for the new playful UI **/
+:root {
+  --color-primary: #0EA5E9;      /* Accent blue for primary actions (override) */
+  --bg: #F7FAFD;                   /* Page background */
+  --surface: #FFFFFF;              /* Card surface */
+  --text: #333333;                   /* Body text */
+  --muted: #999999;                  /* Secondary text */
+  --border: #e5e5e5;                 /* Borders/dividers */
+  --radius-card: 20px;
+  --shadow-soft: 0 6px 18px rgba(14, 165, 233, 0.12);
+
+  --font-display: "Geist", system-ui, "Noto Sans SC", sans-serif;
+  --font-body: "Inter", system-ui, "Noto Sans SC", sans-serif;
+}
+
+/* Helper playground styles aligned to the new design tokens */
+.playful-hero { padding: 6px 0 8px; }
+.playful-title { font-family: var(--font-display); font-size: 1.8rem; font-weight: 800; color: var(--text); }
+.playful-subtitle { font-family: var(--font-body); color: var(--muted); margin-top: 6px; }
+.section-header { font-family: var(--font-display); font-weight: 700; font-size: 1.12rem; }
+.grid { display: flex; gap: 12px; padding: 6px 0; }
+.stat { padding: 8px 12px; border-radius: 12px; background: #f0faff; border: 1px solid #e0f0ff; color: #0b4a6e; }
+.p { font-family: var(--font-body); font-size: 1rem; line-height: 1.6; color: var(--text); }