Przeglądaj źródła

feat(frontend): 成员条空状态引导 + 长按菜单升级关系现状Action Sheet

Sisyphus 1 miesiąc temu
rodzic
commit
6f38d2c209
1 zmienionych plików z 323 dodań i 704 usunięć
  1. 323 704
      cfc-frontend/components/FamilyMemberStrip.vue

+ 323 - 704
cfc-frontend/components/FamilyMemberStrip.vue

@@ -1,226 +1,223 @@
 <template>
-  <view class="member-strip">
-    <!-- 浮标:家庭图标(默认态)→ 当前成员头像(展开态) -->
-    <view class="fab-wrap">
-      <view class="fab-avatar" :class="{ active: fanOpen }" @tap="toggleFan">
-        <!-- 默认态:家庭图标 -->
-        <view class="fab-icon-default" v-if="!fanOpen">
-          <text class="fab-icon-emoji">👨‍👩‍👧‍👦</text>
-        </view>
-        <!-- 展开态:当前成员头像 -->
-        <image class="fab-img" :class="{ 'fab-img-show': fanOpen }" :src="currentMember.avatar || defaultAvatar" mode="aspectFill" />
-        <view class="fab-ring" :class="{ 'ring-active': fanOpen }"></view>
-        <view class="fab-badge" v-if="pendingCount > 0 && isAdmin" @tap.stop="openRequests">
-          <text class="badge-count">{{ pendingCount }}</text>
+  <view class="family-member-strip">
+    <!-- 有成员:滚动条 / 居中模式 + 长按菜单 -->
+    <view v-if="renderedMembers && renderedMembers.length > 0">
+      <scroll-view
+        class="strip-scroll"
+        scroll-x
+        :scroll-with-animation="true"
+        :enhanced="true"
+        show-scrollbar="false"
+        v-if="renderedMembers.length >= 4"
+      >
+        <view class="strip-inner scroll-mode">
+          <FamilyMemberCard
+            v-for="member in renderedMembers"
+            :key="member.id"
+            :member="member"
+            :isSelected="member.id === selectedMemberId"
+            :energyScore="getMemberEnergy(member)"
+            @select="onSelect"
+            @longpress="onLongPress"
+          />
         </view>
+      </scroll-view>
+      <view class="strip-inner center-mode" v-else>
+        <FamilyMemberCard
+          v-for="member in renderedMembers"
+          :key="member.id"
+          :member="member"
+          :isSelected="member.id === selectedMemberId"
+          :energyScore="getMemberEnergy(member)"
+          @select="onSelect"
+          @longpress="onLongPress"
+        />
       </view>
-      <view class="fab-pulse" v-if="!fanOpen"></view>
-    </view>
 
-    <!-- 扇形展开成员 -->
-    <view v-if="fanOpen" class="fan-wrap show">
-      <view class="fan-overlay" @tap="closeFan"></view>
-      <view class="fan-container">
-        <view
-          class="fan-card"
-          v-for="(member, index) in members"
-          :key="member.id"
-          :style="getFanPosition(member, index)"
-          :class="{ selected: member.id === selectedMemberId }"
-          @tap="selectMember(member)"
-          @longpress="onFanLongPress(member)"
-        >
-          <view class="fan-avatar-wrap">
-            <image class="fan-avatar" :src="member.avatar || defaultAvatar" mode="aspectFill" />
-            <view class="fan-relation-badge" :style="{ background: getRelationColor(member) }">
-              <text class="fan-relation-text">{{ member.relationship || member.relativeLabel || member.roleLabel || '成员' }}</text>
-            </view>
-            <view class="fan-spouse-mark" v-if="member.isSpouse">
-              <text class="spouse-icon">💍</text>
+      <!-- 长按操作菜单 -->
+      <view class="action-overlay" v-if="showActionMenu" @click="closeMenu"></view>
+      <view class="action-sheet" v-if="showActionMenu">
+        <view class="action-header">
+          <image class="action-avatar" :src="actionMember ? (actionMember.avatar || '/static/default-avatar.png') : ''" mode="aspectFill" />
+          <view class="action-header-info">
+            <text class="action-title">{{ actionMember ? actionMember.nickname : '' }}</text>
+            <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || actionMember.roleLabel || '') : '' }}</text>
+          </view>
+        </view>
+        <!-- 关系现状区块 -->
+        <view class="action-scores" v-if="actionMember">
+          <view class="score-row">
+            <text class="score-label">信任度</text>
+            <view class="score-bar-track">
+              <view class="score-bar-fill trust" :style="{ width: scoreWidths[0] }"></view>
             </view>
+            <text class="score-value">{{ scoreValues[0] }}</text>
           </view>
