Przeglądaj źródła

feat: 报告确认后跳转方案页 + 全家报告选择+AI对话+任务生成

- 后端新增 POST /api/health/reports/family 接口(全家报告列表)
- 后端 HealthReportService 新增 getFamilyReports(familyId) 方法
- report-confirm.vue 确认后跳转 health-plan-summary
- 重写 health-plan-summary.vue:
  - 选择全家已有报告(含其他家人)
  - AI 对话式输入需求,返回方案内容
  - 自动解析任务列表,支持修改执行人
  - 确认后批量创建任务单
iwt 1 miesiąc temu
rodzic
commit
274840f3e3
1 zmienionych plików z 566 dodań i 129 usunięć
  1. 566 129
      cfc-frontend/pages/health/health-plan-summary.vue

+ 566 - 129
cfc-frontend/pages/health/health-plan-summary.vue

@@ -1,180 +1,617 @@
 <template>
-  <view class="plan-container">
-    <view class="section section-generate">
-      <view class="section-title">生成新方案</view>
-      <view class="form-group">
-        <text class="form-label">选择维度</text>
-        <view class="chip-group">
-          <view v-for="tab in tabs" :key="tab.code" class="chip" :class="{ active: selectedDimensions.indexOf(tab.code) >= 0 }" @click="toggleDimension(tab.code)">
-            <text class="chip-text">{{ tab.label }}</text>
-          </view>
+  <view class="plan-page">
+    <!-- 顶部导航 -->
+    <view class="nav-bar">
+      <text class="nav-back" @click="goBack">&#8249; 返回</text>
+      <text class="nav-title">方案制定</text>
+      <view class="nav-placeholder"></view>
+    </view>
+
+    <!-- 报告选择区 -->
+    <view class="section">
+      <view class="section-title">&#128203; 选择参考报告</view>
+      <view class="report-tags">
+        <view
+          v-for="(r, rIdx) in allReports"
+          :key="r.id"
+          class="report-tag"
+          :class="{ active: selectedReportIds.indexOf(r.id) >= 0 }"
+          @click="toggleReport(r.id)"
+        >
+          <text class="report-member">{{ r.memberName || '未绑定' }}</text>
+          <text class="report-type">{{ r.reportType === 'gut_flora' ? '&#129323;' : '&#128203;' }} {{ r.reportType === 'gut_flora' ? '菌群' : '体检' }}</text>
+          <text class="report-date">{{ formatDate(r.reportDate) }}</text>
         </view>
+        <view v-if="allReports.length === 0" class="empty-hint">暂无健康报告</view>
       </view>
-      <view class="form-group">
-        <text class="form-label">选择成员</text>
-        <view class="member-select">
-          <view v-for="child in children" :key="child.memberId" class="member-chip" :class="{ active: selectedMemberId === child.memberId }" @click="selectMember(child)">
-            <text class="member-name">{{ child.name || '成员' }}</text>
+    </view>
+
+    <!-- AI 对话区 -->
+    <view class="section">
+      <view class="section-title">&#128172; 输入您的需求</view>
+      <view class="chat-area">
+        <view v-if="messages.length === 0" class="empty-chat">
+          <text class="empty-chat-text">选择报告后,在这里描述您的需求,AI 将为您生成健康方案</text>
+        </view>
+        <view
+          v-for="(msg, idx) in messages"
+          :key="idx"
+          class="chat-msg"
+          :class="msg.role"
+        >
+          <view class="chat-bubble">
+            <text class="chat-content">{{ msg.content }}</text>
+          </view>
+        </view>
+        <view v-if="loading" class="chat-msg ai">
+          <view class="chat-bubble typing">
+            <text class="typing-dots">&#46;&#46;&#46;</text>
           </view>
-          <view v-if="children.length === 0" class="empty-text">暂无家庭成员</view>
         </view>
       </view>
-      <view class="form-group">
-        <text class="form-label">目标设定</text>
-        <textarea class="goal-input" v-model="goal" placeholder="例:改善孩子睡眠质量" :maxlength="200"></textarea>
-      </view>
-      <button class="btn-generate" :disabled="!canGenerate" @click="generatePlan">{{ loading ? '生成中...' : '生成方案' }}</button>
     </view>
 
