瀏覽代碼

refactor(frontend): 家庭关系卡重设计为FAB浮标+扇形展开,移除创建/加入家庭按钮(已自动创建)

liaoxg 1 月之前
父節點
當前提交
23221c7664

+ 2 - 32
cfc-frontend/components/FamilyEmptyState/FamilyEmptyState.vue

@@ -3,10 +3,6 @@
     <view class="empty-icon">{{ iconChar }}</view>
     <view class="empty-title">{{ title }}</view>
     <view v-if="desc" class="empty-desc">{{ desc }}</view>
-    <view class="empty-actions">
-      <button class="empty-btn primary" @click="goCreate">{{ primaryText }}</button>
-      <button class="empty-btn secondary" @click="goJoin">{{ secondaryText }}</button>
-    </view>
   </view>
 </template>
 
@@ -14,25 +10,14 @@
 export default {
   name: 'FamilyEmptyState',
   props: {
-    // 形态: 'card'(嵌入卡片,紧凑)/ 'block'(中等块)/ 'list'(列表空态)
     type: { type: String, default: 'card' },
-    title: { type: String, default: '请先创建或加入家庭' },
-    desc: { type: String, default: '加入家庭后即可解锁本功能' },
-    primaryText: { type: String, default: '创建家庭' },
-    secondaryText: { type: String, default: '加入家庭' }
+    title: { type: String, default: '暂无数据' },
+    desc: { type: String, default: '' }
   },
   computed: {
     iconChar: function() {
       return this.type === 'card' ? '🏠' : '👨‍👩‍👧'
     }
-  },
-  methods: {
-    goCreate: function() {
-      uni.navigateTo({ url: '/pages/profile-extra/family-members?action=create' })
-    },
-    goJoin: function() {
-      uni.navigateTo({ url: '/pages/profile-extra/family-members' })
-    }
   }
 }
 </script>