-          <text class="fan-nickname">{{ member.nickname }}</text>
-          <view class="fan-score" v-if="getMemberEnergy(member) !== null">
-            <text class="score-value">{{ getMemberEnergy(member) }}</text>
-            <text class="score-unit">分</text>
+          <view class="score-row">
+            <text class="score-label">亲密度</text>
+            <view class="score-bar-track">
+              <view class="score-bar-fill intimacy" :style="{ width: scoreWidths[1] }"></view>
+            </view>
+            <text class="score-value">{{ scoreValues[1] }}</text>
           </view>
-          <view class="fan-selected-mark" v-if="member.id === selectedMemberId">
-            <text class="mark-icon">✓</text>
+          <view class="score-row">
+            <text class="score-label">沟通质量</text>
+            <view class="score-bar-track">
+              <view class="score-bar-fill communication" :style="{ width: scoreWidths[2] }"></view>
+            </view>
+            <text class="score-value">{{ scoreValues[2] }}</text>
           </view>
         </view>
-      </view>
-    </view>
-
-    <!-- 长按/点击浮标弹出的个人菜单 -->
-    <view class="action-overlay" v-if="showActionMenu" @tap="closeMenu"></view>
-    <view class="action-sheet" v-if="showActionMenu">
-      <view class="action-header">
-        <view class="action-avatar-wrap">
-          <image class="action-avatar" :src="actionMember ? (actionMember.avatar || defaultAvatar) : (currentMember.avatar || defaultAvatar)" mode="aspectFill" />
+        <view class="action-divider"></view>
+        <view class="action-item" @click="addMember" v-if="actionMember && actionMember.isSelf">
+          <text class="action-item-icon">➕</text>
+          <text class="action-item-text">添加成员</text>
         </view>
-        <view class="action-info">
-          <text class="action-title">{{ actionMember ? actionMember.nickname : (currentMember.nickname || '我') }}</text>
-          <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || '') : '' }}</text>
+        <view class="action-item" @click="inviteMember" v-if="actionMember && actionMember.isSelf">
+          <text class="action-item-icon">📨</text>
+          <text class="action-item-text">邀请成员</text>
+        </view>
+        <view class="action-item" @click="switchToMember" v-if="canSwitch">
+          <text class="action-item-icon">🔄</text>
+          <text class="action-item-text">切换到该成员视角</text>
+        </view>
+        <view class="action-item" @click="editMember" v-if="canEdit">
+          <text class="action-item-icon">✏️</text>
+          <text class="action-item-text">编辑资料</text>
+        </view>
+        <view class="action-item" @click="questionnaireMember">
+          <text class="action-item-icon">📋</text>
+          <text class="action-item-text">关系问卷</text>
+          <text class="action-time" v-if="lastQuestionnaireText">  ·  上次填写 {{ lastQuestionnaireText }}</text>
+        </view>
+        <view class="action-item" @click="interactionMember">
+          <text class="action-item-icon">💬</text>
+          <text class="action-item-text">记录互动</text>
+        </view>
+        <view class="action-item" @click="callMember" v-if="actionMember && actionMember.phone">
+          <text class="action-item-icon">📞</text>
+          <text class="action-item-text">拨打电话</text>
+        </view>
+        <view class="action-item action-danger" @click="removeMember" v-if="canRemove">
+          <text class="action-item-icon">🗑️</text>
+          <text class="action-item-text">移除成员</text>
+        </view>
+        <view class="action-item action-cancel" @click="closeMenu">
+          <text class="action-item-text">取消</text>
         </view>
-      </view>
-      <view class="action-item" @tap="switchToMember" v-if="canSwitch">
-        <text class="action-item-icon">🔄</text>
-        <text class="action-item-text">切换到该成员视角</text>
-      </view>
-      <view class="action-item" @tap="editMember" v-if="canEdit">
-        <text class="action-item-icon">✏️</text>
-        <text class="action-item-text">编辑资料</text>
-      </view>
-      <view class="action-item" @tap="questionnaireMember">
-        <text class="action-item-icon">📋</text>
-        <text class="action-item-text">关系问卷</text>
-      </view>
-      <view class="action-item" @tap="interactionMember">
-        <text class="action-item-icon">💬</text>
-        <text class="action-item-text">记录互动</text>
-      </view>
-      <view class="action-item" @tap="callMember" v-if="actionMember && actionMember.phone">
-        <text class="action-item-icon">📞</text>
-        <text class="action-item-text">拨打电话</text>
-      </view>
-      <view class="action-item action-danger" @tap="removeMember" v-if="canRemove">
-        <text class="action-item-icon">🗑️</text>
-        <text class="action-item-text">移除成员</text>
-      </view>
-      <view class="action-item action-cancel" @tap="closeMenu">
-        <text class="action-item-text">取消</text>
       </view>
     </view>
 
