Bladeren bron

feat(doa): 新增doa-stage-detail详情页+实体memberUserId字段

Sisyphus 1 week geleden
bovenliggende
commit
266e38e7d3
1 gewijzigde bestanden met toevoegingen van 213 en 0 verwijderingen
  1. 213 0
      cfc-frontend/pages/health/doa-stage-detail.vue

+ 213 - 0
cfc-frontend/pages/health/doa-stage-detail.vue

@@ -0,0 +1,213 @@
+<template>
+  <view class="page">
+    <view v-if="loading" class="loading-box">加载中...</view>
+
+    <view v-else-if="stage" class="content">
+      <!-- 阶段头部 -->
+      <view class="stage-header">
+        <text class="stage-title">{{ stage.title || '阶段目标' }}</text>
+        <text class="stage-dates">{{ formatDate(stage.startDate) }} ~ {{ formatDate(stage.endDate) }}</text>
+        <view class="progress-row">
+          <text class="progress-text">复盘进度 {{ stage.reviewedWeeks || 0 }}/{{ stage.totalWeeks || 0 }}</text>
+        </view>
+      </view>
+
+      <!-- 目标列表 -->
+      <view class="section">
+        <text class="section-title">🎯 阶段目标</text>
+        <view v-for="g in stage.goals" :key="g.id" class="goal-item">
+          <text class="goal-tag" :class="'domain-' + g.domain">{{ domainName(g.domain) }}</text>
+          <text class="goal-content">{{ g.content }}</text>
+        </view>
+      </view>
+
+      <!-- 周时间线 -->
+      <view class="section">
+        <text class="section-title">📅 每周计划与复盘</text>
+        <view v-for="w in stage.weeks" :key="getWeekKey(w)" class="week-card">
+          <view class="week-header" @click="toggleWeek(w.id)">
+            <text class="week-no">第 {{ w.weekNo }} 周</text>
+            <text class="week-dates">{{ formatDate(w.weekStart) }} ~ {{ formatDate(w.weekEnd) }}</text>
+            <text class="week-status" :class="'ws-' + w.status">{{ weekStatusText(w.status) }}</text>
+          </view>
+
+          <view v-if="expandedWeekId === w.id" class="week-body">
+            <!-- 计划 -->
+            <view class="week-block">
+              <text class="block-title">📋 本周计划</text>
+              <text class="block-text">{{ w.plan || '(未填写)' }}</text>
+            </view>
+            <!-- 复盘 -->
+            <view class="week-block" v-if="w.status === 'reviewed'">
+              <text class="block-title">✅ 复盘总结</text>
+              <view class="review-item">
+                <text class="review-label">达成:</text>
+                <text class="review-text">{{ w.achieved || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">体验:</text>
+                <text class="review-text">{{ w.experience || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">行得通:</text>
+                <text class="review-text worked">{{ w.worked || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">要提升:</text>
+                <text class="review-text improve">{{ w.improve || '—' }}</text>
+              </view>
+              <view class="review-item">
+                <text class="review-label">下周挑战:</text>
+                <text class="review-text">{{ w.nextChallenge || '—' }}</text>
+              </view>
+            </view>
+
+            <!-- 编辑按钮(本人或代管) -->
+            <view v-if="canEdit" class="week-actions">
+              <button class="btn-edit" @click="editWeek(w)">编辑{{ w.status === 'reviewed' ? '' : '计划' }}</button>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 取消阶段 -->
+      <button v-if="canEdit && stage.status === 'active'" class="btn-cancel" @click="cancelStage">取消阶段</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getDoaStageDetail, cancelDoaStage } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      stage: null,
+      stageId: 0,
+      selfMemberId: 0,
+      expandedWeekId: 0,
+      canEdit: false
+    }
+  },
+  onLoad(options) {
+    this.stageId = parseInt(options.stageId || '0', 10)
+    this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
+    this.loadDetail()
+  },
+  methods: {
+    getWeekKey(w) {
+      return w.id
+    },
+    async loadDetail() {
+      this.loading = true
+      try {
+        var res = await getDoaStageDetail(this.stageId)
+        if (res.code === 200 && res.data) {
+          this.stage = res.data
+          // 编辑权限:自己 或 未登录成员(memberNickname 存在且非本人 → 后端已校验,前端保守判断)
+          this.canEdit = this.stage.memberId === this.selfMemberId || !this.stage.memberUserId
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+          setTimeout(function() { uni.navigateBack() }, 1000)
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    domainName(d) {
+      var map = { health: '健康', wealth: '事业', family: '家庭关系', social: '社会关系', help: '帮助他人' }
+      return map[d] || d
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      return String(dateStr).slice(0, 10)
+    },
+    weekStatusText(s) {
+      if (s === 'reviewed') return '已复盘'
+      if (s === 'planned') return '已计划'
+      return '待计划'
+    },
+    toggleWeek(id) {
+      this.expandedWeekId = this.expandedWeekId === id ? 0 : id
+    },
+    editWeek(w) {
+      uni.navigateTo({ url: '/pages/health/doa-week-edit?weekId=' + w.id + '&stageId=' + this.stageId })
+    },
+    cancelStage() {
+      var self = this
+      uni.showModal({
+        title: '确认取消',
+        content: '确定要取消这个阶段吗?周记录将保留但不可再编辑。',
+        success: function(res) {
+          if (res.confirm) {
+            cancelDoaStage(self.stageId).then(function(r) {
+              if (r.code === 200) {
+                uni.showToast({ title: '已取消', icon: 'success' })
+                setTimeout(function() { uni.navigateBack() }, 600)
+              } else {
+                uni.showToast({ title: r.message || '取消失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.loading-box { text-align: center; padding: 100rpx 0; color: #94A3B8; font-size: 28rpx; }
+.stage-header {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  border-radius: 20rpx; padding: 32rpx; margin-bottom: 20rpx; color: #fff;
+}
+.stage-title { font-size: 34rpx; font-weight: 700; display: block; }
+.stage-dates { font-size: 24rpx; opacity: 0.9; margin-top: 8rpx; display: block; }
+.progress-row { margin-top: 16rpx; }
+.progress-text { font-size: 24rpx; font-weight: 600; }
+.section { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
+.section-title { font-size: 28rpx; font-weight: 700; color: #1E293B; display: block; margin-bottom: 16rpx; }
+.goal-item { display: flex; align-items: center; margin-bottom: 12rpx; }
+.goal-tag {
+  font-size: 20rpx; padding: 6rpx 14rpx; border-radius: 8rpx; margin-right: 12rpx;
+  border: 2rpx solid #E2E8F0; color: #475569; flex-shrink: 0;
+}
+.domain-health { border-color: #FF8C42 !important; color: #C2410C !important; }
+.domain-wealth { border-color: #F59E0B !important; color: #B45309 !important; }
+.domain-family { border-color: #10B981 !important; color: #047857 !important; }
+.domain-social { border-color: #6366F1 !important; color: #4338CA !important; }
+.domain-help { border-color: #FF6B9D !important; color: #BE185D !important; }
+.goal-content { font-size: 26rpx; color: #334155; flex: 1; }
+.week-card { background: #F8FAFC; border-radius: 12rpx; padding: 20rpx; margin-bottom: 12rpx; }
+.week-header { display: flex; align-items: center; }
+.week-no { font-size: 26rpx; font-weight: 700; color: #1E293B; flex: 1; }
+.week-dates { font-size: 20rpx; color: #94A3B8; margin-right: 12rpx; }
+.week-status { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 20rpx; }
+.ws-pending { background: #F1F5F9; color: #64748B; }
+.ws-planned { background: #DBEAFE; color: #1D4ED8; }
+.ws-reviewed { background: #DCFCE7; color: #16A34A; }
+.week-body { margin-top: 16rpx; border-top: 2rpx solid #E2E8F0; padding-top: 16rpx; }
+.week-block { margin-bottom: 16rpx; }
+.block-title { font-size: 24rpx; font-weight: 700; color: #475569; display: block; margin-bottom: 8rpx; }
+.block-text { font-size: 24rpx; color: #334155; white-space: pre-wrap; display: block; }
+.review-item { margin-bottom: 8rpx; }
+.review-label { font-size: 22rpx; color: #64748B; }
+.review-text { font-size: 22rpx; color: #334155; }
+.review-text.worked { color: #16A34A; }
+.review-text.improve { color: #DC2626; }
+.week-actions { margin-top: 12rpx; }
+.btn-edit {
+  background: #10B981; color: #fff; font-size: 24rpx; font-weight: 600;
+  padding: 12rpx 32rpx; border-radius: 32rpx; border: none; width: auto;
+}
+.btn-cancel {
+  background: #fff; color: #EF4444; font-size: 26rpx; font-weight: 600;
+  border: 2rpx solid #FCA5A5; border-radius: 42rpx; height: 80rpx; line-height: 80rpx;
+  margin-top: 8rpx;
+}
+</style>