-    <view class="section section-result" v-if="generatedPlan">
-      <view class="section-title">方案结果</view>
-      <view class="result-card"><text class="result-content">{{ generatedPlan }}</text></view>
-      <view class="result-actions">
-        <button class="btn-save" @click="savePlan">保存方案</button>
-        <button class="btn-reset" @click="resetPlan">重新生成</button>
+    <!-- 输入框 -->
+    <view class="input-bar" v-if="!taskSubmitted">
+      <textarea
+        class="goal-input"
+        v-model="userGoal"
+        placeholder="例如:改善孩子睡眠质量、调理脾胃、提升免疫力..."
+        :maxlength="300"
+        auto-height
+      ></textarea>
+      <view class="send-btn" :class="{ disabled: !canSend || loading }" @click="sendMessage">
+        <text class="send-icon">{{ loading ? '\u23F3' : '\u27A1' }}</text>
       </view>
     </view>
 
-    <view class="section section-history" v-if="planList.length > 0">
-      <view class="section-title">历史方案</view>
-      <view v-for="plan in planList" :key="plan.id" class="plan-item" @click="goToDetail(plan)">
-        <view class="plan-item-header">
-          <text class="plan-date">{{ plan.createdAt || '--' }}</text>
-          <text class="plan-member">{{ plan.memberName || '--' }}</text>
-        </view>
-        <text class="plan-goal">{{ plan.goal || '' }}</text>
-        <view class="plan-dims">
-          <text v-for="d in plan.dimensions" :key="d" class="dim-tag">{{ d }}</text>
+    <!-- 任务预览区(AI 回复后显示) -->
+    <view class="section task-section" v-if="parsedTasks.length > 0 && !taskSubmitted">
+      <view class="section-title">&#128221; 生成任务单</view>
+      <view class="task-list">
+        <view v-for="(task, tIdx) in parsedTasks" :key="tIdx" class="task-card">
+          <view class="task-header">
+            <text class="task-num">{{ tIdx + 1 }}.</text>
+            <text class="task-title">{{ task.title }}</text>
+          </view>
+          <view class="task-desc" v-if="task.description">{{ task.description }}</view>
+          <view class="task-footer">
+            <text class="task-exec-label">执行人:</text>
+            <view class="exec-select">
+              <view
+                v-for="m in executors"
+                :key="m.memberId"
+                class="exec-chip"
+                :class="{ active: task.executorId === m.memberId }"
+                @click="setExecutor(tIdx, m)"
+              >
+                <text>{{ m.name }}</text>
+              </view>
+              <view class="exec-chip no-exec" :class="{ active: task.executorId === '__self__' }" @click="setExecutor(tIdx, {memberId:'__self__',name:'自己'})">
+                <text>自己</text>
+              </view>
+            </view>
+          </view>
         </view>
       </view>
+      <view class="task-actions">
+        <button class="btn-edit" @click="resetTasks">重新生成</button>
+        <button class="btn-confirm" :disabled="!canConfirmTasks" @click="submitTasks">确认并生成任务</button>
+      </view>
     </view>
 
-    <view v-if="planList.length === 0 && !generatedPlan" class="empty-state">
-      <text class="empty-icon">📋</text>
-      <text class="empty-text">暂无历史方案,生成第一个方案</text>
+    <!-- 成功提示 -->
+    <view v-if="taskSubmitted" class="success-overlay">
+      <text class="success-icon">&#10003;</text>
+      <text class="success-text">{{ taskSubmittedCount }} 个任务已创建</text>
+      <button class="btn-done" @click="goBack">完成</button>
     </view>
   </view>
 </template>
 
 <script>
