doa-stage-create.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <template>
  2. <view class="page">
  3. <!-- 目标成员(代管时显示,仅未登录成员可选) -->
  4. <view class="field" v-if="!isSelfOnly">
  5. <text class="field-label">目标成员</text>
  6. <view class="member-grid">
  7. <view
  8. v-for="m in members"
  9. :key="m.id"
  10. class="member-item"
  11. :class="{ active: form.memberId === m.id }"
  12. @click="selectMember(m)"
  13. >
  14. <text>{{ m.nickname }}</text>
  15. </view>
  16. </view>
  17. </view>
  18. <!-- 阶段主题 -->
  19. <view class="field">
  20. <text class="field-label">阶段主题(可选)</text>
  21. <input class="field-input" v-model="form.title" placeholder="如:暑期习惯养成" maxlength="50" />
  22. </view>
  23. <!-- 周期选择 -->
  24. <view class="field">
  25. <text class="field-label">阶段周期 <text class="required">*</text></text>
  26. <view class="radio-group">
  27. <view class="radio-btn" :class="{ active: form.durationWeeks === 2 }" @click="form.durationWeeks = 2">2周</view>
  28. <view class="radio-btn" :class="{ active: form.durationWeeks === 4 }" @click="form.durationWeeks = 4">4周</view>
  29. <view class="radio-btn" :class="{ active: form.durationWeeks === 8 }" @click="form.durationWeeks = 8">8周</view>
  30. <view class="radio-btn" :class="{ active: form.durationWeeks === 12 }" @click="form.durationWeeks = 12">12周</view>
  31. </view>
  32. </view>
  33. <!-- 目标列表(1-3条) -->
  34. <view class="field">
  35. <text class="field-label">阶段目标(1-3 个,不同领域)</text>
  36. <view v-for="(goal, idx) in form.goals" :key="getGoalKey(goal, idx)" class="goal-editor">
  37. <view class="goal-domain-row">
  38. <text class="goal-label">目标 {{ idx + 1 }} 领域</text>
  39. <text class="goal-remove" @click="removeGoal(idx)">✕</text>
  40. </view>
  41. <view class="radio-group">
  42. <view
  43. v-for="d in domains"
  44. :key="d.value"
  45. class="radio-btn small"
  46. :class="{ active: goal.domain === d.value }"
  47. @click="goal.domain = d.value"
  48. >{{ d.label }}</view>
  49. </view>
  50. <input class="field-input" v-model="goal.content" placeholder="量化目标,如:体重降到 172 以下" maxlength="100" />
  51. </view>
  52. <view class="add-goal" v-if="form.goals.length < 3" @click="addGoal">+ 添加目标</view>
  53. </view>
  54. <button class="btn-submit" @click="submit">创建阶段</button>
  55. </view>
  56. </template>
  57. <script>
  58. import { createDoaStage, getFamilyMemberList } from '../../utils/api.js'
  59. export default {
  60. data() {
  61. return {
  62. members: [],
  63. selfMemberId: 0,
  64. targetMemberId: 0,
  65. hasLocalMembers: false,
  66. domains: [
  67. { value: 'health', label: '健康' },
  68. { value: 'wealth', label: '事业' },
  69. { value: 'family', label: '家庭关系' },
  70. { value: 'social', label: '社会关系' },
  71. { value: 'help', label: '帮助他人' }
  72. ],
  73. form: {
  74. memberId: 0,
  75. title: '',
  76. durationWeeks: 4,
  77. goals: [{ domain: 'health', content: '' }]
  78. }
  79. }
  80. },
  81. computed: {
  82. isSelfOnly() {
  83. // 只为自己创建时(未传入 memberId 且无可代管成员)
  84. return this.targetMemberId === 0 && !this.hasLocalMembers
  85. }
  86. },
  87. onLoad(options) {
  88. this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
  89. if (options.memberId) {
  90. this.targetMemberId = parseInt(options.memberId, 10)
  91. this.form.memberId = parseInt(options.memberId, 10)
  92. }
  93. this.loadMembers()
  94. },
  95. methods: {
  96. getGoalKey(goal, idx) {
  97. return goal.domain + '_' + idx
  98. },
  99. async loadMembers() {
  100. try {
  101. var res = await getFamilyMemberList({ visibleOnly: true })
  102. var list = res.data || []
  103. this.members = list
  104. this.hasLocalMembers = list.some(function(m) { return !m.userId })
  105. // 若从总览"为他创建"进入且目标成员未登录 → 直接锁定
  106. if (this.targetMemberId > 0) {
  107. this.form.memberId = this.targetMemberId
  108. }
  109. } catch (e) {
  110. this.members = []
  111. this.hasLocalMembers = false
  112. }
  113. },
  114. selectMember(m) {
  115. this.form.memberId = m.id
  116. },
  117. addGoal() {
  118. if (this.form.goals.length >= 3) return
  119. this.form.goals.push({ domain: 'health', content: '' })
  120. },
  121. removeGoal(idx) {
  122. if (this.form.goals.length <= 1) return
  123. this.form.goals.splice(idx, 1)
  124. },
  125. async submit() {
  126. // 目标成员校验
  127. var memberId = this.form.memberId || this.selfMemberId
  128. // 目标校验
  129. var validGoals = []
  130. var usedDomains = {}
  131. var hasError = false
  132. for (var i = 0; i < this.form.goals.length; i++) {
  133. var g = this.form.goals[i]
  134. if (!g.content.trim()) {
  135. uni.showToast({ title: '请填写目标 ' + (i + 1) + ' 的内容', icon: 'none' })
  136. hasError = true
  137. break
  138. }
  139. if (usedDomains[g.domain]) {
  140. uni.showToast({ title: '目标领域不能重复', icon: 'none' })
  141. hasError = true
  142. break
  143. }
  144. usedDomains[g.domain] = true
  145. validGoals.push({ domain: g.domain, content: g.content.trim() })
  146. }
  147. if (hasError) return
  148. if (validGoals.length === 0) {
  149. uni.showToast({ title: '请至少填写1个目标', icon: 'none' })
  150. return
  151. }
  152. var data = {
  153. title: this.form.title.trim(),
  154. durationWeeks: this.form.durationWeeks,
  155. goals: validGoals
  156. }
  157. if (memberId && memberId !== this.selfMemberId) {
  158. data.memberId = memberId
  159. }
  160. try {
  161. var res = await createDoaStage(data)
  162. if (res.code === 200) {
  163. uni.showToast({ title: '创建成功', icon: 'success' })
  164. setTimeout(function() {
  165. uni.navigateBack()
  166. }, 600)
  167. } else {
  168. uni.showToast({ title: res.message || '创建失败', icon: 'none' })
  169. }
  170. } catch (e) {
  171. uni.showToast({ title: '创建失败', icon: 'none' })
  172. }
  173. }
  174. }
  175. }
  176. </script>
  177. <style scoped>
  178. .page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
  179. .field { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
  180. .field-label { font-size: 26rpx; color: #475569; font-weight: 600; margin-bottom: 12rpx; display: block; }
  181. .required { color: #EF4444; }
  182. .field-input {
  183. background: #F8FAFC; border: 1rpx solid #E2E8F0; border-radius: 12rpx;
  184. padding: 16rpx 18rpx; font-size: 28rpx; color: #1E293B; margin-top: 12rpx;
  185. }
  186. .member-grid { display: flex; flex-wrap: wrap; gap: 12rpx; }
  187. .member-item {
  188. padding: 10rpx 22rpx; border-radius: 24rpx; font-size: 24rpx;
  189. color: #475569; background: #F1F5F9; border: 2rpx solid transparent;
  190. }
  191. .member-item.active { background: #DCFCE7; color: #16A34A; border-color: #10B981; }
  192. .radio-group { display: flex; gap: 12rpx; margin-top: 12rpx; flex-wrap: wrap; }
  193. .radio-btn {
  194. flex: 1; text-align: center; padding: 12rpx 0; border-radius: 12rpx;
  195. font-size: 26rpx; color: #475569; background: #F1F5F9;
  196. }
  197. .radio-btn.small { flex: none; padding: 8rpx 18rpx; font-size: 22rpx; }
  198. .radio-btn.active { background: #10B981; color: #fff; font-weight: 600; }
  199. .goal-editor { background: #F8FAFC; border-radius: 12rpx; padding: 16rpx; margin-top: 12rpx; }
  200. .goal-domain-row { display: flex; justify-content: space-between; align-items: center; }
  201. .goal-label { font-size: 24rpx; color: #475569; font-weight: 600; }
  202. .goal-remove { font-size: 28rpx; color: #EF4444; padding: 4rpx 12rpx; }
  203. .add-goal {
  204. margin-top: 16rpx; text-align: center; padding: 16rpx 0;
  205. border: 2rpx dashed #10B981; border-radius: 12rpx;
  206. color: #10B981; font-size: 26rpx; font-weight: 600;
  207. }
  208. .btn-submit {
  209. background: linear-gradient(135deg, #10B981, #34D399);
  210. color: #fff; font-size: 30rpx; font-weight: 600;
  211. border-radius: 42rpx; border: none; height: 84rpx; line-height: 84rpx;
  212. margin-top: 8rpx;
  213. }
  214. </style>