index.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <template>
  2. <view class="container">
  3. <view class="header">
  4. <text class="title">测评方案审核</text>
  5. <view class="header-actions">
  6. <picker :range="['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']" @change="onStatusFilter">
  7. <view class="filter-btn">
  8. <text>{{ statusFilterText }}</text>
  9. </view>
  10. </picker>
  11. </view>
  12. </view>
  13. <view class="family-section" v-for="group in planGroups" :key="group.familyId">
  14. <view class="family-header">
  15. <text class="family-name">家庭 ID: {{ group.familyId }}</text>
  16. <text class="plan-count">{{ group.plans.length }} 个方案</text>
  17. </view>
  18. <view class="plan-card" v-for="plan in group.plans" :key="plan.id" @click="goDetail(plan)">
  19. <view class="plan-header">
  20. <text class="plan-title">{{ plan.name || '测评方案' }}</text>
  21. <text class="plan-status" :class="'status-' + plan.status">{{ statusLabel(plan.status) }}</text>
  22. </view>
  23. <view class="plan-meta">
  24. <text>孩子ID: {{ plan.childId }}</text>
  25. <text>{{ formatDate(plan.createdAt) }}</text>
  26. </view>
  27. <view class="plan-desc" v-if="plan.remark">{{ plan.remark }}</view>
  28. <view class="plan-actions">
  29. <view class="action-btn primary" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, true)">
  30. <text>批准</text>
  31. </view>
  32. <view class="action-btn danger" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, false)">
  33. <text>驳回</text>
  34. </view>
  35. <view class="action-btn success" v-if="plan.status === 'reviewed'" @click.stop="handleActivate(plan)">
  36. <text>激活</text>
  37. </view>
  38. </view>
  39. </view>
  40. </view>
  41. <view class="empty" v-if="planGroups.length === 0">
  42. <text>暂无方案</text>
  43. </view>
  44. <uni-popup ref="reviewPopup" type="dialog">
  45. <uni-popup-dialog title="审核意见" :content="reviewComment" @confirm="submitReview" @close="reviewComment = ''">
  46. <input class="review-input" v-model="reviewComment" placeholder="输入审核意见(可选)" />
  47. </uni-popup-dialog>
  48. </uni-popup>
  49. </view>
  50. </template>
  51. <script>
  52. import { listFamilyPlans, reviewPlan, activatePlan, getGuidePlanDetail } from '@/utils/api'
  53. export default {
  54. data() {
  55. return {
  56. plans: [],
  57. statusFilter: '',
  58. statusFilterText: '全部状态',
  59. currentPlan: null,
  60. reviewApproved: false,
  61. reviewComment: '',
  62. families: []
  63. }
  64. },
  65. computed: {
  66. planGroups() {
  67. const groups = {}
  68. for (const p of this.plans) {
  69. const key = p.familyId
  70. if (!groups[key]) groups[key] = { familyId: key, plans: [] }
  71. groups[key].plans.push(p)
  72. }
  73. return Object.values(groups)
  74. }
  75. },
  76. onShow() {
  77. this.loadPlans()
  78. },
  79. methods: {
  80. async loadPlans() {
  81. try {
  82. const res = await listFamilyPlans({ status: this.statusFilter || null })
  83. if (res.code === 200) this.plans = res.data || []
  84. } catch (e) { /* ignore */ }
  85. },
  86. onStatusFilter(e) {
  87. const labels = ['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']
  88. const vals = ['', 'generated', 'reviewed', 'active', 'completed']
  89. const idx = e.detail.value
  90. this.statusFilterText = labels[idx]
  91. this.statusFilter = vals[idx]
  92. this.loadPlans()
  93. },
  94. statusLabel(s) {
  95. const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
  96. return map[s] || s
  97. },
  98. formatDate(d) {
  99. if (!d) return ''
  100. const date = new Date(d)
  101. return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
  102. },
  103. goDetail(plan) {
  104. uni.navigateTo({ url: '/pages/guide/plans/detail?planId=' + plan.id })
  105. },
  106. handleReview(plan, approved) {
  107. this.currentPlan = plan
  108. this.reviewApproved = approved
  109. this.reviewComment = ''
  110. if (!approved) {
  111. uni.showModal({
  112. title: '驳回方案',
  113. content: '确定驳回该方案吗?',
  114. success: (res) => {
  115. if (res.confirm) this.submitReview()
  116. }
  117. })
  118. } else {
  119. this.submitReview()
  120. }
  121. },
  122. async submitReview() {
  123. if (!this.currentPlan) return
  124. try {
  125. const res = await reviewPlan({
  126. planId: this.currentPlan.id,
  127. approved: this.reviewApproved,
  128. comment: this.reviewComment
  129. })
  130. if (res.code === 200) {
  131. uni.showToast({ title: '操作成功' })
  132. this.loadPlans()
  133. }
  134. } catch (e) { /* ignore */ }
  135. },
  136. handleActivate(plan) {
  137. uni.showModal({
  138. title: '激活方案',
  139. content: '激活后将生成任务,确定激活?',
  140. success: async (res) => {
  141. if (!res.confirm) return
  142. try {
  143. const r = await activatePlan({ planId: plan.id })
  144. if (r.code === 200) {
  145. uni.showToast({ title: '方案已激活' })
  146. this.loadPlans()
  147. }
  148. } catch (e) { /* ignore */ }
  149. }
  150. })
  151. }
  152. }
  153. }
  154. </script>
  155. <style scoped>
  156. .container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
  157. .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
  158. .title { font-size: 36rpx; font-weight: bold; }
  159. .filter-btn { padding: 10rpx 20rpx; background: #fff; border-radius: 8rpx; font-size: 26rpx; }
  160. .family-section { margin-bottom: 30rpx; }
  161. .family-header { display: flex; justify-content: space-between; padding: 20rpx; background: #e8f4fd; border-radius: 12rpx 12rpx 0 0; }
  162. .family-name { font-weight: bold; font-size: 28rpx; }
  163. .plan-count { font-size: 24rpx; color: #999; }
  164. .plan-card { background: #fff; padding: 24rpx; border-bottom: 2rpx solid #f0f0f0; }
  165. .plan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
  166. .plan-title { font-size: 30rpx; font-weight: bold; }
  167. .plan-status { font-size: 24rpx; padding: 4rpx 12rpx; border-radius: 6rpx; }
  168. .status-generated { background: #fff3cd; color: #856404; }
  169. .status-reviewed { background: #d1ecf1; color: #0c5460; }
  170. .status-active { background: #d4edda; color: #155724; }
  171. .status-completed { background: #e2e3e5; color: #383d41; }
  172. .status-rejected { background: #f8d7da; color: #721c24; }
  173. .plan-meta { font-size: 24rpx; color: #999; display: flex; justify-content: space-between; margin-bottom: 12rpx; }
  174. .plan-desc { font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
  175. .plan-actions { display: flex; gap: 20rpx; }
  176. .action-btn { padding: 10rpx 24rpx; border-radius: 8rpx; font-size: 26rpx; }
  177. .action-btn.primary { background: #4A9BD7; color: #fff; }
  178. .action-btn.danger { background: #e74c3c; color: #fff; }
  179. .action-btn.success { background: #27ae60; color: #fff; }
  180. .empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 30rpx; }
  181. .review-input { border: 2rpx solid #ddd; border-radius: 8rpx; padding: 16rpx; margin-top: 20rpx; }
  182. </style>