plan-detail.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <template>
  2. <view class="page">
  3. <view class="card">
  4. <text class="title">{{ planTitle }}</text>
  5. <text class="meta">创建于 {{ formatTime(plan.createdAt) }}</text>
  6. <text class="meta" v-if="plan.teacherId">审核规划师:{{ plan.teacherId }}</text>
  7. <text class="meta" v-else>未分配规划师</text>
  8. </view>
  9. <view class="card">
  10. <text class="label">方案正文</text>
  11. <textarea
  12. class="editor"
  13. v-model="planContent"
  14. :disabled="!editing"
  15. placeholder="方案正文"
  16. />
  17. </view>
  18. <view class="actions">
  19. <view class="btn ghost" @click="toggleEdit">{{ editing ? '取消编辑' : '编辑' }}</view>
  20. <view class="btn ghost" v-if="editing" @click="save">保存</view>
  21. <view class="btn ok" @click="confirmThen('approve')">通过</view>
  22. <view class="btn no" @click="confirmThen('reject')">驳回</view>
  23. </view>
  24. </view>
  25. </template>
  26. <script>
  27. import {
  28. updateGuidePlanContent,
  29. approveGuidePlan,
  30. rejectGuidePlan,
  31. getGuidePendingPlans
  32. } from '@/utils/api.js'
  33. export default {
  34. data() {
  35. return {
  36. familyId: null,
  37. planId: null,
  38. plan: {},
  39. planContent: '',
  40. editing: false
  41. }
  42. },
  43. computed: {
  44. planTitle() {
  45. // HealthPlan 实体无 title 字段,只能依次回退到 goal / memberName
  46. return this.plan.goal || this.plan.memberName || '健康方案'
  47. }
  48. },
  49. onLoad(options) {
  50. this.familyId = options.familyId
  51. this.planId = options.planId
  52. this.fetchPlan()
  53. },
  54. methods: {
  55. async fetchPlan() {
  56. try {
  57. const res = await getGuidePendingPlans(this.familyId)
  58. if (res && res.code === 200) {
  59. const list = res.data || []
  60. for (let i = 0; i < list.length; i++) {
  61. if (String(list[i].id) === String(this.planId)) {
  62. this.plan = list[i]
  63. this.planContent = list[i].planContent || ''
  64. return
  65. }
  66. }
  67. uni.showToast({ title: '方案不存在或已被审核', icon: 'none' })
  68. }
  69. } catch (e) {
  70. uni.showToast({ title: '加载失败', icon: 'none' })
  71. }
  72. },
  73. toggleEdit() {
  74. this.editing = !this.editing
  75. },
  76. async save() {
  77. try {
  78. const res = await updateGuidePlanContent(this.familyId, this.planId, this.planContent)
  79. if (res && res.code === 200) {
  80. uni.showToast({ title: '已保存', icon: 'none' })
  81. this.editing = false
  82. } else {
  83. uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
  84. }
  85. } catch (e) {
  86. uni.showToast({ title: '网络异常', icon: 'none' })
  87. }
  88. },
  89. confirmThen(action) {
  90. uni.showModal({
  91. title: action === 'approve' ? '确认通过?' : '确认驳回?',
  92. content: action === 'approve' ? '通过后将自动生成每日任务' : '驳回后家长需重新提交',
  93. success: (r) => {
  94. if (!r.confirm) return
  95. this.doAction(action, '')
  96. }
  97. })
  98. },
  99. async doAction(action, comment) {
  100. try {
  101. const fn = action === 'approve' ? approveGuidePlan : rejectGuidePlan
  102. const res = await fn(this.familyId, this.planId, comment)
  103. if (res && res.code === 200) {
  104. const d = res.data || {}
  105. // taskGenerated=false 是「已发布但未生成任务」,属部分成功,必须与失败区分开提示
  106. if (action === 'approve' && d.taskGenerated === false) {
  107. uni.showModal({
  108. title: '部分成功',
  109. content: d.message || '方案已发布,但未生成任何任务,请检查方案内容',
  110. showCancel: false
  111. })
  112. } else {
  113. uni.showToast({ title: '操作成功', icon: 'none' })
  114. }
  115. setTimeout(() => uni.navigateBack(), 1200)
  116. } else {
  117. uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
  118. }
  119. } catch (e) {
  120. uni.showToast({ title: '网络异常', icon: 'none' })
  121. }
  122. },
  123. formatTime(t) {
  124. if (!t) return '-'
  125. // 项目统一时间格式:取前19位并把 ISO 的 T 换成空格,避免 toLocaleString 的浏览器差异
  126. return String(t).substring(0, 19).replace('T', ' ')
  127. }
  128. }
  129. }
  130. </script>
  131. <style scoped>
  132. .page { padding: 24rpx; background: #F5FAFE; min-height: 100vh; }
  133. .card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
  134. .title { font-size: 32rpx; font-weight: 600; color: #1F2D3D; display: block; margin-bottom: 12rpx; }
  135. .meta { font-size: 24rpx; color: #8A94A6; display: block; margin-bottom: 6rpx; }
  136. .label { font-size: 26rpx; color: #5A6478; display: block; margin-bottom: 12rpx; }
  137. .editor { width: 100%; min-height: 400rpx; font-size: 28rpx; color: #1F2D3D; padding: 16rpx; background: #F7F9FC; border-radius: 12rpx; }
  138. .actions { display: flex; flex-direction: row; flex-wrap: wrap; }
  139. .btn { flex: 1; text-align: center; padding: 20rpx 0; border-radius: 12rpx; font-size: 28rpx; margin-right: 16rpx; margin-bottom: 16rpx; min-width: 40%; }
  140. .btn.ghost { background: #fff; color: #4A9BD7; border: 1rpx solid #4A9BD7; }
  141. .btn.ok { background: #4A9BD7; color: #fff; }
  142. .btn.no { background: #F2F4F7; color: #5A6478; margin-right: 0; }
  143. </style>