Просмотр исходного кода

feat(frontend): 新增 DimensionMemberTabs 五维成员标签条组件

被身/心/智/富维度页引用(替换原 FamilyMemberStrip),提供成员切换、
长按管理、管理入口三个交互;触摸目标 >= 88rpx,维度色取 uni.scss 令牌。
liaoxg 1 неделя назад
Родитель
Сommit
941aa8999f
1 измененных файлов с 290 добавлено и 0 удалено
  1. 290 0
      cfc-frontend/components/DimensionMemberTabs.vue

+ 290 - 0
cfc-frontend/components/DimensionMemberTabs.vue

@@ -0,0 +1,290 @@
+<template>
+  <!--
+    五维页 · 成员标签条(DimensionMemberTabs)
+    用途:已登录家庭用户在身/心/智/富页通过点击标签切换成员,查看该成员各维度数值。
+    视觉:chip 形态(比大卡更轻,适合作为 PageBanner 下的筛选器);选中态用所属维度色渐变。
+    交互:点击切换(emit select);长按唤出管理菜单(emit longpress);右侧管理入口(emit manage)。
+    规范:触摸目标 >= 88rpx;维度色取 uni.scss 令牌(编译期内联,规避 mp-weixin CSS var 不稳)。
+  -->
+  <view class="member-tabs" :class="'dim-' + (dimensionCode || 'body')">
+    <scroll-view
+      class="tabs-scroll"
+      scroll-x
+      :enhanced="true"
+      :scroll-with-animation="true"
+      show-scrollbar="false"
+      v-if="members && members.length > 0"
+    >
+      <view class="tabs-inner">
+        <view
+          v-for="m in members"
+          :key="m.id"
+          class="member-tab"
+          :class="{ 'is-selected': String(m.id) === String(selectedMemberId), 'is-self': m.isSelf }"
+          @click="onTap(m)"
+          @longpress="onLongPress(m)"
+        >
+          <view class="tab-avatar-wrap">
+            <image class="tab-avatar" :src="m.avatar || defaultAvatar" mode="aspectFill" />
+            <view class="tab-self-mark" v-if="m.isSelf">我</view>
+          </view>
+          <view class="tab-body">
+            <text class="tab-name">{{ m.nickname || '成员' }}</text>
+            <view class="tab-meta">
+              <text class="tab-relation">{{ relationText(m) }}</text>
+              <text class="tab-score" v-if="scoreOf(m) !== null">{{ scoreOf(m) }}<text class="tab-score-unit">分</text></text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 管理入口 -->
+        <view class="tab-manage" @click="onManage" v-if="members.length > 1">
+          <text class="tab-manage-icon">管理</text>
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 空态:未创建家庭 / 无可见成员 -->
+    <view class="tabs-empty" v-else>
+      <text class="tabs-empty-text">创建家庭后,可在这里切换查看每位家人的维度数值</text>
+      <view class="tabs-empty-btn" @click="onManage">+ 管理家庭成员</view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'DimensionMemberTabs',
+  props: {
+    members: { type: Array, default: function() { return [] } },
+    selectedMemberId: { type: [Number, String], default: null },
+    // 所属维度码:body / mind / wisdom / wealth / action,决定选中态配色
+    dimensionCode: { type: String, default: 'body' },
+    // { memberId: score },用于在标签上直接显示该成员的维度分数
+    scoreMap: { type: Object, default: function() { return {} } }
+  },
+  data: function() {
+    return { defaultAvatar: '/static/default-avatar.png' }
+  },
+  computed: {
+    dimensionList: function() {
+      return this.members || []
+    }
+  },
+  methods: {
+    relationText: function(m) {
+      if (!m) return '成员'
+      return m.relationship || m.relativeLabel || m.roleLabel || (m.isSelf ? '本人' : '成员')
+    },
+    scoreOf: function(m) {
+      if (!m || !this.scoreMap) return null
+      var v = this.scoreMap[m.id]
+      if (v === undefined || v === null || v === '') return null
+      var n = Number(v)
+      return isNaN(n) ? null : Math.round(n)
+    },
+    onTap: function(m) {
+      if (m && String(m.id) === String(this.selectedMemberId)) return
+      this.$emit('select', m)
+    },
+    onLongPress: function(m) {
+      this.$emit('longpress', m)
+    },
+    onManage: function() {
+      this.$emit('manage')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+// 维度色映射:选中态渐变用主色 -> 深阶,五维统一从 uni.scss 取令牌
+$tab-dims: (
+  body:   $dim-body,
+  mind:   $dim-mind,
+  wisdom: $dim-wisdom,
+  wealth: $dim-wealth,
+  action: $dim-action
+);
+
+.member-tabs {
+  padding: 8rpx 30rpx 4rpx;
+}
+
+.tabs-scroll {
+  width: 100%;
+  white-space: nowrap;
+}
+
+.tabs-inner {
+  display: inline-flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 16rpx;
+  padding: 4rpx 0;
+}
+
+/* ---- 成员标签(chip) ---- */
+.member-tab {
+  display: inline-flex;
+  flex-direction: row;
+  align-items: center;
+  min-height: 88rpx;
+  padding: 10rpx 22rpx 10rpx 12rpx;
+  background: $app-surface;
+  border: 2rpx solid $app-border-subtle;
+  border-radius: 44rpx;
+  transition: border-color $dur-state $ease-standard, background $dur-state $ease-standard, transform $dur-press $ease-standard;
+  flex-shrink: 0;
+
+  &:active {
+    transform: scale(0.97);
+    opacity: $press-opacity;
+  }
+}
+
+.tab-avatar-wrap {
+  position: relative;
+  flex-shrink: 0;
+}
+
+.tab-avatar {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: $app-surface-subtle;
+}
+
+.tab-self-mark {
+  position: absolute;
+  right: -6rpx;
+  bottom: -6rpx;
+  min-width: 36rpx;
+  height: 36rpx;
+  padding: 0 8rpx;
+  border-radius: 18rpx;
+  background: $brand-primary;
+  color: $app-surface;
+  font-size: 20rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-body {
+  display: flex;
+  flex-direction: column;
+  margin-left: 14rpx;
+  max-width: 200rpx;
+}
+
+.tab-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: $app-text;
+  line-height: 1.3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.tab-meta {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 2rpx;
+  gap: 10rpx;
+}
+
+.tab-relation {
+  font-size: 20rpx;
+  color: $app-text-muted;
+  line-height: 1.2;
+}
+
+.tab-score {
+  font-size: 22rpx;
+  font-weight: 700;
+  color: $app-text-secondary;
+  line-height: 1.2;
+}
+
+.tab-score-unit {
+  font-size: 18rpx;
+  font-weight: 400;
+  margin-left: 1rpx;
+}
+
+/* ---- 管理入口 ---- */
+.tab-manage {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 88rpx;
+  min-width: 88rpx;
+  padding: 0 20rpx;
+  border-radius: 44rpx;
+  background: $app-surface-subtle;
+  flex-shrink: 0;
+
+  &:active {
+    opacity: $press-opacity;
+  }
+}
+
+.tab-manage-icon {
+  font-size: 24rpx;
+  color: $app-text-secondary;
+  font-weight: 500;
+}
+
+/* ---- 空态 ---- */
+.tabs-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  padding: 24rpx 28rpx;
+  background: $app-surface;
+  border-radius: 16rpx;
+  border: 2rpx dashed $app-border;
+}
+
+.tabs-empty-text {
+  font-size: 24rpx;
+  color: $app-text-secondary;
+  line-height: 1.5;
+}
+
+.tabs-empty-btn {
+  margin-top: 16rpx;
+  padding: 12rpx 32rpx;
+  border-radius: 40rpx;
+  background: $brand-primary;
+  color: $app-surface;
+  font-size: 26rpx;
+  font-weight: 500;
+
+  &:active {
+    opacity: $press-opacity;
+  }
+}
+
+/* ---- 选中态:按维度着色 ---- */
+@each $code, $color in $tab-dims {
+  .dim-#{$code} .member-tab.is-selected {
+    background: linear-gradient(135deg, $color, rgba(red($color), green($color), blue($color), 0.82));
+    border-color: $color;
+
+    .tab-name,
+    .tab-relation,
+    .tab-score {
+      color: $app-surface;
+    }
+
+    .tab-avatar {
+      border: 2rpx solid $app-surface;
+    }
+  }
+}
+</style>