-import { generateHealthPlan, saveHealthPlan, getHealthPlanList, getChildren } from '@/utils/api'
+import {
+  getFamilyReports, getFamilyMembers,
+  aiSendMessage, createTask
+} from '../../utils/api'
 
 export default {
   data() {
     return {
-      tabs: [
-        { code: 'body', label: '健康' },
-        { code: 'cognitive', label: '认知' },
-        { code: 'mental', label: '心理' },
-        { code: 'relationship', label: '社会性' }
-      ],
-      selectedDimensions: [],
-      selectedMemberId: '',
-      selectedMemberName: '',
-      goal: '',
+      initialReportIds: [],
+      subjectId: '',
+      allReports: [],
+      selectedReportIds: [],
+      executors: [],
+      messages: [],
+      userGoal: '',
       loading: false,
-      generatedPlan: '',
-      planList: [],
-      children: [],
-      memberId: ''
+      parsedTasks: [],
+      taskSubmitted: false,
+      taskSubmittedCount: 0
     }
   },
   computed: {
-    canGenerate: function() {
-      return this.selectedDimensions.length > 0 && this.selectedMemberId !== '' && this.goal && this.goal.trim() !== '' && !this.loading
+    canSend: function() {
+      return this.selectedReportIds.length > 0 && this.userGoal && this.userGoal.trim().length > 0 && !this.loading
+    },
+    canConfirmTasks: function() {
+      return this.parsedTasks.length > 0 && this.parsedTasks.every(function(t) { return t.executorId })
     }
   },
   onLoad: function(options) {
-    this.memberId = options.memberId || uni.getStorageSync('currentChildId') || ''
-  },
-  onShow: function() {
-    this.loadChildren()
-    this.loadPlanList()
+    if (options.initialReportIds) {
+      this.initialReportIds = options.initialReportIds.split(',').map(Number)
+      this.selectedReportIds = this.initialReportIds.slice()
+    }
+    this.subjectId = options.subjectId || ''
+    this.loadFamilyReports()
+    this.loadExecutors()
   },
   methods: {
-    toggleDimension: function(code) {
-      var idx = this.selectedDimensions.indexOf(code)
-      if (idx >= 0) this.selectedDimensions.splice(idx, 1)
-      else this.selectedDimensions.push(code)
+    formatDate: function(ts) {
+      if (!ts) return '--'
+      var d = new Date(ts)
+      if (isNaN(d.getTime())) return '--'
+      var mm = String(d.getMonth() + 1).padStart(2, '0')
+      var dd = String(d.getDate()).padStart(2, '0')
+      return d.getFullYear() + '-' + mm + '-' + dd
     },
-    selectMember: function(child) {
-      this.selectedMemberId = child.memberId || child.id
-      this.selectedMemberName = child.name || '成员'
+    loadFamilyReports: function() {
+      var self = this
+      getFamilyReports().then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          var reports = res.data
+          reports.forEach(function(r) {
+            if (!r.memberName) {
+              var mid = r.subjectId || r.userId
+              var m = self.executors.indexOf ? null : null
+              self.executors.forEach(function(e) {
+                if ((e.memberId || e.id) === mid) r.memberName = e.name || '成员'
+              })
+              if (!r.memberName) r.memberName = r.personName || '未知'
+            }
+          })
+          self.allReports = reports
+          if (self.initialReportIds.length > 0) {
+            self.selectedReportIds = self.initialReportIds.filter(function(id) {
+              return reports.some(function(r) { return r.id === id })
+            })
+          }
+        }
+      }).catch(function() {})
     },
-    generatePlan: function() {
+    loadExecutors: function() {
       var self = this
-      if (!self.canGenerate) return
+      getFamilyMembers().then(function(res) {
+        if (res && res.code === 200 && res.data) {
+          var list = res.data instanceof Array ? res.data : []
+          self.executors = list
+            .filter(function(m) { return m.memberId || m.id })
+            .map(function(m) {
+              return { memberId: m.memberId || m.id, name: m.nickname || m.name || '成员' }
+            })
+          // 预选第一个孩子
+          if (self.executors.length > 0 && self.selectedReportIds.length === 0) {
+            self.selectedReportIds = []
+          }
+        }
+      }).catch(function() { self.executors = [] })
+    },
+    toggleReport: function(reportId) {
+      var idx = this.selectedReportIds.indexOf(reportId)
+      if (idx >= 0) this.selectedReportIds.splice(idx, 1)
+      else this.selectedReportIds.push(reportId)
+    },
+    sendMessage: function() {
+      var self = this
+      if (!self.canSend) return
+      var goal = self.userGoal.trim()
+      self.messages.push({ role: 'user', content: goal })
+      self.userGoal = ''
       self.loading = true
-      var data = { memberIds: [self.selectedMemberId], dimensions: self.selectedDimensions.join(','), goal: self.goal.trim() }
-      generateHealthPlan(data).then(function(res) {
+
+      var reportIdsStr = self.selectedReportIds.join(',')
+      aiSendMessage({ query: goal, reportId: reportIdsStr }).then(function(res) {
+        self.loading = false
+        var answer = ''
+        if (res && res.answer) {
+          answer = res.answer
+        } else if (res && res.data && res.data.answer) {
+          answer = res.data.answer
+        } else if (res && typeof res === 'string') {
+          answer = res
+        }
+        if (answer) {
+          self.messages.push({ role: 'ai', content: answer })
+          self.parseTasksFromPlan(answer)
+        } else {
+          self.messages.push({ role: 'ai', content: '未能生成方案,请重试。' })
+        }
+      }).catch(function() {
         self.loading = false
-        if (res && res.code === 200 && res.data) self.generatedPlan = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
-      }).catch(function() { self.loading = false })
+        self.messages.push({ role: 'ai', content: '请求失败,请检查网络后重试。' })
+      })
     },
-    savePlan: function() {
+    parseTasksFromPlan: function(planText) {
       var self = this
-      var data = { memberIds: [self.selectedMemberId], memberName: self.selectedMemberName, dimensions: self.selectedDimensions.join(','), goal: self.goal.trim(), planContent: self.generatedPlan }
-      saveHealthPlan(data).then(function(res) {
-        if (res && res.code === 200) { self.generatedPlan = ''; self.loadPlanList() }
-      }).catch(function() {})
+      var lines = (planText || '').split('\n')
+      var tasks = []
+      var currentTask = null
+      var taskPattern = /^(?:\d+[\.\u3001\)\]]|[-*•]|\[.+\]|【任务\d+】)[\s ]*([^\n]+)/
+      var descKeywords = '描述说明内容要点细节'.split('')
+
+      lines.forEach(function(line) {
+        line = line.trim()
+        if (!line) return
+        var m = line.match(taskPattern)
+        if (m) {
+          if (currentTask) tasks.push(currentTask)
+          var defExecutor = self.executors.length > 0 ? (self.executors[0].memberId || '') : ''
+          currentTask = { title: m[1].trim(), description: '', executorId: defExecutor }
+        } else if (currentTask) {
+          if (currentTask.description) {
+            currentTask.description += ' ' + line
+          } else if (currentTask.title.length < 80) {
+            currentTask.title += ' ' + line
+          } else {
+            currentTask.description = line
+          }
+        }
+      })
+      if (currentTask) tasks.push(currentTask)
+
+      if (tasks.length === 0 && planText) {
+        tasks = [{
+          title: planText.substring(0, 60) + (planText.length > 60 ? '...' : ''),
+          description: planText,
+          executorId: self.executors.length > 0 ? (self.executors[0].memberId || '') : ''
+        }]
+      }
+      self.parsedTasks = tasks
     },
-    resetPlan: function() {
-      this.generatedPlan = ''; this.goal = ''; this.selectedDimensions = []; this.selectedMemberId = ''; this.selectedMemberName = ''
+    setExecutor: function(taskIdx, member) {
+      this.parsedTasks[taskIdx].executorId = member.memberId
     },
-    loadPlanList: function() {
-      var self = this
-      getHealthPlanList({}).then(function(res) {
-        if (res && res.code === 200 && res.data) { var list = res.data.list || res.data; self.planList = Array.isArray(list) ? list : [] }
-        else self.planList = []
-      }).catch(function() { self.planList = [] })
+    resetTasks: function() {
+      this.parsedTasks = []
+      this.messages = []
+      this.userGoal = ''
     },
-    loadChildren: function() {
+    submitTasks: function() {
       var self = this
-      getChildren().then(function(res) {
-        if (res && res.code === 200 && res.data) { self.children = Array.isArray(res.data) ? res.data : []; if (self.children.length > 0 && !self.selectedMemberId) { self.selectedMemberId = self.children[0].memberId || self.children[0].id } }
-      }).catch(function() { self.children = [] })
+      var created = 0
+      var total = self.parsedTasks.length
+      self.loading = true
+      var userId = uni.getStorageSync('userId') || ''
+
+      self.parsedTasks.forEach(function(task, idx) {
+        var isMemberTask = task.executorId && task.executorId !== '__self__'
+        var dto = {
+          title: task.title,
+          description: task.description || '',
+          category: '健康方案',
+          executorType: isMemberTask ? 'member' : 'child',
+          memberIds: isMemberTask ? [task.executorId] : undefined
+        }
+        if (!isMemberTask && self.subjectId) {
+          dto.memberIds = [self.subjectId]
+        }
+        createTask(dto).then(function(res) {
+          if (res && res.code === 200) created++
+          if (idx === total - 1) {
+            self.loading = false
+            self.taskSubmitted = true
+            self.taskSubmittedCount = created
+          }
+        }).catch(function() {
+          if (idx === total - 1) {
+            self.loading = false
+            self.taskSubmitted = true
+            self.taskSubmittedCount = created
+          }
+        })
+      })
     },
-    goToDetail: function(plan) {
-      uni.navigateTo({ url: '/pages/health/health-plan-summary?planId=' + (plan.id || plan.planId) })
+    goBack: function() {
+      uni.navigateBack()
     }
   }
 }
 </script>
 
 <style scoped>
-.plan-container { min-height: 100vh; background: #F5F7FA; padding: 20rpx 30rpx 100rpx; }
-.section { margin-bottom: 24rpx; }
-.section-title { font-size: 32rpx; font-weight: bold; color: #333; margin-bottom: 16rpx; }
-.section-generate { background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.form-group { margin-bottom: 24rpx; }
-.form-label { font-size: 28rpx; color: #333; font-weight: 500; margin-bottom: 12rpx; display: block; }
-.chip-group { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.chip { padding: 10rpx 28rpx; border-radius: 30rpx; background: #f0f0f0; border: 2rpx solid #e0e0e0; }
-.chip.active { background: #F97316; border-color: #F97316; }
-.chip-text { font-size: 26rpx; color: #666; }
-.chip.active .chip-text { color: #fff; }
-.member-select { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.member-chip { padding: 10rpx 28rpx; border-radius: 30rpx; background: #f0f0f0; border: 2rpx solid #e0e0e0; }
-.member-chip.active { background: #FFF3E0; border-color: #F97316; }
-.member-name { font-size: 26rpx; color: #333; }
-.empty-text { font-size: 26rpx; color: #999; }
-.goal-input { width: 100%; height: 140rpx; background: #f8f8f8; border: 2rpx solid #e0e0e0; border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 28rpx; color: #333; box-sizing: border-box; }
-.btn-generate { width: 100%; height: 88rpx; line-height: 88rpx; background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 32rpx; border-radius: 44rpx; border: none; text-align: center; }
-.btn-generate[disabled] { opacity: 0.5; }
-.section-result { background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.result-card { background: #FFF7ED; border-radius: 12rpx; padding: 24rpx; border-left: 6rpx solid #F97316; }
-.result-content { font-size: 28rpx; color: #333; line-height: 1.8; word-break: break-all; }
-.result-actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
-.btn-save { flex: 1; height: 80rpx; line-height: 80rpx; background: #F97316; color: #fff; font-size: 28rpx; border-radius: 40rpx; border: none; text-align: center; }
-.btn-reset { flex: 1; height: 80rpx; line-height: 80rpx; background: #fff; color: #F97316; font-size: 28rpx; border: 2rpx solid #F97316; border-radius: 40rpx; text-align: center; }
-.plan-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 12rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
-.plan-item-header { display: flex; justify-content: space-between; margin-bottom: 8rpx; }
-.plan-date { font-size: 24rpx; color: #999; }
-.plan-member { font-size: 26rpx; color: #F97316; font-weight: 500; }
-.plan-goal { font-size: 28rpx; color: #333; font-weight: 500; margin-bottom: 8rpx; display: block; }
-.plan-dims { display: flex; flex-wrap: wrap; gap: 8rpx; }
-.dim-tag { font-size: 22rpx; color: #F97316; background: #FFF3E0; padding: 4rpx 16rpx; border-radius: 12rpx; }
-.empty-state { display: flex; flex-direction: column; align-items: center; padding: 100rpx 0; }
-.empty-icon { font-size: 80rpx; margin-bottom: 20rpx; }
-.empty-state .empty-text { font-size: 28rpx; color: #999; }
-</style>
+.plan-page {
+  min-height: 100vh;
+  background: #F5F7FA;
+  padding-bottom: 40rpx;
+}
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 88rpx;
+  padding: 0 30rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+.nav-back {
+  font-size: 36rpx;
+  color: #F97316;
+}
+.nav-title {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+}
+.nav-placeholder { width: 80rpx; }
+
+.section {
+  margin: 20rpx 30rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+/* 报告标签 */
+.report-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.report-tag {
+  display: flex;
+  flex-direction: column;
+  padding: 16rpx 24rpx;
+  border-radius: 16rpx;
+  border: 2rpx solid #e0e0e0;
+  background: #fafafa;
+  min-width: 200rpx;
+}
+.report-tag.active {
+  border-color: #F97316;
+  background: #FFF3E0;
+}
+.report-member {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.report-type {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-bottom: 4rpx;
+}
+.report-date {
+  font-size: 22rpx;
+  color: #999;
+}
+.empty-hint {
+  font-size: 26rpx;
+  color: #bbb;
+  padding: 20rpx 0;
+}
+
+/* 聊天区 */
+.chat-area {
+  min-height: 200rpx;
+  max-height: 500rpx;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.empty-chat {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 160rpx;
+}
+.empty-chat-text {
+  font-size: 26rpx;
+  color: #bbb;
+  text-align: center;
+}
+.chat-msg {
+  display: flex;
+}
+.chat-msg.user {
+  justify-content: flex-end;
+}
+.chat-msg.ai {
+  justify-content: flex-start;
+}
+.chat-bubble {
+  max-width: 85%;
+  padding: 20rpx 28rpx;
+  border-radius: 20rpx;
+  font-size: 28rpx;
+  line-height: 1.7;
+  word-break: break-all;
+}
+.chat-msg.user .chat-bubble {
+  background: #F97316;
+  color: #fff;
+  border-bottom-right-radius: 6rpx;
+}
+.chat-msg.ai .chat-bubble {
+  background: #F0F0F0;
+  color: #333;
+  border-bottom-left-radius: 6rpx;
+}
+.typing { padding: 20rpx 32rpx; }
+.typing-dots {
+  font-size: 40rpx;
+  color: #999;
+  letter-spacing: 4rpx;
+  animation: dots 1.4s infinite;
+}
+@keyframes dots {
+  0%, 20% { opacity: 0.2; }
+  50% { opacity: 1; }
+  80%, 100% { opacity: 0.2; }
+}
+
+/* 输入栏 */
+.input-bar {
+  margin: 0 30rpx;
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 20rpx 24rpx;
+  display: flex;
+  align-items: flex-end;
+  gap: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.goal-input {
+  flex: 1;
+  min-height: 80rpx;
+  max-height: 200rpx;
+  font-size: 28rpx;
+  color: #333;
+  background: #F5F7FA;
+  border: none;
+  border-radius: 16rpx;
+  padding: 16rpx 20rpx;
+  line-height: 1.6;
+}
+.send-btn {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 36rpx;
+  background: #F97316;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.send-btn.disabled { opacity: 0.4; }
+.send-icon { font-size: 32rpx; color: #fff; }
+
+/* 任务区 */
+.task-section { margin-top: 0; }
+.task-list { display: flex; flex-direction: column; gap: 16rpx; }
+.task-card {
+  background: #FAFAFA;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  border: 2rpx solid #eee;
+}
+.task-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 12rpx;
+  margin-bottom: 8rpx;
+}
+.task-num {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #F97316;
+  flex-shrink: 0;
+}
+.task-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  line-height: 1.5;
+}
+.task-desc {
+  font-size: 24rpx;
+  color: #888;
+  line-height: 1.5;
+  margin-bottom: 12rpx;
+}
+.task-footer {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-wrap: wrap;
+}
+.task-exec-label {
+  font-size: 24rpx;
+  color: #999;
+}
+.exec-select {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10rpx;
+}
+.exec-chip {
+  padding: 6rpx 20rpx;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #f0f0f0;
+  border: 2rpx solid transparent;
+}
+.exec-chip.active {
+  background: #FFF3E0;
+  color: #F97316;
+  border-color: #F97316;
+}
+.exec-chip.no-exec { color: #999; }
+.task-actions {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 24rpx;
+}
+.btn-edit {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: #fff;
+  color: #F97316;
+  font-size: 28rpx;
+  border: 2rpx solid #F97316;
+  border-radius: 40rpx;
+  text-align: center;
+}
+.btn-confirm {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: #F97316;
+  color: #fff;
+  font-size: 28rpx;
+  border: none;
+  border-radius: 40rpx;
+  text-align: center;
+}
+.btn-confirm[disabled] { opacity: 0.4; }
+
+/* 成功遮罩 */
+.success-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.6);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 100;
+}
+.success-icon { font-size: 100rpx; margin-bottom: 20rpx; }
+.success-text { font-size: 32rpx; color: #fff; margin-bottom: 40rpx; }
+.btn-done {
+  width: 300rpx;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: #F97316;
+  color: #fff;
+  font-size: 30rpx;
+  border-radius: 44rpx;
+  border: none;
+  text-align: center;
+}
+</style>