Bläddra i källkod

feat(doa): 新增doa-stage-create创建阶段页

Sisyphus 1 månad sedan
förälder
incheckning
62d50d4baf
1 ändrade filer med 223 tillägg och 0 borttagningar
  1. 223 0
      cfc-frontend/pages/health/doa-stage-create.vue

+ 223 - 0
cfc-frontend/pages/health/doa-stage-create.vue

@@ -0,0 +1,223 @@
+<template>
+  <view class="page">
+    <!-- 目标成员(代管时显示,仅未登录成员可选) -->
+    <view class="field" v-if="!isSelfOnly">
+      <text class="field-label">目标成员</text>
+      <view class="member-grid">
+        <view
+          v-for="m in members"
+          :key="m.id"
+          class="member-item"
+          :class="{ active: form.memberId === m.id }"
+          @click="selectMember(m)"
+        >
+          <text>{{ m.nickname }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 阶段主题 -->
+    <view class="field">
+      <text class="field-label">阶段主题(可选)</text>
+      <input class="field-input" v-model="form.title" placeholder="如:暑期习惯养成" maxlength="50" />
+    </view>
+
+    <!-- 周期选择 -->
+    <view class="field">
+      <text class="field-label">阶段周期 <text class="required">*</text></text>
+      <view class="radio-group">
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 2 }" @click="form.durationWeeks = 2">2周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 4 }" @click="form.durationWeeks = 4">4周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 8 }" @click="form.durationWeeks = 8">8周</view>
+        <view class="radio-btn" :class="{ active: form.durationWeeks === 12 }" @click="form.durationWeeks = 12">12周</view>
+      </view>
+    </view>
+
+    <!-- 目标列表(1-3条) -->
+    <view class="field">
+      <text class="field-label">阶段目标(1-3 个,不同领域)</text>
+      <view v-for="(goal, idx) in form.goals" :key="getGoalKey(goal, idx)" class="goal-editor">
+        <view class="goal-domain-row">
+          <text class="goal-label">目标 {{ idx + 1 }} 领域</text>
+          <text class="goal-remove" @click="removeGoal(idx)">✕</text>
+        </view>
+        <view class="radio-group">
+          <view
+            v-for="d in domains"
+            :key="d.value"
+            class="radio-btn small"
+            :class="{ active: goal.domain === d.value }"
+            @click="goal.domain = d.value"
+          >{{ d.label }}</view>
+        </view>
+        <input class="field-input" v-model="goal.content" placeholder="量化目标,如:体重降到 172 以下" maxlength="100" />
+      </view>
+      <view class="add-goal" v-if="form.goals.length < 3" @click="addGoal">+ 添加目标</view>
+    </view>
+
+    <button class="btn-submit" @click="submit">创建阶段</button>
+  </view>
+</template>
+
+<script>
+import { createDoaStage, getFamilyMemberList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      members: [],
+      selfMemberId: 0,
+      targetMemberId: 0,
+      hasLocalMembers: false,
+      domains: [
+        { value: 'health', label: '健康' },
+        { value: 'wealth', label: '事业' },
+        { value: 'family', label: '家庭关系' },
+        { value: 'social', label: '社会关系' },
+        { value: 'help', label: '帮助他人' }
+      ],
+      form: {
+        memberId: 0,
+        title: '',
+        durationWeeks: 4,
+        goals: [{ domain: 'health', content: '' }]
+      }
+    }
+  },
+  computed: {
+    isSelfOnly() {
+      // 只为自己创建时(未传入 memberId 且无可代管成员)
+      return this.targetMemberId === 0 && !this.hasLocalMembers
+    }
+  },
+  onLoad(options) {
+    this.selfMemberId = uni.getStorageSync('currentMemberId') || 0
+    if (options.memberId) {
+      this.targetMemberId = parseInt(options.memberId, 10)
+      this.form.memberId = parseInt(options.memberId, 10)
+    }
+    this.loadMembers()
+  },
+  methods: {
+    getGoalKey(goal, idx) {
+      return goal.domain + '_' + idx
+    },
+    async loadMembers() {
+      try {
+        var res = await getFamilyMemberList({ visibleOnly: true })
+        var list = res.data || []
+        this.members = list
+        this.hasLocalMembers = list.some(function(m) { return !m.userId })
+        // 若从总览"为他创建"进入且目标成员未登录 → 直接锁定
+        if (this.targetMemberId > 0) {
+          this.form.memberId = this.targetMemberId
+        }
+      } catch (e) {
+        this.members = []
+        this.hasLocalMembers = false
+      }
+    },
+    selectMember(m) {
+      this.form.memberId = m.id
+    },
+    addGoal() {
+      if (this.form.goals.length >= 3) return
+      this.form.goals.push({ domain: 'health', content: '' })
+    },
+    removeGoal(idx) {
+      if (this.form.goals.length <= 1) return
+      this.form.goals.splice(idx, 1)
+    },
+    async submit() {
+      // 目标成员校验
+      var memberId = this.form.memberId || this.selfMemberId
+      // 目标校验
+      var validGoals = []
+      var usedDomains = {}
+      var hasError = false
+      for (var i = 0; i < this.form.goals.length; i++) {
+        var g = this.form.goals[i]
+        if (!g.content.trim()) {
+          uni.showToast({ title: '请填写目标 ' + (i + 1) + ' 的内容', icon: 'none' })
+          hasError = true
+          break
+        }
+        if (usedDomains[g.domain]) {
+          uni.showToast({ title: '目标领域不能重复', icon: 'none' })
+          hasError = true
+          break
+        }
+        usedDomains[g.domain] = true
+        validGoals.push({ domain: g.domain, content: g.content.trim() })
+      }
+      if (hasError) return
+      if (validGoals.length === 0) {
+        uni.showToast({ title: '请至少填写1个目标', icon: 'none' })
+        return
+      }
+
+      var data = {
+        title: this.form.title.trim(),
+        durationWeeks: this.form.durationWeeks,
+        goals: validGoals
+      }
+      if (memberId && memberId !== this.selfMemberId) {
+        data.memberId = memberId
+      }
+
+      try {
+        var res = await createDoaStage(data)
+        if (res.code === 200) {
+          uni.showToast({ title: '创建成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 600)
+        } else {
+          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page { min-height: 100vh; background: #F5F7FA; padding: 24rpx; }
+.field { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
+.field-label { font-size: 26rpx; color: #475569; font-weight: 600; margin-bottom: 12rpx; display: block; }
+.required { color: #EF4444; }
+.field-input {
+  background: #F8FAFC; border: 1rpx solid #E2E8F0; border-radius: 12rpx;
+  padding: 16rpx 18rpx; font-size: 28rpx; color: #1E293B; margin-top: 12rpx;
+}
+.member-grid { display: flex; flex-wrap: wrap; gap: 12rpx; }
+.member-item {
+  padding: 10rpx 22rpx; border-radius: 24rpx; font-size: 24rpx;
+  color: #475569; background: #F1F5F9; border: 2rpx solid transparent;
+}
+.member-item.active { background: #DCFCE7; color: #16A34A; border-color: #10B981; }
+.radio-group { display: flex; gap: 12rpx; margin-top: 12rpx; flex-wrap: wrap; }
+.radio-btn {
+  flex: 1; text-align: center; padding: 12rpx 0; border-radius: 12rpx;
+  font-size: 26rpx; color: #475569; background: #F1F5F9;
+}
+.radio-btn.small { flex: none; padding: 8rpx 18rpx; font-size: 22rpx; }
+.radio-btn.active { background: #10B981; color: #fff; font-weight: 600; }
+.goal-editor { background: #F8FAFC; border-radius: 12rpx; padding: 16rpx; margin-top: 12rpx; }
+.goal-domain-row { display: flex; justify-content: space-between; align-items: center; }
+.goal-label { font-size: 24rpx; color: #475569; font-weight: 600; }
+.goal-remove { font-size: 28rpx; color: #EF4444; padding: 4rpx 12rpx; }
+.add-goal {
+  margin-top: 16rpx; text-align: center; padding: 16rpx 0;
+  border: 2rpx dashed #10B981; border-radius: 12rpx;
+  color: #10B981; font-size: 26rpx; font-weight: 600;
+}
+.btn-submit {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff; font-size: 30rpx; font-weight: 600;
+  border-radius: 42rpx; border: none; height: 84rpx; line-height: 84rpx;
+  margin-top: 8rpx;
+}
+</style>