-    <!-- 待审批请求弹窗 -->
-    <view class="request-overlay" v-if="showRequests" @tap="closeRequests"></view>
-    <view class="request-sheet" v-if="showRequests">
-      <view class="sheet-header">
-        <text class="sheet-title">待处理加入申请({{ pendingRequests.length }})</text>
-        <text class="sheet-close" @tap="closeRequests">×</text>
-      </view>
-      <view v-if="pendingRequests.length === 0" class="empty-requests">
-        <text>暂无待处理的请求</text>
-      </view>
-      <view class="request-item" v-for="req in pendingRequests" :key="req.id">
-        <image class="req-avatar" src="/static/default-avatar.png" mode="aspectFill"></image>
-        <view class="req-info">
-          <text class="req-name">{{ req.requesterNickname }}</text>
-          <text class="req-time">{{ req.createdAt }}</text>
-        </view>
-      <view class="req-actions">
-        <button class="req-btn btn-approve" @tap="approveRequest(req.id)">通过</button>
-          <button class="req-btn btn-reject" @tap="rejectRequest(req.id)">拒绝</button>
-        </view>
+    <!-- 未创建家庭引导 -->
+    <view class="family-strip-empty" v-else>
+      <view class="empty-content">
+        <text class="empty-icon">🏠</text>
+        <text class="empty-title">还未创建家庭</text>
+        <text class="empty-desc">创建家庭,开启全家幸福之旅</text>
+        <view class="empty-btn" @click="onCreateFamily">+ 创建家庭</view>
       </view>
     </view>
+
+    <!-- 邀请成员分享海报弹窗 -->
+    <SharePoster
+      :show="showPoster"
+      :referralCode="inviteCode"
+      :qrCodeBase64="inviteQrCode"
+      :nickname="posterNickname"
+      :userAvatar="posterAvatar"
+      @close="showPoster = false" />
   </view>
 </template>
 
 <script>
