Browse Source

feat(signature): S-cfc-137 family.signature.healthPlanSummary 实现使绿 - health-plan-summary.vue 摘要组件落地(计划名/周期/项目/参与人 + confirm 回调),signature.healthPlanSummary.test.js 8红8绿

Sisyphus Agent 4 days ago
parent
commit
0a01a0e8a
1 changed files with 170 additions and 0 deletions
  1. 170 0
      cfc-frontend/pages/signature/components/health-plan-summary.vue

+ 170 - 0
cfc-frontend/pages/signature/components/health-plan-summary.vue

@@ -0,0 +1,170 @@
+<!--
+  健康计划摘要组件(Step 3 签名前确认,Vue 2 Options API / uni-app 小程序兼容)
+  Props:
+    planName       string   计划名
+    cycleText      string   周期说明(如「8 周」/「2026-08-15 ~ 2026-10-15」)
+    planItems      array    项目列表(元素可为字符串或 { name } 对象)
+    participants   array    参与人列表(元素可为字符串或 { name } 对象)
+  Emits:
+    confirm                 点击「确认」按钮时触发(由父页面承接签名/生成方案等后续动作)
+  用法:
+    <health-plan-summary
+      :plan-name="planName"
+      :cycle-text="cycleText"
+      :plan-items="planItems"
+      :participants="participants"
+      @confirm="onConfirm"
+    />
+  约定:
+    - 纯展示组件,数据一律经 props 传入,不发起任何 API 请求(父组件通过 props 传递数据)
+    - 遵守小程序约束:不用可选链 ?.、不用 CSS Grid、:key 用方法调用、日期解析统一走 parseDate
+-->
+<template>
+  <view class="health-plan-summary">
+    <view class="summary-card">
+      <view class="summary-row">
+        <text class="summary-label">计划名</text>
+        <text class="summary-value">{{ planName }}</text>
+      </view>
+      <view class="summary-row">
+        <text class="summary-label">周期</text>
+        <text class="summary-value">{{ cycleText }}</text>
+      </view>
+      <view class="summary-block">
+        <text class="summary-label">项目</text>
+        <view class="summary-tags">
+          <view v-for="(item, index) in planItems" :key="getItemKey(item, index)" class="summary-tag">
+            <text class="summary-tag-text">{{ item && item.name ? item.name : item }}</text>
+          </view>
+          <text v-if="!planItems || planItems.length === 0" class="summary-empty">暂无项目</text>
+        </view>
+      </view>
+      <view class="summary-block">
+        <text class="summary-label">参与人</text>
+        <view class="summary-tags">
+          <view v-for="(participant, index) in participants" :key="getParticipantKey(participant, index)" class="summary-tag">
+            <text class="summary-tag-text">{{ participant && participant.name ? participant.name : participant }}</text>
+          </view>
+          <text v-if="!participants || participants.length === 0" class="summary-empty">暂无参与人</text>
+        </view>
+      </view>
+    </view>
+    <button class="btn-confirm" @click="onConfirm">确认</button>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'HealthPlanSummary',
+  props: {
+    planName: {
+      type: String,
+      default: ''
+    },
+    cycleText: {
+      type: String,
+      default: ''
+    },
+    planItems: {
+      type: Array,
+      default: function() {
+        return []
+      }
+    },
+    participants: {
+      type: Array,
+      default: function() {
+        return []
+      }
+    }
+  },
+  methods: {
+    // 小程序模板不支持带运算符的 :key 绑定 → 统一用方法返回稳定键
+    getItemKey: function(item, index) {
+      if (item && item.id != null) return String(item.id)
+      return String(index)
+    },
+    getParticipantKey: function(participant, index) {
+      if (participant && participant.memberId != null) return String(participant.memberId)
+      if (participant && participant.id != null) return String(participant.id)
+      return String(index)
+    },
+    // 确认回调:仅通知父页面,具体后续(签名/生成方案)由父页面决定
+    onConfirm: function() {
+      this.$emit('confirm')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.health-plan-summary {
+  width: 100%;
+}
+.summary-card {
+  width: 100%;
+  padding: 24rpx;
+  background: $app-surface;
+  border: 2rpx solid $facade-fill-2;
+  border-radius: 16rpx;
+}
+.summary-row {
+  display: flex;
+  align-items: flex-start;
+  padding: 12rpx 0;
+}
+.summary-block {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  padding: 12rpx 0;
+}
+.summary-label {
+  flex-shrink: 0;
+  width: 120rpx;
+  font-size: calc(13px * var(--font-scale, 1));
+  color: $facade-text-muted;
+  line-height: 1.6;
+}
+.summary-value {
+  flex: 1;
+  font-size: calc(14px * var(--font-scale, 1));
+  color: $facade-text-strong;
+  line-height: 1.6;
+}
+.summary-tags {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 8rpx;
+}
+.summary-tag {
+  margin: 0 12rpx 12rpx 0;
+  padding: 8rpx 20rpx;
+  background: $facade-fill-2;
+  border-radius: 24rpx;
+}
+.summary-tag-text {
+  font-size: calc(12px * var(--font-scale, 1));
+  color: $facade-text-body;
+  line-height: 1.4;
+}
+.summary-empty {
+  font-size: calc(12px * var(--font-scale, 1));
+  color: $facade-text-ghost;
+  line-height: 1.6;
+}
+.btn-confirm {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  margin-top: 24rpx;
+  font-size: calc(15px * var(--font-scale, 1));
+  color: $app-surface;
+  background: linear-gradient(135deg, $accent-indigo 0%, $accent-indigo-deep 100%);
+  border: none;
+  border-radius: 24rpx;
+}
+.btn-confirm::after {
+  border: none;
+}
+</style>