doa-stage-detail.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <view class="page">
  3. <view v-if="loading" class="loading-box">加载中...</view>
  4. <view v-else-if="stage" class="content">
  5. <!-- 阶段头部 -->
  6. <view class="stage-header">
  7. <text class="stage-title">{{ stage.title || '阶段目标' }}</text>
  8. <text class="stage-dates">{{ formatDate(stage.startDate) }} ~ {{ formatDate(stage.endDate) }}</text>
  9. <view class="progress-row">
  10. <text class="progress-text">复盘进度 {{ stage.reviewedWeeks || 0 }}/{{ stage.totalWeeks || 0 }}</text>
  11. </view>
  12. </view>
  13. <!-- 目标列表 -->
  14. <view class="section">
  15. <text class="section-title">🎯 阶段目标</text>
  16. <view v-for="g in stage.goals" :key="g.id" class="goal-item">
  17. <text class="goal-tag" :class="'domain-' + g.domain">{{ domainName(g.domain) }}</text>
  18. <text class="goal-content">{{ g.content }}</text>
  19. </view>
  20. </view>
  21. <!-- 周时间线 -->
  22. <view class="section">
  23. <text class="section-title">📅 每周计划与复盘</text>
  24. <view v-for="w in stage.weeks" :key="getWeekKey(w)" class="week-card">
  25. <view class="week-header" @click="toggleWeek(w.id)">
  26. <text class="week-no">第 {{ w.weekNo }} 周</text>
  27. <text class="week-dates">{{ formatDate(w.weekStart) }} ~ {{ formatDate(w.weekEnd) }}</text>
  28. <text class="week-status" :class="'ws-' + w.status">{{ weekStatusText(w.status) }}</text>
  29. </view>
  30. <view v-if="expandedWeekId === w.id" class="week-body">
  31. <!-- 计划 -->
  32. <view class="week-block">
  33. <text class="block-title">📋 本周计划</text>
  34. <text class="block-text">{{ w.plan || '(未填写)' }}</text>
  35. </view>
  36. <!-- 复盘 -->
  37. <view class="week-block" v-if="w.status === 'reviewed'">
  38. <text class="block-title">✅ 复盘总结</text>
  39. <view class="review-item">
  40. <text class="review-label">达成:</text>
  41. <text class="review-text">{{ w.achieved || '—' }}</text>
  42. </view>
  43. <view class="review-item">
  44. <text class="review-label">体验:</text>
  45. <text class="review-text">{{ w.experience || '—' }}</text>
  46. </view>
  47. <view class="review-item">
  48. <text class="review-label">行得通:</text>
  49. <text class="review-text worked">{{ w.worked || '—' }}</text>
  50. </view>
  51. <view class="review-item">
  52. <text class="review-label">要提升:</text>
  53. <text class="review-text improve">{{ w.improve || '—' }}</text>
  54. </view>
  55. <view class="review-item">
  56. <text class="review-label">下周挑战:</text>
  57. <text class="review-text">{{ w.nextChallenge || '—' }}</text>
  58. </view>
  59. </view>
  60. <!-- 编辑按钮(本人或代管) -->
  61. <view v-if="canEdit" class="week-actions">
  62. <button class="btn-edit" @click="editWeek(w)">编辑{{ w.status === 'reviewed' ? '' : '计划' }}</button>
  63. </view>
  64. </view>
  65. </view>
  66. </view>
  67. <!-- 取消阶段 -->
  68. <button v-if="canEdit && stage.status === 'active'" class="btn-cancel" @click="cancelStage">取消阶段</button>
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. import { getDoaStageDetail, cancelDoaStage } from '../../utils/api.js'
  74. export default {
  75. data() {
  76. return {
  77. loading: true,
  78. stage: null,
  79. stageId: 0,
  80. selfMemberId: 0,
  81. expandedWeekId: 0,
  82. canEdit: false
  83. }
  84. },
  85. onLoad(options) {
  86. this.stageId = parseInt(options.stageId || '0', 10)
  87. this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
  88. this.loadDetail()
  89. },
  90. methods: {
  91. getWeekKey(w) {
  92. return w.id
  93. },
  94. async loadDetail() {
  95. this.loading = true
  96. try {
  97. var res = await getDoaStageDetail(this.stageId)
  98. if (res.code === 200 && res.data) {
  99. this.stage = res.data
  100. // 编辑权限:自己 或 未登录成员(memberNickname 存在且非本人 → 后端已校验,前端保守判断)
  101. this.canEdit = this.stage.memberId === this.selfMemberId || !this.stage.memberUserId
  102. } else {
  103. uni.showToast({ title: res.message || '加载失败', icon: 'none' })
  104. setTimeout(function() { uni.navigateBack() }, 1000)
  105. }
  106. } catch (e) {
  107. uni.showToast({ title: '加载失败', icon: 'none' })
  108. } finally {
  109. this.loading = false
  110. }
  111. },
  112. domainName(d) {
  113. var map = { health: '健康', wealth: '事业', family: '家庭关系', social: '社会关系', help: '帮助他人' }
  114. return map[d] || d
  115. },
  116. formatDate(dateStr) {
  117. if (!dateStr) return ''
  118. return String(dateStr).slice(0, 10)
  119. },
  120. weekStatusText(s) {
  121. if (s === 'reviewed') return '已复盘'
  122. if (s === 'planned') return '已计划'
  123. return '待计划'
  124. },
  125. toggleWeek(id) {
  126. this.expandedWeekId = this.expandedWeekId === id ? 0 : id
  127. },
  128. editWeek(w) {
  129. uni.navigateTo({ url: '/pages/health/doa-week-edit?weekId=' + w.id + '&stageId=' + this.stageId })
  130. },
  131. cancelStage() {
  132. var self = this
  133. uni.showModal({
  134. title: '确认取消',
  135. content: '确定要取消这个阶段吗?周记录将保留但不可再编辑。',
  136. success: function(res) {
  137. if (res.confirm) {
  138. cancelDoaStage(self.stageId).then(function(r) {
  139. if (r.code === 200) {
  140. uni.showToast({ title: '已取消', icon: 'success' })
  141. setTimeout(function() { uni.navigateBack() }, 600)
  142. } else {
  143. uni.showToast({ title: r.message || '取消失败', icon: 'none' })
  144. }
  145. })
  146. }
  147. }
  148. })
  149. }
  150. }
  151. }
  152. </script>
  153. <style scoped>
  154. .page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
  155. .loading-box { text-align: center; padding: 100rpx 0; color: #94A3B8; font-size: 28rpx; }
  156. .stage-header {
  157. background: linear-gradient(135deg, #10B981, #34D399);
  158. border-radius: 20rpx; padding: 32rpx; margin-bottom: 20rpx; color: #fff;
  159. }
  160. .stage-title { font-size: 34rpx; font-weight: 700; display: block; }
  161. .stage-dates { font-size: 24rpx; opacity: 0.9; margin-top: 8rpx; display: block; }
  162. .progress-row { margin-top: 16rpx; }
  163. .progress-text { font-size: 24rpx; font-weight: 600; }
  164. .section { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
  165. .section-title { font-size: 28rpx; font-weight: 700; color: #1E293B; display: block; margin-bottom: 16rpx; }
  166. .goal-item { display: flex; align-items: center; margin-bottom: 12rpx; }
  167. .goal-tag {
  168. font-size: 20rpx; padding: 6rpx 14rpx; border-radius: 8rpx; margin-right: 12rpx;
  169. border: 2rpx solid #E2E8F0; color: #475569; flex-shrink: 0;
  170. }
  171. .domain-health { border-color: #FF8C42 !important; color: #C2410C !important; }
  172. .domain-wealth { border-color: #F59E0B !important; color: #B45309 !important; }
  173. .domain-family { border-color: #10B981 !important; color: #047857 !important; }
  174. .domain-social { border-color: #6366F1 !important; color: #4338CA !important; }
  175. .domain-help { border-color: #FF6B9D !important; color: #BE185D !important; }
  176. .goal-content { font-size: 26rpx; color: #334155; flex: 1; }
  177. .week-card { background: #F8FAFC; border-radius: 12rpx; padding: 20rpx; margin-bottom: 12rpx; }
  178. .week-header { display: flex; align-items: center; }
  179. .week-no { font-size: 26rpx; font-weight: 700; color: #1E293B; flex: 1; }
  180. .week-dates { font-size: 20rpx; color: #94A3B8; margin-right: 12rpx; }
  181. .week-status { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 20rpx; }
  182. .ws-pending { background: #F1F5F9; color: #64748B; }
  183. .ws-planned { background: #DBEAFE; color: #1D4ED8; }
  184. .ws-reviewed { background: #DCFCE7; color: #16A34A; }
  185. .week-body { margin-top: 16rpx; border-top: 2rpx solid #E2E8F0; padding-top: 16rpx; }
  186. .week-block { margin-bottom: 16rpx; }
  187. .block-title { font-size: 24rpx; font-weight: 700; color: #475569; display: block; margin-bottom: 8rpx; }
  188. .block-text { font-size: 24rpx; color: #334155; white-space: pre-wrap; display: block; }
  189. .review-item { margin-bottom: 8rpx; }
  190. .review-label { font-size: 22rpx; color: #64748B; }
  191. .review-text { font-size: 22rpx; color: #334155; }
  192. .review-text.worked { color: #16A34A; }
  193. .review-text.improve { color: #DC2626; }
  194. .week-actions { margin-top: 12rpx; }
  195. .btn-edit {
  196. background: #10B981; color: #fff; font-size: 24rpx; font-weight: 600;
  197. padding: 12rpx 32rpx; border-radius: 32rpx; border: none; width: auto;
  198. }
  199. .btn-cancel {
  200. background: #fff; color: #EF4444; font-size: 26rpx; font-weight: 600;
  201. border: 2rpx solid #FCA5A5; border-radius: 42rpx; height: 80rpx; line-height: 80rpx;
  202. margin-top: 8rpx;
  203. }
  204. </style>