Parcourir la source

Merge remote-tracking branch 'origin/cfclub' into cfclub

# Conflicts:
#	cfc-web/src/views/admin/AssessmentPlanManage.vue
#	cfc-web/src/views/admin/HealthPlanManage.vue
jiapu il y a 3 semaines
Parent
commit
971671d24c

+ 4 - 1
cfc-frontend/pages/health/health-plan-summary.vue

@@ -932,6 +932,9 @@ selectedMembers: function() {
         self.loading = false
         if (res && res.code === 200 && res.data) {
           self.sections[key].content = res.data.content || res.data
+          if (res.data.tasks) {
+            self.sections[key].tasks = res.data.tasks
+          }
           uni.showToast({ title: '生成成功', icon: 'success' })
         } else {
           uni.showToast({ title: res && res.message ? res.message : '生成失败', icon: 'none' })
@@ -965,7 +968,7 @@ selectedMembers: function() {
       var planJson = JSON.stringify({
         overview: self.planData.overview || '',
         sections: self.sectionKeys.map(function(key) {
-          return { key: key, content: self.sections[key].content, items: self.sections[key].items || [] }
+          return { key: key, content: self.sections[key].content, items: self.sections[key].items || [], tasks: self.sections[key].tasks || [] }
         })
       })
       var saveData = {

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-a1add80b794e96ef5dd730f35ab493943943f908
+a9a541165c26652d72b07d15e0ec22117de4efd7

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1231",
+  "version": "1.0.1233",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1231",
+      "version": "1.0.1233",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1232",
+  "version": "1.0.1234",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 29 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,35 @@
 
 ---
 
+## v1.0.1234 (2026-08-28)
+
+### 新功能
+- 规划师端健康方案管理页 + v1.0.1233 版本升级
+
+
+## v1.0.1233 (2026-08-28)
+
+### 文档
+- 登记生成方案标准内容格式实现计划(PROJECT-OVERVIEW 🟢 已实施)
+
+### Bug 修复
+- 健康方案 submitPlan 补 tasks 字段 + regenerate 更新 tasks
+- 海报扫码进入文章/活动详情页解析 scene 参数,修复参数错误
+- 首页自检卡跳转改为五维自检页 self-check
+
+### 新功能
+- 规划师端健康方案结构化任务条目编辑
+- 支持分享到朋友圈(onShareTimeline)
+
+### 其他
+- 优化修复
+- 优化修复
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- 优化修复
+- 优化修复
+
+
 ## v1.0.1232 (2026-08-28)
 
 ### Bug 修复

+ 30 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1232
+> 当前版本: v1.0.1234
 
 ## 历史版本
 
@@ -8,6 +8,35 @@
 
 ---
 
+## v1.0.1234 (2026-08-28)
+
+### 新功能
+- 规划师端健康方案管理页 + v1.0.1233 版本升级
+
+
+## v1.0.1233 (2026-08-28)
+
+### 文档
+- 登记生成方案标准内容格式实现计划(PROJECT-OVERVIEW 🟢 已实施)
+
+### Bug 修复
+- 健康方案 submitPlan 补 tasks 字段 + regenerate 更新 tasks
+- 海报扫码进入文章/活动详情页解析 scene 参数,修复参数错误
+- 首页自检卡跳转改为五维自检页 self-check
+
+### 新功能
+- 规划师端健康方案结构化任务条目编辑
+- 支持分享到朋友圈(onShareTimeline)
+
+### 其他
+- 优化修复
+- 优化修复
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- 优化修复
+- 优化修复
+
+
 ## v1.0.1232 (2026-08-28)
 
 ### Bug 修复

+ 17 - 7
cfc-web/src/views/admin/AssessmentPlanManage.vue

@@ -1,11 +1,15 @@
 <template>
-  <div class="assessment-plan-manage admin-page">
-    <el-card>
-      <div slot="header" class="admin-page-header">
-        <span class="admin-page-title">测评方案管理</span>
-      </div>
-      <el-empty description="功能开发中,敬请期待" />
-    </el-card>
+  <div class="assessment-plan-manage">
+    <div class="page-header">
+      <h2>测评方案管理</h2>
+    </div>
+    <el-alert
+      title="该功能开发中"
+      type="info"
+      description="测评方案管理模块正在建设中,敬请期待。"
+      show-icon
+      :closable="false"
+    />
   </div>
 </template>
 
@@ -14,3 +18,9 @@ export default {
   name: 'AssessmentPlanManage'
 }
 </script>
+
+<style scoped>
+.assessment-plan-manage { padding: 20px; }
+.page-header { margin-bottom: 20px; }
+.page-header h2 { margin: 0; font-size: 20px; }
+</style>

+ 168 - 176
cfc-web/src/views/admin/HealthPlanManage.vue

@@ -1,127 +1,106 @@
 <template>
-  <div class="health-plan-manage admin-page">
-    <el-card>
-      <div slot="header" class="admin-page-header">
-        <span class="admin-page-title">健康方案管理</span>
-        <div class="flex items-center gap-sm">
-          <el-input
-            v-model="keyword"
-            placeholder="搜索成员/目标"
-            prefix-icon="el-icon-search"
-            clearable
-            style="width:200px"
-            @keyup.enter.native="handleSearch"
-            @clear="handleSearch"
-          />
-          <el-select v-model="statusFilter" placeholder="状态" clearable style="width:130px" @change="handleSearch">
-            <el-option label="草稿" value="draft" />
-            <el-option label="待审核" value="pending_review" />
-            <el-option label="已发布" value="published" />
-            <el-option label="已驳回" value="rejected" />
-          </el-select>
-          <el-input
-            v-model="familyIdFilter"
-            placeholder="家庭ID"
-            clearable
-            style="width:120px"
-            @keyup.enter.native="handleSearch"
-            @clear="handleSearch"
-          />
-          <el-button type="primary" size="mini" @click="handleSearch">查询</el-button>
-        </div>
+  <div class="health-plan-manage">
+    <div class="page-header">
+      <h2>健康方案管理</h2>
+      <div class="filters">
+        <el-input v-model="keyword" placeholder="搜索成员名/目标" style="width:200px" clearable @keyup.enter="loadPlans" />
+        <el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width:140px;margin-left:10px" @change="loadPlans">
+          <el-option label="草稿" value="draft" />
+          <el-option label="待审核" value="pending_review" />
+          <el-option label="已发布" value="published" />
+          <el-option label="已驳回" value="rejected" />
+        </el-select>
+        <el-button type="primary" @click="loadPlans" style="margin-left:10px">查询</el-button>
       </div>
+    </div>
 
-      <div class="table-scroll-wrap">
-        <el-table :data="list" v-loading="loading" border stripe :max-height="tableHeight">
-          <el-table-column prop="id" label="ID" width="70" />
-          <el-table-column label="家庭" min-width="120" show-overflow-tooltip>
-            <template slot-scope="{ row }">
-              {{ row.familyName || ('家庭' + (row.familyId || '-')) }}
-            </template>
-          </el-table-column>
-          <el-table-column label="成员" width="140" show-overflow-tooltip>
-            <template slot-scope="{ row }">{{ row.memberName || row.memberIds || '-' }}</template>
-          </el-table-column>
-          <el-table-column label="目标" min-width="180" show-overflow-tooltip>
-            <template slot-scope="{ row }">{{ row.goal || '-' }}</template>
-          </el-table-column>
-          <el-table-column label="维度" width="140">
-            <template slot-scope="{ row }">
-              <el-tag
-                v-for="(d, i) in splitDimensions(row.dimensions)"
-                :key="i"
-                size="mini"
-                style="margin-right:4px"
-              >{{ d }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="状态" width="100">
-            <template slot-scope="{ row }">
-              <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
-            </template>
-          </el-table-column>
-          <el-table-column label="创建时间" width="170">
-            <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
-          </el-table-column>
-          <el-table-column label="操作" width="100" fixed="right">
-            <template slot-scope="{ row }">
-              <el-button size="mini" type="text" @click="showDetail(row)">详情</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
+    <el-table :data="plans" border stripe style="width:100%" v-loading="loading" :max-height="tableHeight">
+      <el-table-column prop="id" label="方案ID" width="80" />
+      <el-table-column prop="familyName" label="家庭" width="140">
+        <template slot-scope="{ row }">{{ row.familyName || row.familyId }}</template>
+      </el-table-column>
+      <el-table-column prop="memberName" label="成员" width="120">
+        <template slot-scope="{ row }">{{ row.memberName || row.memberIds || '-' }}</template>
+      </el-table-column>
+      <el-table-column label="维度" width="160">
+        <template slot-scope="{ row }">
+          <el-tag v-for="(d, i) in (row.dimensions || '').split(',')" :key="i" size="mini" style="margin-right:3px">{{ d }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="目标" min-width="200">
+        <template slot-scope="{ row }">
+          {{ row.goal ? (row.goal.length > 50 ? row.goal.substring(0, 50) + '...' : row.goal) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="100">
+        <template slot-scope="{ row }">
+          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="reviewComment" label="审核意见" width="150">
+        <template slot-scope="{ row }">{{ row.reviewComment || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="createdAt" label="创建时间" width="160">
+        <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="100">
+        <template slot-scope="{ row }">
+          <el-button size="mini" type="primary" @click="showDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
 
-      <el-pagination
-        style="margin-top:16px;text-align:right"
-        layout="total, prev, pager, next, sizes"
-        :current-page.sync="page"
-        :page-size.sync="size"
-        :page-sizes="[10, 20, 50]"
-        :total="total"
-        @current-change="loadList"
-        @size-change="handleSizeChange"
-      />
-    </el-card>
+    <el-pagination
+      style="margin-top:16px;text-align:right"
+      background
+      layout="total, prev, pager, next"
+      :total="total"
+      :page-size="pageSize"
+      :current-page="pageNum"
+      @current-change="handlePageChange"
+    />
 
-    <el-dialog title="健康方案详情" :visible.sync="detailVisible" width="720px" destroy-on-close>
-      <div v-loading="detailLoading" v-if="currentPlan">
-        <el-descriptions :column="2" border size="small">
+    <!-- 详情弹窗 -->
+    <el-dialog title="健康方案详情" :visible.sync="detailVisible" width="800px" destroy-on-close>
+      <div v-if="currentPlan" v-loading="detailLoading">
+        <el-descriptions :column="2" border>
           <el-descriptions-item label="方案ID">{{ currentPlan.id }}</el-descriptions-item>
-          <el-descriptions-item label="状态">
-            <el-tag :type="statusType(currentPlan.status)" size="mini">{{ statusLabel(currentPlan.status) }}</el-tag>
-          </el-descriptions-item>
-          <el-descriptions-item label="家庭">
-            {{ currentPlan.familyName || ('家庭' + (currentPlan.familyId || '-')) }}
-            <span v-if="currentPlan.familyId" style="color:#999;margin-left:6px">(#{{ currentPlan.familyId }})</span>
-          </el-descriptions-item>
+          <el-descriptions-item label="家庭">{{ currentPlan.familyName || currentPlan.familyId }}</el-descriptions-item>
           <el-descriptions-item label="成员">{{ currentPlan.memberName || currentPlan.memberIds || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="维度" :span="2">{{ currentPlan.dimensions || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="维度">{{ currentPlan.dimensions || '-' }}</el-descriptions-item>
           <el-descriptions-item label="目标" :span="2">{{ currentPlan.goal || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="创建时间">{{ formatTime(currentPlan.createdAt) }}</el-descriptions-item>
-          <el-descriptions-item label="更新时间">{{ formatTime(currentPlan.updatedAt) }}</el-descriptions-item>
-          <el-descriptions-item v-if="currentPlan.reviewComment" label="审核意见" :span="2">
-            {{ currentPlan.reviewComment }}
+          <el-descriptions-item label="状态">
+            <el-tag :type="statusType(currentPlan.status)">{{ statusLabel(currentPlan.status) }}</el-tag>
           </el-descriptions-item>
+          <el-descriptions-item label="规划师ID">{{ currentPlan.teacherId || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="审核人ID">{{ currentPlan.reviewedBy || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="审核时间">{{ formatTime(currentPlan.reviewedAt) }}</el-descriptions-item>
+          <el-descriptions-item label="审核意见">{{ currentPlan.reviewComment || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间" :span="2">{{ formatTime(currentPlan.createdAt) }}</el-descriptions-item>
         </el-descriptions>
 
-        <div class="detail-section-title">方案内容</div>
-        <pre class="plan-content">{{ currentPlan.planContent || '暂无内容' }}</pre>
+        <el-divider content-position="left">方案内容</el-divider>
+        <div class="plan-content" v-html="renderPlanContent(currentPlan.planContent)"></div>
 
-        <div v-if="currentPlan.tasks && currentPlan.tasks.length" class="detail-section-title">
-          分解任务({{ currentPlan.tasks.length }})
-        </div>
-        <el-table
-          v-if="currentPlan.tasks && currentPlan.tasks.length"
-          :data="currentPlan.tasks"
-          border
-          size="mini"
-          style="margin-top:8px"
-        >
-          <el-table-column prop="id" label="ID" width="70" />
-          <el-table-column prop="title" label="任务" min-width="180" show-overflow-tooltip />
-          <el-table-column prop="status" label="状态" width="100" />
-          <el-table-column label="截止时间" width="160">
-            <template slot-scope="{ row }">{{ formatTime(row.deadline) }}</template>
+        <el-divider content-position="left">分解任务</el-divider>
+        <el-table :data="tasks" border stripe size="small" v-loading="tasksLoading">
+          <el-table-column prop="title" label="任务标题" min-width="180" />
+          <el-table-column prop="actionType" label="类型" width="100">
+            <template slot-scope="{ row }">{{ actionTypeLabel(row.actionType) }}</template>
+          </el-table-column>
+          <el-table-column prop="dimension" label="维度" width="80">
+            <template slot-scope="{ row }">
+              <el-tag size="mini">{{ dimensionLabel(row.dimension) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="frequency" label="频率" width="80">
+            <template slot-scope="{ row }">{{ frequencyLabel(row.frequency) }}</template>
+          </el-table-column>
+          <el-table-column prop="notes" label="备注" width="150" />
+          <el-table-column prop="status" label="状态" width="80">
+            <template slot-scope="{ row }">
+              <el-tag :type="taskStatusType(row.status)" size="mini">{{ taskStatusLabel(row.status) }}</el-tag>
+            </template>
           </el-table-column>
         </el-table>
       </div>
@@ -139,69 +118,41 @@ export default {
   name: 'HealthPlanManage',
   computed: {
     tableHeight() {
-      return window.innerHeight - 320
+      return window.innerHeight - 280
     }
   },
   data() {
     return {
-      loading: false,
-      detailLoading: false,
-      list: [],
+      plans: [],
       total: 0,
-      page: 1,
-      size: 20,
+      pageNum: 1,
+      pageSize: 20,
       keyword: '',
       statusFilter: '',
-      familyIdFilter: '',
+      loading: false,
       detailVisible: false,
-      currentPlan: null
+      currentPlan: null,
+      detailLoading: false,
+      tasks: [],
+      tasksLoading: false
     }
   },
   created() {
-    this.loadList()
+    this.loadPlans()
   },
   methods: {
-    formatTime(t) {
-      return t ? String(t).replace('T', ' ').substring(0, 19) : '-'
-    },
-    splitDimensions(dims) {
-      if (!dims) return []
-      return String(dims).split(',').map(function (s) { return s.trim() }).filter(Boolean)
-    },
-    statusType(s) {
-      var map = { pending_review: 'warning', draft: 'info', published: 'success', rejected: 'danger' }
-      return map[s] || 'info'
-    },
-    statusLabel(s) {
-      var map = { pending_review: '待审核', draft: '草稿', published: '已发布', rejected: '已驳回' }
-      return map[s] || s || '-'
-    },
-    handleSearch() {
-      this.page = 1
-      this.loadList()
-    },
-    handleSizeChange() {
-      this.page = 1
-      this.loadList()
-    },
-    async loadList() {
+    async loadPlans() {
       this.loading = true
       try {
-        var familyId = this.familyIdFilter ? Number(this.familyIdFilter) : null
-        if (familyId !== null && isNaN(familyId)) familyId = null
-        var res = await adminListHealthPlans({
-          page: this.page,
-          size: this.size,
+        const res = await adminListHealthPlans({
+          page: this.pageNum,
+          size: this.pageSize,
           keyword: this.keyword || null,
-          status: this.statusFilter || null,
-          familyId: familyId
+          status: this.statusFilter || null
         })
         if (res.code === 200) {
-          var data = res.data || {}
-          this.list = data.records || []
-          this.total = data.total || 0
-        } else {
-          this.$message.error(res.message || '加载失败')
+          this.plans = res.data?.records || []
+          this.total = res.data?.total || 0
         }
       } catch (e) {
         this.$message.error('加载失败')
@@ -209,44 +160,85 @@ export default {
         this.loading = false
       }
     },
+    handlePageChange(page) {
+      this.pageNum = page
+      this.loadPlans()
+    },
     async showDetail(row) {
       this.detailVisible = true
-      this.currentPlan = row
       this.detailLoading = true
+      this.tasksLoading = true
+      this.currentPlan = row
+      this.tasks = []
       try {
-        var res = await adminGetHealthPlanDetail({ planId: row.id })
+        const res = await adminGetHealthPlanDetail({ planId: row.id })
         if (res.code === 200) {
           this.currentPlan = res.data
-        } else {
-          this.$message.error(res.message || '加载详情失败')
+          this.tasks = res.data.tasks || []
         }
       } catch (e) {
         this.$message.error('加载详情失败')
       } finally {
         this.detailLoading = false
+        this.tasksLoading = false
       }
+    },
+    formatTime(t) {
+      if (!t) return '-'
+      return t.replace('T', ' ').substring(0, 19)
+    },
+    statusType(s) {
+      const map = { draft: 'info', pending_review: 'warning', published: 'success', rejected: 'danger' }
+      return map[s] || 'info'
+    },
+    statusLabel(s) {
+      const map = { draft: '草稿', pending_review: '待审核', published: '已发布', rejected: '已驳回' }
+      return map[s] || s
+    },
+    actionTypeLabel(type) {
+      const map = { buy: '购买', read: '阅读', exercise: '运动', checkin: '打卡', diet: '饮食', activity: '活动' }
+      return map[type] || type || '-'
+    },
+    dimensionLabel(d) {
+      const map = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
+      return map[d] || d || '-'
+    },
+    frequencyLabel(f) {
+      const map = { once: '一次性', daily: '每日' }
+      return map[f] || f || '-'
+    },
+    taskStatusType(s) {
+      const map = { pending: 'warning', completed: 'success', failed: 'danger' }
+      return map[s] || 'info'
+    },
+    taskStatusLabel(s) {
+      const map = { pending: '待完成', completed: '已完成', failed: '未完成' }
+      return map[s] || s || '-'
+    },
+    renderPlanContent(content) {
+      if (!content) return '<span style="color:#999">暂无内容</span>'
+      return content
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/\n/g, '<br>')
     }
   }
 }
 </script>
 
 <style scoped>
-.detail-section-title {
-  margin: 16px 0 8px;
-  font-weight: 600;
-  color: #303133;
-}
+.health-plan-manage { padding: 20px; }
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+.page-header h2 { margin: 0; font-size: 20px; }
+.filters { display: flex; align-items: center; }
 .plan-content {
-  white-space: pre-wrap;
-  word-break: break-word;
   background: #f5f7fa;
-  padding: 12px;
-  border-radius: 6px;
-  max-height: 360px;
-  overflow: auto;
-  margin: 0;
-  font-size: 13px;
-  line-height: 1.6;
-  color: #303133;
+  border-radius: 4px;
+  padding: 16px;
+  min-height: 80px;
+  line-height: 1.8;
+  white-space: pre-wrap;
+  font-size: 14px;
 }
 </style>

+ 81 - 2
cfc-web/src/views/teacher/HealthPlanReview.vue

@@ -52,6 +52,27 @@
         <el-form-item label="方案内容">
           <el-input v-model="editForm.planContent" type="textarea" :rows="18" placeholder="编辑方案内容..." />
         </el-form-item>
+        <el-form-item label="任务条目">
+          <div v-for="(sec, si) in editTasks.sections" :key="'sec'+si" style="margin-bottom:12px;border:1px solid #ebeef5;border-radius:4px;padding:8px;">
+            <div style="font-weight:600;margin-bottom:6px;">{{ sec.title || sec.key }}</div>
+            <div v-for="(task, ti) in sec.tasks" :key="'t'+si+'-'+ti" style="display:flex;gap:6px;margin-bottom:6px;align-items:center;">
+              <el-select v-model="task.action_type" size="mini" style="width:90px">
+                <el-option v-for="a in actionTypes" :key="a.value" :label="a.label" :value="a.value" />
+              </el-select>
+              <el-select v-model="task.dimension" size="mini" style="width:80px">
+                <el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
+              </el-select>
+              <el-select v-model="task.frequency" size="mini" style="width:70px">
+                <el-option label="一次性" value="once" />
+                <el-option label="每日" value="daily" />
+              </el-select>
+              <el-input v-model="task.title" size="mini" placeholder="任务标题" />
+              <el-input v-model="task.notes" size="mini" placeholder="备注" style="width:120px" />
+              <el-button size="mini" type="danger" icon="el-icon-delete" @click="removeTask(si, ti)" />
+            </div>
+            <el-button size="mini" type="text" icon="el-icon-plus" @click="addTask(si)">添加任务</el-button>
+          </div>
+        </el-form-item>
         <el-form-item label="审核意见">
           <el-input v-model="editForm.reviewComment" type="textarea" :rows="3" placeholder="可选:添加审核备注" />
         </el-form-item>
@@ -95,7 +116,23 @@ export default {
       rejecting: false,
       currentPlan: null,
       editForm: { planContent: '', reviewComment: '' },
-      rejectForm: { planId: null, comment: '' }
+      rejectForm: { planId: null, comment: '' },
+      editTasks: { sections: [] },
+      actionTypes: [
+        { value: 'buy', label: '购买' },
+        { value: 'read', label: '阅读' },
+        { value: 'exercise', label: '运动' },
+        { value: 'checkin', label: '打卡' },
+        { value: 'diet', label: '饮食' },
+        { value: 'activity', label: '活动' }
+      ],
+      dimensions: [
+        { value: 'body', label: '身' },
+        { value: 'mind', label: '心' },
+        { value: 'wisdom', label: '智' },
+        { value: 'action', label: '行' },
+        { value: 'wealth', label: '富' }
+      ]
     }
   },
   created() {
@@ -142,15 +179,17 @@ export default {
         planContent: row.planContent || '',
         reviewComment: row.reviewComment || ''
       }
+      this.loadEditTasks(row.planJson)
       this.editVisible = true
     },
     async saveEdit() {
       this.saving = true
       try {
+        const newPlanJson = this.buildPlanJsonWithTasks()
         const res = await updatePlanContent({
           planId: this.currentPlan.id,
           planContent: this.editForm.planContent,
-          planJson: this.currentPlan.planJson
+          planJson: newPlanJson
         })
         if (res.code === 200) {
           this.$message.success('保存成功')
@@ -165,6 +204,46 @@ export default {
         this.saving = false
       }
     },
+    parsePlanJson(str) {
+      try {
+        return JSON.parse(str || '{}')
+      } catch (e) {
+        return {}
+      }
+    },
+    loadEditTasks(planJson) {
+      const parsed = this.parsePlanJson(planJson)
+      const sections = (parsed.sections || []).map(s => ({
+        key: s.key || '',
+        title: s.title || '',
+        tasks: (s.tasks || []).map(t => ({
+          action_type: t.action_type || 'diet',
+          title: t.title || '',
+          dimension: t.dimension || 'body',
+          frequency: t.frequency || 'daily',
+          notes: t.notes || ''
+        }))
+      }))
+      this.editTasks = { sections }
+    },
+    addTask(si) {
+      this.editTasks.sections[si].tasks.push({
+        action_type: 'diet', title: '', dimension: 'body', frequency: 'daily', notes: ''
+      })
+    },
+    removeTask(si, ti) {
+      this.editTasks.sections[si].tasks.splice(ti, 1)
+    },
+    buildPlanJsonWithTasks() {
+      const parsed = this.parsePlanJson(this.currentPlan.planJson)
+      const sections = parsed.sections || []
+      this.editTasks.sections.forEach(es => {
+        const target = sections.find(s => s.key === es.key)
+        if (!target) return
+        target.tasks = es.tasks.filter(t => t.title && t.title.trim())
+      })
+      return JSON.stringify(parsed)
+    },
     showReject(row) {
       this.rejectForm = { planId: row.id, comment: '' }
       this.rejectVisible = true

+ 2 - 1
docs/superpowers/PROJECT-OVERVIEW.md

@@ -359,7 +359,7 @@
 | `2026-08-13-health-status-survey-design.md` | 🔄 v2 设计已定稿(2026-08-18;v1 已实施;v2 疾病史两源预置清单+确诊时间+服药时长) | 当前状态调研 — 健康现状档案 |
 | `2026-08-18-health-status-survey-v2.md` | 🟢 已实施(单文件改 health-status-form.vue;8 组 54 项预置清单分类多选+确诊时间;用药年月+时长自动计算;后端零变更;v1 旧数据兼容) | 当前状态调研 — 健康现状档案 v2 |
 | `2026-08-23-coach-butler-design.md` | ✅ 已实施 | AI健康教练人格分化(浠宝/福宝话术路由)与 L2 家庭管家自助选择设计 |
-| `2026-08-28-plan-standard-content-design.md` | 🟡 设计已确认 | 生成方案标准内容格式(plan_json.sections[].tasks 唯一事实源 + LangGraph prompt tasks 输出 + Pydantic 校验 + 正则兜底 + 存量懒加载回填 + 规划师端结构化任务编辑 UI) |
+| `2026-08-28-plan-standard-content-design.md` | 🟢 已实施 | 生成方案标准内容格式(plan_json.sections[].tasks 唯一事实源 + LangGraph prompt tasks 输出 + Pydantic 校验 + 正则兜底 + 存量懒加载回填 + 规划师端结构化任务编辑 UI) |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 
 ### 实施计划(plans/)
@@ -432,6 +432,7 @@
 | `2026-08-15-daily-task-overview.md` | 🟡 待实施 | 今日任务首页卡片实施计划(仅小程序接入 + 1个只读聚合接口 `/api/daily-task/overview`,聚合打卡/任务/阅读活动/测评报告四类,点击跳转+返回自动刷新;设计稿:2026-08-15-daily-task-overview-design.md) |
 | `2026-08-15-sku-price-cents-and-spec-selector.md` | ✅ 已实施(7 Tasks:实体/DTO/订单/购物车/迁移/管理端/小程序,5 commits) | SKU 规格价格统一分单位 + 详情页规格按钮修复实施计划 |
 | `2026-08-23-coach-butler.md` | ✅ 已完成(11 Tasks:DB迁移/实体Mapper/Service/控制器×2/AI注入/LangGraph路由/小程序/Web管理端/文档验收) | AI健康教练人格分化与管家自助选择实施计划(设计稿:2026-08-23-coach-butler-design.md) |
+| `2026-08-28-plan-standard-content.md` | ✅ 已实施(9 Tasks:LangGraph模型/prompt任务契约/Pydantic校验+正则兜底/regenerate/Java任务生成优先+懒加载回填/Java编译/cfc-web结构化任务编辑/小程序tasks回流/文档验收) | 生成方案标准内容格式实施计划(plan_json.sections[].tasks 唯一事实源;设计稿:2026-08-28-plan-standard-content-design.md) |
 
 ### 计划与设计文档(specs/)