@@ -50,19 +35,4 @@ export default {
 .empty-icon { font-size: 48rpx; margin-bottom: 12rpx; }
 .empty-title { font-size: 28rpx; color: #333; margin-bottom: 8rpx; }
 .empty-desc { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
-.empty-actions { display: flex; flex-direction: row; gap: 16rpx; margin-top: 8rpx; }
-.empty-btn {
-  font-size: 26rpx;
-  padding: 10rpx 28rpx;
-  border-radius: 32rpx;
-}
-.empty-btn.primary {
-  background: #F97316;
-  color: #fff;
-}
-.empty-btn.secondary {
-  background: #FFF7ED;
-  color: #F97316;
-  border: 2rpx solid #F97316;
-}
 </style>

+ 32 - 32
cfc-frontend/components/FamilyMemberCard.vue

@@ -86,9 +86,9 @@ export default {
   display: flex;
   flex-direction: column;
   align-items: center;
-  width: 140rpx;
-  padding: 16rpx 12rpx;
-  border-radius: 20rpx;
+  width: 110rpx;
+  padding: 10rpx 8rpx;
+  border-radius: 16rpx;
   background: #FFFFFF;
   border: 2rpx solid transparent;
   position: relative;
@@ -100,13 +100,13 @@ export default {
 }
 .card-avatar-wrap {
   position: relative;
-  width: 88rpx;
-  height: 88rpx;
-  margin-bottom: 10rpx;
+  width: 64rpx;
+  height: 64rpx;
+  margin-bottom: 6rpx;
 }
 .card-avatar {
-  width: 88rpx;
-  height: 88rpx;
+  width: 64rpx;
+  height: 64rpx;
   border-radius: 50%;
   background: #F3F4F6;
 }
@@ -115,37 +115,37 @@ export default {
   bottom: -4rpx;
   left: 50%;
   transform: translateX(-50%);
-  padding: 2rpx 12rpx;
-  border-radius: 12rpx;
+  padding: 1rpx 8rpx;
+  border-radius: 10rpx;
   white-space: nowrap;
-  max-width: 110rpx;
+  max-width: 84rpx;
 }
 .card-relation-text {
-  font-size: 18rpx;
+  font-size: 16rpx;
   color: #FFFFFF;
   font-weight: 500;
   line-height: 1.4;
 }
 .card-spouse-mark {
   position: absolute;
-  top: -6rpx;
-  right: -6rpx;
-  width: 32rpx;
-  height: 32rpx;
+  top: -4rpx;
+  right: -4rpx;
+  width: 24rpx;
+  height: 24rpx;
   display: flex;
   align-items: center;
   justify-content: center;
 }
 .spouse-icon {
-  font-size: 24rpx;
+  font-size: 18rpx;
 }
 .card-nickname {
-  font-size: 24rpx;
+  font-size: 22rpx;
   color: #374151;
   font-weight: 600;
   text-align: center;
   line-height: 1.3;
-  max-width: 130rpx;
+  max-width: 100rpx;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -154,45 +154,45 @@ export default {
   margin-top: 2rpx;
 }
 .card-generation-text {
-  font-size: 16rpx;
+  font-size: 14rpx;
   color: #9CA3AF;
 }
 .card-contact-row {
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-top: 4rpx;
+  margin-top: 2rpx;
 }
 .contact-icon {
-  font-size: 22rpx;
+  font-size: 18rpx;
 }
 .contact-phone {
-  font-size: 18rpx;
+  font-size: 16rpx;
   color: #3B82F6;
-  margin-left: 4rpx;
+  margin-left: 2rpx;
 }
 .card-energy {
   display: flex;
   align-items: baseline;
   justify-content: center;
-  margin-top: 4rpx;
+  margin-top: 2rpx;
 }
 .energy-value {
-  font-size: 22rpx;
+  font-size: 20rpx;
   color: #F97316;
   font-weight: bold;
 }
 .energy-unit {
-  font-size: 18rpx;
+  font-size: 16rpx;
   color: #9CA3AF;
   margin-left: 2rpx;
 }
 .card-selected-mark {
   position: absolute;
-  top: 6rpx;
-  right: 6rpx;
-  width: 32rpx;
-  height: 32rpx;
+  top: 4rpx;
+  right: 4rpx;
+  width: 24rpx;
+  height: 24rpx;
   border-radius: 50%;
   background: #F97316;
   display: flex;
@@ -200,7 +200,7 @@ export default {
   justify-content: center;
 }
 .mark-icon {
-  font-size: 20rpx;
+  font-size: 16rpx;
   color: #FFFFFF;
   font-weight: bold;
 }

+ 447 - 104
cfc-frontend/components/FamilyMemberStrip.vue

@@ -1,50 +1,61 @@
 <template>
-  <view v-if="hasFamily" class="family-member-strip">
-    <!-- 待审批加入请求徽章 -->
-    <view class="strip-toolbar" v-if="isAdmin">
-      <view class="pending-badge" v-if="pendingCount > 0" @tap="openRequests">
-        <text class="badge-icon">🔔</text>
-        <text class="badge-count">{{ pendingCount }}</text>
+  <view class="member-strip">
+    <!-- 浮标头像 -->
+    <view class="fab-wrap">
+      <view class="fab-avatar" :class="{ active: fanOpen }" @tap="toggleFan">
+        <image class="fab-img" :src="currentMember.avatar || defaultAvatar" mode="aspectFill" />
+        <view class="fab-badge" v-if="pendingCount > 0 && isAdmin" @tap.stop="openRequests">
+          <text class="badge-count">{{ pendingCount }}</text>
+        </view>
       </view>
+      <view class="fab-pulse" v-if="fanOpen"></view>
     </view>
-    <scroll-view
-      class="strip-scroll"
-      scroll-x
-      :scroll-with-animation="true"
-      :enhanced="true"
-      show-scrollbar="false"
-      v-if="members.length >= 4"
-    >
-      <view class="strip-inner scroll-mode">
-        <FamilyMemberCard
-          v-for="member in members"
+
+    <!-- 扇形展开成员 -->
+    <view class="fan-wrap" :class="{ show: fanOpen }">
+      <view class="fan-overlay" @tap="closeFan"></view>
+      <view class="fan-container">
+        <view
+          class="fan-card"
+          v-for="(member, index) in members"
           :key="member.id"
-          :member="member"
-          :isSelected="member.id === selectedMemberId"
-          :energyScore="getMemberEnergy(member)"
-          @select="onSelect"
-          @longpress="onLongPress"
-        />
+          :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>
+          </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>
+          <view class="fan-selected-mark" v-if="member.id === selectedMemberId">
+            <text class="mark-icon">✓</text>
+          </view>
+        </view>
       </view>
-    </scroll-view>
-    <view class="strip-inner center-mode" v-else>
-      <FamilyMemberCard
-        v-for="member in members"
-        :key="member.id"
-        :member="member"
-        :isSelected="member.id === selectedMemberId"
-        :energyScore="getMemberEnergy(member)"
-        @select="onSelect"
-        @longpress="onLongPress"
-      />
     </view>
 
-    <!-- 长按操作菜单 -->
+    <!-- 长按/点击浮标弹出的个人菜单 -->
     <view class="action-overlay" v-if="showActionMenu" @tap="closeMenu"></view>
     <view class="action-sheet" v-if="showActionMenu">
       <view class="action-header">
-        <text class="action-title">{{ actionMember ? actionMember.nickname : '' }}</text>
-        <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || '') : '' }}</text>
+        <view class="action-avatar-wrap">
+          <image class="action-avatar" :src="actionMember ? (actionMember.avatar || defaultAvatar) : (currentMember.avatar || defaultAvatar)" mode="aspectFill" />
+        </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>
       </view>
       <view class="action-item" @tap="switchToMember" v-if="canSwitch">
         <text class="action-item-icon">🔄</text>
@@ -91,24 +102,19 @@
           <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>
+      <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>
     </view>
   </view>
-  <view v-else>
-    <FamilyEmptyState type="card" />
-  </view>
 </template>
 
 <script>
-import FamilyMemberCard from '@/components/FamilyMemberCard'
 import { kickFamilyMemberById, getPendingFamilyRequests, approveFamilyJoinRequest, rejectFamilyJoinRequest } from '@/utils/api'
 
 export default {
-  components: { FamilyMemberCard },
   props: {
     members: { type: Array, default: function() { return [] } },
     selectedMemberId: { type: Number, default: null },
@@ -118,6 +124,7 @@ export default {
   },
   data: function() {
     return {
+      fanOpen: false,
       showActionMenu: false,
       actionMember: null,
       pendingCount: 0,
@@ -128,26 +135,86 @@ export default {
   mounted: function() {
     this.loadPendingRequests()
   },
-  computed: {
-    hasFamily: function() {
-      return this.$store.getters.hasFamily
+   computed: {
+    defaultAvatar: function() {
+      return '/static/default-avatar.png'
+    },
+    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]
+      }
+      return this.members[0]
     },
     canSwitch: function() {
-      // 仅家庭成员记录(source=family_member)且已关联账号时才能切换视角;
-      // 家长卡片(source=parent)是 users 表记录,switch 接口只查 family_members 表,切换会失败
-      return this.actionMember && this.actionMember.userId && this.actionMember.source === 'family_member'
+      var target = this.actionMember || this.currentMember
+      return target && target.userId && target.source === 'family_member'
     },
     canEdit: function() {
-      return this.isParent && this.actionMember && !this.actionMember.userId && this.actionMember.source === 'family_member'
+      var target = this.actionMember || this.currentMember
+      return this.isParent && target && !target.userId && target.source === 'family_member'
     },
     canRemove: function() {
-      // 仅家庭成员记录可移除(kick 接口只查 family_members 表),且不能移除自己
-      return this.isParent && this.actionMember && !this.actionMember.isSelf && this.actionMember.source === 'family_member'
+      var target = this.actionMember || this.currentMember
+      return this.isParent && target && !target.isSelf && target.source === 'family_member'
     }
   },
   methods: {
-    onSelect: function(member) {
+    // ===== 扇形展开 =====
+    toggleFan: function(e) {
+      if (this.fanOpen) {
+        this.closeFan()
+      } else {
+        this.fanOpen = true
+      }
+    },
+    closeFan: function() {
+      this.fanOpen = false
+    },
+    // 计算扇形位置: 头像在左边,成员向右扇形排列
+    // 用固定的偏移量数组,避免用 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 = 280
+      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);'
+    },
+    getMemberEnergy: function(member) {
+      if (!member || !this.energyMap) return null
+      return this.energyMap[member.id] || this.energyMap[member.memberId] || null
+    },
+    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'
+    },
+
+    // ===== 成员选择 =====
+    selectMember: function(member) {
       this.$emit('select', member)
+      // 关闭扇形
+      this.fanOpen = false
+    },
+
+    // ===== 个人菜单 =====
+    onFanLongPress: function(member) {
+      this.actionMember = member
+      this.showActionMenu = true
     },
     onLongPress: function(member) {
       this.actionMember = member
@@ -158,14 +225,14 @@ export default {
       this.actionMember = null
     },
     switchToMember: function() {
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member && member.userId) {
         this.$emit('switch', member)
       }
     },
     editMember: function() {
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member) {
         var params = 'memberId=' + member.id + '&nickname=' + encodeURIComponent(member.nickname || '')
@@ -176,21 +243,21 @@ export default {
       }
     },
     questionnaireMember: function() {
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member) {
         uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire?memberId=' + member.id })
       }
     },
     interactionMember: function() {
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member) {
         uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + member.id })
       }
     },
     callMember: function() {
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member && member.phone) {
         uni.makePhoneCall({ phoneNumber: member.phone })
@@ -198,7 +265,7 @@ export default {
     },
     removeMember: function() {
       var self = this
-      var member = this.actionMember
+      var member = this.actionMember || this.currentMember
       this.closeMenu()
       if (member) {
         uni.showModal({
@@ -211,7 +278,6 @@ export default {
                 .then(function() {
                   uni.hideLoading()
                   uni.showToast({ title: '已移除', icon: 'success' })
-                  // 通知父组件刷新成员列表(替代无效的 globalData 标志)
                   self.$emit('removed', member)
                 })
                 .catch(function(err) {
@@ -223,10 +289,8 @@ export default {
         })
       }
     },
-    getMemberEnergy: function(member) {
-      if (!member || !this.energyMap) return null
-      return this.energyMap[member.id] || this.energyMap[member.memberId] || null
-    },
+
+    // ===== 待审批请求 =====
     loadPendingRequests: function() {
       var self = this
       if (!this.isAdmin) return
@@ -293,35 +357,226 @@ export default {
 </script>
 
 <style scoped>
-.family-member-strip {
-  margin: 16rpx 0;
-  padding: 0 30rpx;
+.member-strip {
+  position: relative;
+  margin: 8rpx 0;
+  padding: 0 20rpx;
+  z-index: 100;
+}
+
+/* ===== 浮标 ===== */
+.fab-wrap {
+  position: relative;
+  display: inline-block;
+  margin: 4rpx 0;
+  z-index: 1001;
+}
+.fab-avatar {
+  width: 68rpx;
+  height: 68rpx;
+  border-radius: 50%;
+  background: #FFFFFF;
+  box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.25);
+  border: 3rpx solid #F97316;
+  position: relative;
+  transition: transform 0.3s ease;
+}
+.fab-avatar:active {
+  transform: scale(0.92);
+}
+.fab-avatar.active {
+  transform: rotate(180deg);
 }
-.strip-scroll {
+.fab-img {
   width: 100%;
+  height: 100%;
+  border-radius: 50%;
+}
+.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: 68rpx;
+  height: 68rpx;
+  margin-top: -34rpx;
+  margin-left: -34rpx;
+  border-radius: 50%;
+  border: 2rpx solid #F97316;
+  animation: fab-pulse-anim 2s ease-out infinite;
+}
+@keyframes fab-pulse-anim {
+  0% { transform: scale(1); opacity: 0.8; }
+  100% { transform: scale(2); opacity: 0; }
+}
+
+/* ===== 扇形展开 ===== */
+.fan-wrap {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100vh;
+  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 {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 0;
+}
+
+.fan-card {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100rpx;
+  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;
+}
+.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;
 }
-.strip-inner {
+.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;
   display: flex;
-  flex-direction: row;
+  align-items: center;
+  justify-content: center;
 }
-.strip-inner.scroll-mode {
-  gap: 16rpx;
+.spouse-icon {
+  font-size: 16rpx;
 }
-.strip-inner.center-mode {
+.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;
+}
+.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;
   justify-content: center;
-  gap: 24rpx;
-  flex-wrap: wrap;
 }
-.strip-scroll .strip-inner.scroll-mode {
-  display: inline-flex;
+.mark-icon {
+  font-size: 14rpx;
+  color: #FFFFFF;
+  font-weight: bold;
 }
-/* 操作菜单 */
+
+/* ===== 操作菜单 ===== */
 .action-overlay {
   position: fixed;
   top: 0; left: 0; right: 0; bottom: 0;
   background: rgba(0,0,0,0.4);
-  z-index: 999;
+  z-index: 1001;
 }
 .action-sheet {
   position: fixed;
@@ -330,42 +585,63 @@ export default {
   right: 0;
   background: #FFFFFF;
   border-radius: 24rpx 24rpx 0 0;
-  z-index: 1000;
+  z-index: 1002;
   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: 30rpx;
+  font-size: 28rpx;
   font-weight: 600;
   color: #1F2937;
 }
 .action-relation {
-  font-size: 24rpx;
+  font-size: 22rpx;
   color: #9CA3AF;
-  margin-top: 4rpx;
 }
 .action-item {
   display: flex;
   align-items: center;
-  padding: 24rpx 30rpx;
+  padding: 22rpx 30rpx;
   border-bottom: 1rpx solid #F9FAFB;
 }
 .action-item:active {
   background: #F3F4F6;
 }
 .action-item-icon {
-  font-size: 32rpx;
+  font-size: 30rpx;
   margin-right: 16rpx;
 }
 .action-item-text {
-  font-size: 28rpx;
+  font-size: 26rpx;
   color: #374151;
 }
 .action-danger .action-item-text {
@@ -373,34 +649,101 @@ export default {
 }
 .action-cancel {
   justify-content: center;
-  margin-top: 10rpx;
+  margin-top: 8rpx;
   border-bottom: none;
 }
 .action-cancel .action-item-text {
   color: #6B7280;
 }
 
-/* 待审批徽章 */
-.strip-toolbar {
-  display: flex;
-  justify-content: flex-end;
-  padding: 8rpx 30rpx 4rpx;
+/* ===== 待审批请求 ===== */
+.request-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.4);
+  z-index: 1003;
 }
-
-.pending-badge {
+.request-sheet {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  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 {
   display: flex;
+  justify-content: space-between;
   align-items: center;
-  padding: 8rpx 16rpx;
-  background: #FFFFFF;
-  border: 2rpx solid #EF4444;
-  border-radius: 32rpx;
-  box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.15);
+  padding: 0 30rpx 16rpx;
+  border-bottom: 1rpx solid #F3F4F6;
 }
-
-.badge-icon {
+.sheet-title {
   font-size: 28rpx;
-  margin-right: 6rpx;
+  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;
+}
+.req-info {
+  flex: 1;
+  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;
+}
+.btn-approve {
+  background: #10B981;
+  color: #FFFFFF;
+}
+.btn-reject {
+  background: #F3F4F6;
+  color: #666;
 }
+</style>
 
 .badge-count {
   font-size: 26rpx;