-import { kickFamilyMemberById, getPendingFamilyRequests, approveFamilyJoinRequest, rejectFamilyJoinRequest } from '@/utils/api'
+import FamilyMemberCard from '@/components/FamilyMemberCard'
+import SharePoster from '@/components/SharePoster.vue'
+import { kickFamilyMemberById, createFamily, generateFamilyInviteQrCode, getPairwiseRelationships } from '@/utils/api'
+import { parseDate } from '@/utils/format.js'
 
 export default {
+  components: { FamilyMemberCard, SharePoster },
   props: {
     members: { type: Array, default: function() { return [] } },
     selectedMemberId: { type: Number, default: null },
     energyMap: { type: Object, default: function() { return {} } },
     isParent: { type: Boolean, default: false },
-    isAdmin: { type: Boolean, default: false }
+    centerMemberId: { type: Number, default: null }
   },
   data: function() {
     return {
-      fanOpen: false,
       showActionMenu: false,
       actionMember: null,
-      pendingCount: 0,
-      pendingRequests: [],
-      showRequests: false
+      renderedMembers: [],
+      creatingFamily: false,
+      showPoster: false,
+      inviteCode: '',
+      inviteQrCode: '',
+      posterNickname: '',
+      posterAvatar: ''
     }
   },
-  mounted: function() {
-    this.loadPendingRequests()
-  },
-   computed: {
-    defaultAvatar: function() {
-      return '/static/default-avatar.png'
+  watch: {
+    members: {
+      handler: function(val) {
+        this.renderedMembers = val || []
+      },
+      immediate: true
     },
-    currentMember: function() {
-      if (!this.members || this.members.length === 0) return {}
-      for (var i = 0; i < this.members.length; i++) {
-        if (this.members[i].id === this.selectedMemberId) return this.members[i]
+    centerMemberId: function(val) {
+      if (val) {
+        this.loadPairwise(val)
       }
-      return this.members[0]
-    },
+    }
+  },
+  computed: {
     canSwitch: function() {
-      var target = this.actionMember || this.currentMember
-      return target && target.userId && target.source === 'family_member'
+      // 仅家庭成员记录(source=family_member)且已关联账号时才能切换视角;
+      // 家长卡片(source=parent)是 users 表记录,switch 接口只查 family_members 表,切换会失败
+      return this.actionMember && this.actionMember.userId && this.actionMember.source === 'family_member'
     },
     canEdit: function() {
-      var target = this.actionMember || this.currentMember
-      return this.isParent && target && !target.userId && target.source === 'family_member'
+      return this.isParent && this.actionMember && !this.actionMember.userId && this.actionMember.source === 'family_member'
     },
     canRemove: function() {
-      var target = this.actionMember || this.currentMember
-      return this.isParent && target && !target.isSelf && target.source === 'family_member'
-    }
-  },
-  methods: {
-    // ===== 扇形展开 =====
-    toggleFan: function(e) {
-      if (this.fanOpen) {
-        this.closeFan()
-      } else {
-        this.fanOpen = true
-      }
-    },
-    closeFan: function() {
-      this.fanOpen = false
+      // 仅家庭成员记录可移除(kick 接口只查 family_members 表),且不能移除自己
+      return this.isParent && this.actionMember && !this.actionMember.isSelf && this.actionMember.source === 'family_member'
     },
-    // 计算扇形位置: 头像在左边,成员向右扇形排列
-    // 用固定的偏移量数组,避免用 transform-origin 的兼容问题
-    getFanPosition: function(member, index) {
-      if (!this.fanOpen) return 'transform: translate(0rpx, 0rpx) rotate(0deg);'
-      var total = this.members.length
-      var angleRange = 50
-      var steps = Math.max(total - 1, 1)
-      var angle = -angleRange + (index / steps) * angleRange * 2
-      var rad = angle * Math.PI / 180
-      var radius = 220
-      var dx = Math.cos(rad) * radius
-      var dy = Math.sin(rad) * radius
-      var rotation = angle * 0.3
-      return 'transform: translate(' + dx + 'rpx,' + dy + 'rpx) rotate(' + rotation + 'deg);'
+    // 关系现状三项宽度/数值(下标 0=信任度 1=亲密度 2=沟通质量)
+    actionScores: function() {
+      var m = this.actionMember
+      if (!m) return []
+      var keys = ['trustScore', 'intimacyScore', 'communicationScore']
+      return keys.map(function(k) {
+        var v = Number(m[k]) || 0
+        return { width: Math.max(0, Math.min(100, v)) + '%', value: Math.round(v) }
+      })
     },
-    getMemberEnergy: function(member) {
-      if (!member || !this.energyMap) return null
-      return this.energyMap[member.id] || this.energyMap[member.memberId] || null
+    scoreWidths: function() {
+      return this.actionScores.map(function(s) { return s.width })
     },
-    getRelationColor: function(member) {
-      var rel = member && member.relationship
-      if (rel === '爸爸' || rel === '妈妈' || rel === '父亲' || rel === '母亲') return '#F97316'
-      if (rel === '儿子' || rel === '女儿') return '#0EA5E9'
-      if (rel === '爷爷' || rel === '奶奶' || rel === '外公' || rel === '外婆') return '#6B7280'
-      if (rel === '哥哥' || rel === '姐姐' || rel === '弟弟' || rel === '妹妹') return '#10B981'
-      if (member && member.isSpouse) return '#EC4899'
-      var role = member && member.effectiveRole
-      if (role === 'parent') return '#F97316'
-      if (role === 'child') return '#0EA5E9'
-      if (role === 'teacher') return '#8B5CF6'
-      return '#9CA3AF'
+    scoreValues: function() {
+      return this.actionScores.map(function(s) { return s.value })
     },
-
-    // ===== 成员选择 =====
-    selectMember: function(member) {
+    // 上次填写时间(问卷提交时 updateMemberScores 已更新 lastInteractionAt)
+    lastQuestionnaireText: function() {
+      var m = this.actionMember
+      if (!m || !m.lastInteractionAt) return ''
+      var d = parseDate(m.lastInteractionAt)
+      if (!d) return ''
+      var now = new Date()
+      var diffMs = now.getTime() - d.getTime()
+      var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
+      if (diffDays === 0) return '今天'
+      if (diffDays === 1) return '昨天'
+      if (diffDays < 7) return diffDays + '天前'
+      if (diffDays < 30) return Math.floor(diffDays / 7) + '周前'
+      return Math.floor(diffDays / 30) + '个月前'
+    }
+  },
+  methods: {
+    onSelect: function(member) {
       this.$emit('select', member)
-      // 关闭扇形
-      this.fanOpen = false
-    },
-
-    // ===== 个人菜单 =====
-    onFanLongPress: function(member) {
-      this.actionMember = member
-      this.showActionMenu = true
     },
     onLongPress: function(member) {
       this.actionMember = member
@@ -231,39 +228,35 @@ export default {
       this.actionMember = null
     },
     switchToMember: function() {
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member && member.userId) {
         this.$emit('switch', member)
       }
     },
     editMember: function() {
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member) {
-        var params = 'memberId=' + member.id + '&nickname=' + encodeURIComponent(member.nickname || '')
-        if (member.gender) params += '&gender=' + member.gender
-        if (member.phone) params += '&phone=' + member.phone
-        if (member.birthday) params += '&birthday=' + member.birthday
-        uni.navigateTo({ url: '/pages/family/add-member?' + params })
+        uni.navigateTo({ url: '/pages/family/add-member?memberId=' + member.id })
       }
     },
     questionnaireMember: function() {
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member) {
         uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire?memberId=' + member.id })
       }
     },
     interactionMember: function() {
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member) {
         uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + member.id })
       }
     },
     callMember: function() {
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member && member.phone) {
         uni.makePhoneCall({ phoneNumber: member.phone })
@@ -271,7 +264,7 @@ export default {
     },
     removeMember: function() {
       var self = this
-      var member = this.actionMember || this.currentMember
+      var member = this.actionMember
       this.closeMenu()
       if (member) {
         uni.showModal({
@@ -284,6 +277,7 @@ export default {
                 .then(function() {
                   uni.hideLoading()
                   uni.showToast({ title: '已移除', icon: 'success' })
+                  // 通知父组件刷新成员列表(替代无效的 globalData 标志)
                   self.$emit('removed', member)
                 })
                 .catch(function(err) {
@@ -295,350 +289,108 @@ export default {
         })
       }
     },
-
-    // ===== 待审批请求 =====
-    loadPendingRequests: function() {
+    getMemberEnergy: function(member) {
+      if (!member || !this.energyMap) return null
+      return this.energyMap[member.id] || this.energyMap[member.memberId] || null
+    },
+    onCreateFamily: function() {
       var self = this
-      if (!this.isAdmin) return
-      getPendingFamilyRequests()
+      if (this.creatingFamily) return
+      this.creatingFamily = true
+      // 家庭名称使用当前用户昵称 + '的家庭'(与 user-edit.vue createFamilyAction 一致)
+      var userInfo = uni.getStorageSync('userInfo')
+      var nickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || '我的'
+      uni.showLoading({ title: '创建中...' })
+      createFamily(nickname + '的家庭')
         .then(function(res) {
+          uni.hideLoading()
+          self.creatingFamily = false
           if (res.code === 200) {
-            var data = res.data || []
-            self.pendingCount = data.length
-            self.pendingRequests = data
+            uni.showToast({ title: '家庭创建成功', icon: 'success' })
+            self.$emit('created')
+          } else {
+            uni.showToast({ title: res.message || '创建失败', icon: 'none' })
           }
         })
-        .catch(function() {
-          // 静默失败
+        .catch(function(err) {
+          uni.hideLoading()
+          self.creatingFamily = false
+          uni.showToast({ title: (err && err.message) || '创建失败', icon: 'none' })
         })
     },
-    openRequests: function() {
-      this.showRequests = true
-    },
-    closeRequests: function() {
-      this.showRequests = false
+    addMember: function() {
+      this.closeMenu()
+      uni.navigateTo({ url: '/pages/family/add-member' })
     },
-    approveRequest: function(requestId) {
+    // 邀请成员:弹窗展示分享海报(与 family-members.vue onInvite 一致的模式)
+    inviteMember: function() {
       var self = this
-      uni.showModal({
-        title: '确认通过',
-        content: '确认通过该成员的加入申请?',
-        success: function(modalRes) {
-          if (modalRes.confirm) {
-            approveFamilyJoinRequest(requestId)
-              .then(function() {
-                uni.showToast({ title: '已通过', icon: 'success' })
-                self.loadPendingRequests()
-                self.closeRequests()
-              })
-              .catch(function(err) {
-                uni.showToast({ title: err.message || '操作失败', icon: 'none' })
-              })
+      this.closeMenu()
+      var userInfo = uni.getStorageSync('userInfo')
+      this.posterNickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || ''
+      this.posterAvatar = uni.getStorageSync('avatar') || (userInfo && userInfo.avatar) || ''
+      this.inviteCode = ''
+      this.inviteQrCode = ''
+      uni.showLoading({ title: '生成邀请...' })
+      generateFamilyInviteQrCode()
+        .then(function(res) {
+          uni.hideLoading()
+          if (res.code === 200 && res.data) {
+            self.inviteCode = res.data.inviteCode
+            self.inviteQrCode = res.data.qrCodeBase64
+            self.showPoster = true
+          } else {
+            uni.showToast({ title: res.message || '生成失败', icon: 'none' })
           }
-        }
-      })
+        })
+        .catch(function() {
+          uni.hideLoading()
+          uni.showToast({ title: '生成失败', icon: 'none' })
+        })
     },
-    rejectRequest: function(requestId) {
+    loadPairwise: function(centerMemberId) {
       var self = this
-      uni.showModal({
-        title: '确认拒绝',
-        content: '确认拒绝该成员的加入申请?',
-        success: function(modalRes) {
-          if (modalRes.confirm) {
-            rejectFamilyJoinRequest(requestId)
-              .then(function() {
-                uni.showToast({ title: '已拒绝', icon: 'success' })
-                self.loadPendingRequests()
-                self.closeRequests()
-              })
-              .catch(function(err) {
-                uni.showToast({ title: err.message || '操作失败', icon: 'none' })
-              })
-          }
+      getPairwiseRelationships(centerMemberId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          var arr = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
+          self.renderedMembers = arr
         }
-      })
+      }).catch(function() {})
     }
   }
 }
 </script>
 
 <style scoped>
-.member-strip {
-  /* FAB 悬浮在左侧边缘中部偏下;禁用 transform(transform 会创建 containing block,
-     使后续 .fan-overlay (position: fixed) 退化为相对本容器定位,全屏遮罩失效) */
-  position: fixed;
-  bottom: 45%;
-  left: 16rpx;
-  z-index: 100;
+.family-member-strip {
+  margin: 16rpx 0;
+  padding: 0 30rpx;
 }
-
-/* ===== 浮标 ===== */
-.fab-wrap {
-  position: relative;
-  display: inline-block;
-  margin: 4rpx 0;
-  z-index: 1001;
-}
-.fab-avatar {
-  width: 72rpx;
-  height: 72rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #F97316, #FB923C);
-  box-shadow: 0 6rpx 24rpx rgba(249, 115, 22, 0.35), 0 0 0 4rpx rgba(249, 115, 22, 0.12);
-  position: relative;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
-}
-.fab-avatar:active {
-  transform: scale(0.9);
-}
-.fab-avatar.active {
-  transform: scale(1.05);
-  box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.45), 0 0 0 6rpx rgba(249, 115, 22, 0.15);
-}
-
-/* 默认态:家庭图标 */
-.fab-icon-default {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.fab-icon-emoji {
-  font-size: 36rpx;
-  line-height: 1;
-  filter: drop-shadow(0 1rpx 2rpx rgba(0,0,0,0.1));
-}
-
-/* 展开态:当前成员头像(渐变出现) */
-.fab-img {
-  position: absolute;
-  top: 0;
-  left: 0;
+.strip-scroll {
   width: 100%;
-  height: 100%;
-  border-radius: 50%;
-  opacity: 0;
-  transform: scale(0.8);
-  transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
-}
-.fab-img-show {
-  opacity: 1;
-  transform: scale(1);
-}
-
-/* 光环 */
-.fab-ring {
-  position: absolute;
-  top: -4rpx;
-  left: -4rpx;
-  right: -4rpx;
-  bottom: -4rpx;
-  border-radius: 50%;
-  border: 3rpx solid rgba(255, 255, 255, 0.6);
-  opacity: 0;
-  transition: opacity 0.3s ease;
-}
-.fab-ring.ring-active {
-  opacity: 1;
-}
-.fab-badge {
-  position: absolute;
-  top: -4rpx;
-  right: -8rpx;
-  width: 32rpx;
-  height: 32rpx;
-  border-radius: 50%;
-  background: #EF4444;
-  border: 2rpx solid #FFFFFF;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.badge-count {
-  font-size: 16rpx;
-  color: #FFFFFF;
-  font-weight: bold;
-}
-.fab-pulse {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  width: 72rpx;
-  height: 72rpx;
-  margin-top: -36rpx;
-  margin-left: -36rpx;
-  border-radius: 50%;
-  background: rgba(249, 115, 22, 0.25);
-  animation: fab-pulse-anim 2.5s ease-out infinite;
-  /* 呼吸动画层不拦截点击,否则常驻覆盖 FAB 导致无法展开成员扇形 */
-  pointer-events: none;
-}
-@keyframes fab-pulse-anim {
-  0% { transform: scale(1); opacity: 0.6; }
-  100% { transform: scale(2); opacity: 0; }
-}
-
-/* ===== 扇形展开 ===== */
-/* fan-wrap 用 fixed 覆盖整个视口,避免 absolute 套小容器导致位置错乱 */
-.fan-wrap {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  z-index: 999;
-  pointer-events: none;
-}
-.fan-wrap.show {
-  pointer-events: auto;
-}
-.fan-overlay {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.25);
-  opacity: 0;
-  transition: opacity 0.3s ease;
-  z-index: 999;
-}
-.fan-wrap.show .fan-overlay {
-  opacity: 1;
-}
-/* fan-container 锚定到 FAB 坐标(FAB 在 left:16rpx, bottom:33.33%),零尺寸做原点 */
-.fan-container {
-  position: absolute;
-  bottom: 33.33%;
-  left: 16rpx;
-  top: auto;
-  right: auto;
-  width: 0;
-  height: 0;
-  padding: 0;
-  overflow: visible;
-}
-
-.fan-card {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 110rpx;
-  pointer-events: auto;
-  transform: translate(0, 0) rotate(0deg);
-  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease;
-  opacity: 0;
-  z-index: 1000;
-}
-.fan-wrap.show .fan-card {
-  opacity: 1;
-}
-.fan-card.selected {
-  filter: brightness(0.95);
-}
-.fan-avatar-wrap {
-  position: relative;
-  width: 64rpx;
-  height: 64rpx;
-  margin: 0 auto;
-}
-.fan-avatar {
-  width: 64rpx;
-  height: 64rpx;
-  border-radius: 50%;
-  background: #FFFFFF;
-  border: 2rpx solid #FFFFFF;
-  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
-}
-.fan-relation-badge {
-  position: absolute;
-  bottom: -4rpx;
-  left: 50%;
-  transform: translateX(-50%);
-  padding: 1rpx 8rpx;
-  border-radius: 10rpx;
   white-space: nowrap;
-  max-width: 76rpx;
 }
-.fan-relation-text {
-  font-size: 15rpx;
-  color: #FFFFFF;
-  font-weight: 500;
-  line-height: 1.4;
-}
-.fan-spouse-mark {
-  position: absolute;
-  top: -2rpx;
-  right: -2rpx;
-  width: 22rpx;
-  height: 22rpx;
+.strip-inner {
   display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.spouse-icon {
-  font-size: 16rpx;
+  flex-direction: row;
 }
-.fan-nickname {
-  display: block;
-  margin-top: 4rpx;
-  text-align: center;
-  font-size: 20rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-  text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
-  line-height: 1.2;
-  max-width: 90rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.fan-score {
-  display: flex;
-  align-items: baseline;
-  justify-content: center;
-  margin-top: 2rpx;
+.strip-inner.scroll-mode {
+  gap: 16rpx;
 }
-.score-value {
-  font-size: 18rpx;
-  color: #FBBF24;
-  font-weight: bold;
-  text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
-}
-.score-unit {
-  font-size: 15rpx;
-  color: rgba(255, 255, 255, 0.8);
-  margin-left: 2rpx;
-  text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
-}
-.fan-selected-mark {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 22rpx;
-  height: 22rpx;
-  border-radius: 50%;
-  background: #F97316;
-  display: flex;
-  align-items: center;
+.strip-inner.center-mode {
   justify-content: center;
+  gap: 24rpx;
+  flex-wrap: wrap;
 }
-.mark-icon {
-  font-size: 14rpx;
-  color: #FFFFFF;
-  font-weight: bold;
+.strip-scroll .strip-inner.scroll-mode {
+  display: inline-flex;
 }
-
-/* ===== 操作菜单 ===== */
+/* 操作菜单 */
 .action-overlay {
   position: fixed;
   top: 0; left: 0; right: 0; bottom: 0;
   background: rgba(0,0,0,0.4);
-  z-index: 1001;
+  z-index: 999;
 }
 .action-sheet {
   position: fixed;
@@ -647,63 +399,42 @@ export default {
   right: 0;
   background: #FFFFFF;
   border-radius: 24rpx 24rpx 0 0;
-  z-index: 1002;
+  z-index: 1000;
   padding: 20rpx 0;
   padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
-  animation: sheet-up 0.3s ease;
-}
-@keyframes sheet-up {
-  from { transform: translateY(100%); }
-  to { transform: translateY(0); }
 }
 .action-header {
   display: flex;
+  flex-direction: column;
   align-items: center;
   padding: 16rpx 30rpx 20rpx;
   border-bottom: 1rpx solid #F3F4F6;
-  gap: 16rpx;
-}
-.action-avatar-wrap {
-  width: 56rpx;
-  height: 56rpx;
-  border-radius: 50%;
-  overflow: hidden;
-  flex-shrink: 0;
-}
-.action-avatar {
-  width: 56rpx;
-  height: 56rpx;
-  border-radius: 50%;
-}
-.action-info {
-  display: flex;
-  flex-direction: column;
-  gap: 2rpx;
 }
 .action-title {
-  font-size: 28rpx;
+  font-size: 30rpx;
   font-weight: 600;
   color: #1F2937;
 }
 .action-relation {
-  font-size: 22rpx;
+  font-size: 24rpx;
   color: #9CA3AF;
+  margin-top: 4rpx;
 }
 .action-item {
   display: flex;
   align-items: center;
-  padding: 22rpx 30rpx;
+  padding: 24rpx 30rpx;
   border-bottom: 1rpx solid #F9FAFB;
 }
 .action-item:active {
   background: #F3F4F6;
 }
 .action-item-icon {
-  font-size: 30rpx;
+  font-size: 32rpx;
   margin-right: 16rpx;
 }
 .action-item-text {
-  font-size: 26rpx;
+  font-size: 28rpx;
   color: #374151;
 }
 .action-danger .action-item-text {
@@ -711,212 +442,100 @@ export default {
 }
 .action-cancel {
   justify-content: center;
-  margin-top: 8rpx;
+  margin-top: 10rpx;
   border-bottom: none;
 }
 .action-cancel .action-item-text {
   color: #6B7280;
 }
-
-/* ===== 待审批请求 ===== */
-.request-overlay {
-  position: fixed;
-  top: 0; left: 0; right: 0; bottom: 0;
-  background: rgba(0,0,0,0.4);
-  z-index: 1003;
+.action-time {
+  font-size: 22rpx;
+  color: #9CA3AF;
+  margin-left: 4rpx;
 }
-.request-sheet {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
+/* 空状态 */
+.family-strip-empty {
+  margin: 16rpx 0;
+  padding: 40rpx 30rpx;
   background: #FFFFFF;
-  border-radius: 24rpx 24rpx 0 0;
-  z-index: 1004;
-  padding: 24rpx 0;
-  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
-  animation: sheet-up 0.3s ease;
-  max-height: 60vh;
-  overflow-y: auto;
-}
-.sheet-header {
+  border-radius: 24rpx;
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 0 30rpx 16rpx;
-  border-bottom: 1rpx solid #F3F4F6;
-}
-.sheet-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1F2937;
-}
-.sheet-close {
-  font-size: 32rpx;
-  color: #999;
-}
-.empty-requests {
-  padding: 60rpx 0;
-  text-align: center;
-  font-size: 24rpx;
-  color: #999;
-}
-.request-item {
-  display: flex;
-  align-items: center;
-  padding: 20rpx 30rpx;
-  gap: 16rpx;
-}
-.req-avatar {
-  width: 56rpx;
-  height: 56rpx;
-  border-radius: 50%;
-  flex-shrink: 0;
+  justify-content: center;
 }
-.req-info {
-  flex: 1;
+.empty-content {
   display: flex;
   flex-direction: column;
-  gap: 2rpx;
-}
-.req-name {
-  font-size: 26rpx;
-  color: #333;
-}
-.req-time {
-  font-size: 20rpx;
-  color: #999;
-}
-.req-actions {
-  display: flex;
-  gap: 12rpx;
-}
-.req-btn {
-  padding: 8rpx 20rpx;
-  border-radius: 20rpx;
-  font-size: 22rpx;
-  font-weight: 500;
+  align-items: center;
 }
-.btn-approve {
-  background: #10B981;
+.empty-icon { font-size: 56rpx; }
+.empty-title { font-size: 30rpx; font-weight: 600; color: #1F2937; margin-top: 12rpx; }
+.empty-desc { font-size: 24rpx; color: #9CA3AF; margin-top: 8rpx; }
+.empty-btn {
+  margin-top: 24rpx;
+  padding: 16rpx 48rpx;
+  background: #F97316;
   color: #FFFFFF;
+  font-size: 28rpx;
+  border-radius: 40rpx;
 }
-.btn-reject {
-  background: #F3F4F6;
-  color: #666;
-}
-</style>
-
-.badge-count {
-  font-size: 26rpx;
-  font-weight: 700;
-  color: #EF4444;
-}
-
-/* 待审批请求弹窗 */
-.request-overlay {
-  position: fixed;
-  top: 0; left: 0; right: 0; bottom: 0;
-  background: rgba(0,0,0,0.4);
-  z-index: 1500;
-}
-
-.request-sheet {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: #FFFFFF;
-  border-radius: 24rpx 24rpx 0 0;
-  z-index: 1501;
-  max-height: 70vh;
-  overflow-y: auto;
-  padding: 20rpx 0;
-  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
-}
-
-.sheet-header {
+/* Action Sheet 头部(覆盖原有 column 布局) */
+.action-header {
   display: flex;
-  justify-content: space-between;
+  flex-direction: row;
   align-items: center;
-  padding: 16rpx 30rpx 16rpx;
+  padding: 24rpx 30rpx;
   border-bottom: 1rpx solid #F3F4F6;
 }
-
-.sheet-title {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #1F2937;
+.action-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  margin-right: 20rpx;
+  background: #F3F4F6;
 }
-
-.sheet-close {
-  font-size: 40rpx;
-  color: #9CA3AF;
-  padding: 8rpx;
-  line-height: 1;
+.action-header-info {
+  display: flex;
+  flex-direction: column;
 }
-
-.empty-requests {
-  text-align: center;
-  padding: 60rpx 0;
-  font-size: 28rpx;
-  color: #9CA3AF;
+/* 关系现状 */
+.action-scores {
+  padding: 20rpx 30rpx;
 }
-
-.request-item {
+.score-row {
   display: flex;
+  flex-direction: row;
   align-items: center;
-  padding: 20rpx 30rpx;
-  border-bottom: 1rpx solid #F9FAFB;
+  margin-bottom: 12rpx;
 }
-
-.req-avatar {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 50%;
-  margin-right: 16rpx;
-  flex-shrink: 0;
+.score-label {
+  width: 110rpx;
+  font-size: 24rpx;
+  color: #6B7280;
 }
-
-.req-info {
+.score-bar-track {
   flex: 1;
-  min-width: 0;
-}
-
-.req-name {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #1F2937;
-  display: block;
-}
-
-.req-time {
-  font-size: 24rpx;
-  color: #9CA3AF;
-  display: block;
-  margin-top: 4rpx;
+  height: 14rpx;
+  background: #F3F4F6;
+  border-radius: 7rpx;
+  overflow: hidden;
 }
-
-.req-actions {
-  display: flex;
-  gap: 12rpx;
+.score-bar-fill {
+  height: 100%;
+  border-radius: 7rpx;
 }
-
-.req-btn {
-  padding: 12rpx 24rpx;
-  border-radius: 8rpx;
+.score-bar-fill.trust { background: #10B981; }
+.score-bar-fill.intimacy { background: #3B82F6; }
+.score-bar-fill.communication { background: #F59E0B; }
+.score-value {
+  width: 70rpx;
+  text-align: right;
   font-size: 24rpx;
+  color: #1F2937;
   font-weight: 600;
-  border: none;
-}
-
-.btn-approve {
-  background: #16A34A;
-  color: #FFFFFF;
 }
-
-.btn-reject {
-  background: #EF4444;
-  color: #FFFFFF;
+.action-divider {
+  height: 1rpx;
+  background: #F3F4F6;
+  margin: 0 30rpx;
 }
 </style>