FamilyMemberStrip.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. <template>
  2. <view class="member-strip">
  3. <!-- 浮标头像 -->
  4. <view class="fab-wrap">
  5. <view class="fab-avatar" :class="{ active: fanOpen }" @tap="toggleFan">
  6. <image class="fab-img" :src="currentMember.avatar || defaultAvatar" mode="aspectFill" />
  7. <view class="fab-badge" v-if="pendingCount > 0 && isAdmin" @tap.stop="openRequests">
  8. <text class="badge-count">{{ pendingCount }}</text>
  9. </view>
  10. </view>
  11. <view class="fab-pulse" v-if="fanOpen"></view>
  12. </view>
  13. <!-- 扇形展开成员 -->
  14. <view class="fan-wrap" :class="{ show: fanOpen }">
  15. <view class="fan-overlay" @tap="closeFan"></view>
  16. <view class="fan-container">
  17. <view
  18. class="fan-card"
  19. v-for="(member, index) in members"
  20. :key="member.id"
  21. :style="getFanPosition(member, index)"
  22. :class="{ selected: member.id === selectedMemberId }"
  23. @tap="selectMember(member)"
  24. @longpress="onFanLongPress(member)"
  25. >
  26. <view class="fan-avatar-wrap">
  27. <image class="fan-avatar" :src="member.avatar || defaultAvatar" mode="aspectFill" />
  28. <view class="fan-relation-badge" :style="{ background: getRelationColor(member) }">
  29. <text class="fan-relation-text">{{ member.relationship || member.relativeLabel || member.roleLabel || '成员' }}</text>
  30. </view>
  31. <view class="fan-spouse-mark" v-if="member.isSpouse">
  32. <text class="spouse-icon">💍</text>
  33. </view>
  34. </view>
  35. <text class="fan-nickname">{{ member.nickname }}</text>
  36. <view class="fan-score" v-if="getMemberEnergy(member) !== null">
  37. <text class="score-value">{{ getMemberEnergy(member) }}</text>
  38. <text class="score-unit">分</text>
  39. </view>
  40. <view class="fan-selected-mark" v-if="member.id === selectedMemberId">
  41. <text class="mark-icon">✓</text>
  42. </view>
  43. </view>
  44. </view>
  45. </view>
  46. <!-- 长按/点击浮标弹出的个人菜单 -->
  47. <view class="action-overlay" v-if="showActionMenu" @tap="closeMenu"></view>
  48. <view class="action-sheet" v-if="showActionMenu">
  49. <view class="action-header">
  50. <view class="action-avatar-wrap">
  51. <image class="action-avatar" :src="actionMember ? (actionMember.avatar || defaultAvatar) : (currentMember.avatar || defaultAvatar)" mode="aspectFill" />
  52. </view>
  53. <view class="action-info">
  54. <text class="action-title">{{ actionMember ? actionMember.nickname : (currentMember.nickname || '我') }}</text>
  55. <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || '') : '' }}</text>
  56. </view>
  57. </view>
  58. <view class="action-item" @tap="switchToMember" v-if="canSwitch">
  59. <text class="action-item-icon">🔄</text>
  60. <text class="action-item-text">切换到该成员视角</text>
  61. </view>
  62. <view class="action-item" @tap="editMember" v-if="canEdit">
  63. <text class="action-item-icon">✏️</text>
  64. <text class="action-item-text">编辑资料</text>
  65. </view>
  66. <view class="action-item" @tap="questionnaireMember">
  67. <text class="action-item-icon">📋</text>
  68. <text class="action-item-text">关系问卷</text>
  69. </view>
  70. <view class="action-item" @tap="interactionMember">
  71. <text class="action-item-icon">💬</text>
  72. <text class="action-item-text">记录互动</text>
  73. </view>
  74. <view class="action-item" @tap="callMember" v-if="actionMember && actionMember.phone">
  75. <text class="action-item-icon">📞</text>
  76. <text class="action-item-text">拨打电话</text>
  77. </view>
  78. <view class="action-item action-danger" @tap="removeMember" v-if="canRemove">
  79. <text class="action-item-icon">🗑️</text>
  80. <text class="action-item-text">移除成员</text>
  81. </view>
  82. <view class="action-item action-cancel" @tap="closeMenu">
  83. <text class="action-item-text">取消</text>
  84. </view>
  85. </view>
  86. <!-- 待审批请求弹窗 -->
  87. <view class="request-overlay" v-if="showRequests" @tap="closeRequests"></view>
  88. <view class="request-sheet" v-if="showRequests">
  89. <view class="sheet-header">
  90. <text class="sheet-title">待处理加入申请({{ pendingRequests.length }})</text>
  91. <text class="sheet-close" @tap="closeRequests">×</text>
  92. </view>
  93. <view v-if="pendingRequests.length === 0" class="empty-requests">
  94. <text>暂无待处理的请求</text>
  95. </view>
  96. <view class="request-item" v-for="req in pendingRequests" :key="req.id">
  97. <image class="req-avatar" src="/static/default-avatar.png" mode="aspectFill"></image>
  98. <view class="req-info">
  99. <text class="req-name">{{ req.requesterNickname }}</text>
  100. <text class="req-time">{{ req.createdAt }}</text>
  101. </view>
  102. <view class="req-actions">
  103. <button class="req-btn btn-approve" @tap="approveRequest(req.id)">通过</button>
  104. <button class="req-btn btn-reject" @tap="rejectRequest(req.id)">拒绝</button>
  105. </view>
  106. </view>
  107. </view>
  108. </view>
  109. </template>
  110. <script>
  111. import { kickFamilyMemberById, getPendingFamilyRequests, approveFamilyJoinRequest, rejectFamilyJoinRequest } from '@/utils/api'
  112. export default {
  113. props: {
  114. members: { type: Array, default: function() { return [] } },
  115. selectedMemberId: { type: Number, default: null },
  116. energyMap: { type: Object, default: function() { return {} } },
  117. isParent: { type: Boolean, default: false },
  118. isAdmin: { type: Boolean, default: false }
  119. },
  120. data: function() {
  121. return {
  122. fanOpen: false,
  123. showActionMenu: false,
  124. actionMember: null,
  125. pendingCount: 0,
  126. pendingRequests: [],
  127. showRequests: false
  128. }
  129. },
  130. mounted: function() {
  131. this.loadPendingRequests()
  132. },
  133. computed: {
  134. defaultAvatar: function() {
  135. return '/static/default-avatar.png'
  136. },
  137. currentMember: function() {
  138. if (!this.members || this.members.length === 0) return {}
  139. for (var i = 0; i < this.members.length; i++) {
  140. if (this.members[i].id === this.selectedMemberId) return this.members[i]
  141. }
  142. return this.members[0]
  143. },
  144. canSwitch: function() {
  145. var target = this.actionMember || this.currentMember
  146. return target && target.userId && target.source === 'family_member'
  147. },
  148. canEdit: function() {
  149. var target = this.actionMember || this.currentMember
  150. return this.isParent && target && !target.userId && target.source === 'family_member'
  151. },
  152. canRemove: function() {
  153. var target = this.actionMember || this.currentMember
  154. return this.isParent && target && !target.isSelf && target.source === 'family_member'
  155. }
  156. },
  157. methods: {
  158. // ===== 扇形展开 =====
  159. toggleFan: function(e) {
  160. if (this.fanOpen) {
  161. this.closeFan()
  162. } else {
  163. this.fanOpen = true
  164. }
  165. },
  166. closeFan: function() {
  167. this.fanOpen = false
  168. },
  169. // 计算扇形位置: 头像在左边,成员向右扇形排列
  170. // 用固定的偏移量数组,避免用 transform-origin 的兼容问题
  171. getFanPosition: function(member, index) {
  172. if (!this.fanOpen) return 'transform: translate(0rpx, 0rpx) rotate(0deg);'
  173. var total = this.members.length
  174. var angleRange = 50
  175. var steps = Math.max(total - 1, 1)
  176. var angle = -angleRange + (index / steps) * angleRange * 2
  177. var rad = angle * Math.PI / 180
  178. var radius = 280
  179. var dx = Math.cos(rad) * radius
  180. var dy = Math.sin(rad) * radius
  181. var rotation = angle * 0.3
  182. return 'transform: translate(' + dx + 'rpx,' + dy + 'rpx) rotate(' + rotation + 'deg);'
  183. },
  184. getMemberEnergy: function(member) {
  185. if (!member || !this.energyMap) return null
  186. return this.energyMap[member.id] || this.energyMap[member.memberId] || null
  187. },
  188. getRelationColor: function(member) {
  189. var rel = member && member.relationship
  190. if (rel === '爸爸' || rel === '妈妈' || rel === '父亲' || rel === '母亲') return '#F97316'
  191. if (rel === '儿子' || rel === '女儿') return '#0EA5E9'
  192. if (rel === '爷爷' || rel === '奶奶' || rel === '外公' || rel === '外婆') return '#6B7280'
  193. if (rel === '哥哥' || rel === '姐姐' || rel === '弟弟' || rel === '妹妹') return '#10B981'
  194. if (member && member.isSpouse) return '#EC4899'
  195. var role = member && member.effectiveRole
  196. if (role === 'parent') return '#F97316'
  197. if (role === 'child') return '#0EA5E9'
  198. if (role === 'teacher') return '#8B5CF6'
  199. return '#9CA3AF'
  200. },
  201. // ===== 成员选择 =====
  202. selectMember: function(member) {
  203. this.$emit('select', member)
  204. // 关闭扇形
  205. this.fanOpen = false
  206. },
  207. // ===== 个人菜单 =====
  208. onFanLongPress: function(member) {
  209. this.actionMember = member
  210. this.showActionMenu = true
  211. },
  212. onLongPress: function(member) {
  213. this.actionMember = member
  214. this.showActionMenu = true
  215. },
  216. closeMenu: function() {
  217. this.showActionMenu = false
  218. this.actionMember = null
  219. },
  220. switchToMember: function() {
  221. var member = this.actionMember || this.currentMember
  222. this.closeMenu()
  223. if (member && member.userId) {
  224. this.$emit('switch', member)
  225. }
  226. },
  227. editMember: function() {
  228. var member = this.actionMember || this.currentMember
  229. this.closeMenu()
  230. if (member) {
  231. var params = 'memberId=' + member.id + '&nickname=' + encodeURIComponent(member.nickname || '')
  232. if (member.gender) params += '&gender=' + member.gender
  233. if (member.phone) params += '&phone=' + member.phone
  234. if (member.birthday) params += '&birthday=' + member.birthday
  235. uni.navigateTo({ url: '/pages/family/add-member?' + params })
  236. }
  237. },
  238. questionnaireMember: function() {
  239. var member = this.actionMember || this.currentMember
  240. this.closeMenu()
  241. if (member) {
  242. uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire?memberId=' + member.id })
  243. }
  244. },
  245. interactionMember: function() {
  246. var member = this.actionMember || this.currentMember
  247. this.closeMenu()
  248. if (member) {
  249. uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + member.id })
  250. }
  251. },
  252. callMember: function() {
  253. var member = this.actionMember || this.currentMember
  254. this.closeMenu()
  255. if (member && member.phone) {
  256. uni.makePhoneCall({ phoneNumber: member.phone })
  257. }
  258. },
  259. removeMember: function() {
  260. var self = this
  261. var member = this.actionMember || this.currentMember
  262. this.closeMenu()
  263. if (member) {
  264. uni.showModal({
  265. title: '确认移除',
  266. content: '确定要移除 ' + member.nickname + ' 吗?',
  267. success: function(res) {
  268. if (res.confirm) {
  269. uni.showLoading({ title: '移除中...' })
  270. kickFamilyMemberById(member.id)
  271. .then(function() {
  272. uni.hideLoading()
  273. uni.showToast({ title: '已移除', icon: 'success' })
  274. self.$emit('removed', member)
  275. })
  276. .catch(function(err) {
  277. uni.hideLoading()
  278. uni.showToast({ title: (err && err.message) || '移除失败', icon: 'none' })
  279. })
  280. }
  281. }
  282. })
  283. }
  284. },
  285. // ===== 待审批请求 =====
  286. loadPendingRequests: function() {
  287. var self = this
  288. if (!this.isAdmin) return
  289. getPendingFamilyRequests()
  290. .then(function(res) {
  291. if (res.code === 200) {
  292. var data = res.data || []
  293. self.pendingCount = data.length
  294. self.pendingRequests = data
  295. }
  296. })
  297. .catch(function() {
  298. // 静默失败
  299. })
  300. },
  301. openRequests: function() {
  302. this.showRequests = true
  303. },
  304. closeRequests: function() {
  305. this.showRequests = false
  306. },
  307. approveRequest: function(requestId) {
  308. var self = this
  309. uni.showModal({
  310. title: '确认通过',
  311. content: '确认通过该成员的加入申请?',
  312. success: function(modalRes) {
  313. if (modalRes.confirm) {
  314. approveFamilyJoinRequest(requestId)
  315. .then(function() {
  316. uni.showToast({ title: '已通过', icon: 'success' })
  317. self.loadPendingRequests()
  318. self.closeRequests()
  319. })
  320. .catch(function(err) {
  321. uni.showToast({ title: err.message || '操作失败', icon: 'none' })
  322. })
  323. }
  324. }
  325. })
  326. },
  327. rejectRequest: function(requestId) {
  328. var self = this
  329. uni.showModal({
  330. title: '确认拒绝',
  331. content: '确认拒绝该成员的加入申请?',
  332. success: function(modalRes) {
  333. if (modalRes.confirm) {
  334. rejectFamilyJoinRequest(requestId)
  335. .then(function() {
  336. uni.showToast({ title: '已拒绝', icon: 'success' })
  337. self.loadPendingRequests()
  338. self.closeRequests()
  339. })
  340. .catch(function(err) {
  341. uni.showToast({ title: err.message || '操作失败', icon: 'none' })
  342. })
  343. }
  344. }
  345. })
  346. }
  347. }
  348. }
  349. </script>
  350. <style scoped>
  351. .member-strip {
  352. /* FAB 悬浮在左侧边缘中部;禁用 transform(transform 会创建 containing block,
  353. 使后代 .fan-overlay (position: fixed) 退化为相对本容器定位,全屏遮罩失效) */
  354. position: fixed;
  355. top: 50%;
  356. left: 16rpx;
  357. margin-top: -34rpx;
  358. z-index: 100;
  359. }
  360. /* ===== 浮标 ===== */
  361. .fab-wrap {
  362. position: relative;
  363. display: inline-block;
  364. margin: 4rpx 0;
  365. z-index: 1001;
  366. }
  367. .fab-avatar {
  368. width: 68rpx;
  369. height: 68rpx;
  370. border-radius: 50%;
  371. background: #FFFFFF;
  372. box-shadow: 0 4rpx 16rpx rgba(249, 115, 22, 0.25);
  373. border: 3rpx solid #F97316;
  374. position: relative;
  375. transition: transform 0.3s ease;
  376. }
  377. .fab-avatar:active {
  378. transform: scale(0.92);
  379. }
  380. .fab-avatar.active {
  381. transform: rotate(180deg);
  382. }
  383. .fab-img {
  384. width: 100%;
  385. height: 100%;
  386. border-radius: 50%;
  387. }
  388. .fab-badge {
  389. position: absolute;
  390. top: -4rpx;
  391. right: -8rpx;
  392. width: 32rpx;
  393. height: 32rpx;
  394. border-radius: 50%;
  395. background: #EF4444;
  396. border: 2rpx solid #FFFFFF;
  397. display: flex;
  398. align-items: center;
  399. justify-content: center;
  400. }
  401. .badge-count {
  402. font-size: 16rpx;
  403. color: #FFFFFF;
  404. font-weight: bold;
  405. }
  406. .fab-pulse {
  407. position: absolute;
  408. top: 50%;
  409. left: 50%;
  410. width: 68rpx;
  411. height: 68rpx;
  412. margin-top: -34rpx;
  413. margin-left: -34rpx;
  414. border-radius: 50%;
  415. border: 2rpx solid #F97316;
  416. animation: fab-pulse-anim 2s ease-out infinite;
  417. }
  418. @keyframes fab-pulse-anim {
  419. 0% { transform: scale(1); opacity: 0.8; }
  420. 100% { transform: scale(2); opacity: 0; }
  421. }
  422. /* ===== 扇形展开 ===== */
  423. .fan-wrap {
  424. position: absolute;
  425. top: 0;
  426. left: 0;
  427. width: 100%;
  428. height: 100vh;
  429. z-index: 999;
  430. pointer-events: none;
  431. }
  432. .fan-wrap.show {
  433. pointer-events: auto;
  434. }
  435. .fan-overlay {
  436. position: fixed;
  437. top: 0;
  438. left: 0;
  439. right: 0;
  440. bottom: 0;
  441. background: rgba(0, 0, 0, 0.25);
  442. opacity: 0;
  443. transition: opacity 0.3s ease;
  444. z-index: 999;
  445. }
  446. .fan-wrap.show .fan-overlay {
  447. opacity: 1;
  448. }
  449. .fan-container {
  450. position: absolute;
  451. top: 0;
  452. left: 0;
  453. right: 0;
  454. bottom: 0;
  455. padding: 0;
  456. }
  457. .fan-card {
  458. position: absolute;
  459. top: 0;
  460. left: 0;
  461. width: 100rpx;
  462. transform: translate(0, 0) rotate(0deg);
  463. transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease;
  464. opacity: 0;
  465. }
  466. .fan-wrap.show .fan-card {
  467. opacity: 1;
  468. }
  469. .fan-card.selected {
  470. filter: brightness(0.95);
  471. }
  472. .fan-avatar-wrap {
  473. position: relative;
  474. width: 64rpx;
  475. height: 64rpx;
  476. margin: 0 auto;
  477. }
  478. .fan-avatar {
  479. width: 64rpx;
  480. height: 64rpx;
  481. border-radius: 50%;
  482. background: #FFFFFF;
  483. border: 2rpx solid #FFFFFF;
  484. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
  485. }
  486. .fan-relation-badge {
  487. position: absolute;
  488. bottom: -4rpx;
  489. left: 50%;
  490. transform: translateX(-50%);
  491. padding: 1rpx 8rpx;
  492. border-radius: 10rpx;
  493. white-space: nowrap;
  494. max-width: 76rpx;
  495. }
  496. .fan-relation-text {
  497. font-size: 15rpx;
  498. color: #FFFFFF;
  499. font-weight: 500;
  500. line-height: 1.4;
  501. }
  502. .fan-spouse-mark {
  503. position: absolute;
  504. top: -2rpx;
  505. right: -2rpx;
  506. width: 22rpx;
  507. height: 22rpx;
  508. display: flex;
  509. align-items: center;
  510. justify-content: center;
  511. }
  512. .spouse-icon {
  513. font-size: 16rpx;
  514. }
  515. .fan-nickname {
  516. display: block;
  517. margin-top: 4rpx;
  518. text-align: center;
  519. font-size: 20rpx;
  520. color: #FFFFFF;
  521. font-weight: 600;
  522. text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
  523. line-height: 1.2;
  524. max-width: 90rpx;
  525. overflow: hidden;
  526. text-overflow: ellipsis;
  527. white-space: nowrap;
  528. }
  529. .fan-score {
  530. display: flex;
  531. align-items: baseline;
  532. justify-content: center;
  533. margin-top: 2rpx;
  534. }
  535. .score-value {
  536. font-size: 18rpx;
  537. color: #FBBF24;
  538. font-weight: bold;
  539. text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
  540. }
  541. .score-unit {
  542. font-size: 15rpx;
  543. color: rgba(255, 255, 255, 0.8);
  544. margin-left: 2rpx;
  545. text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.4);
  546. }
  547. .fan-selected-mark {
  548. position: absolute;
  549. top: 0;
  550. right: 0;
  551. width: 22rpx;
  552. height: 22rpx;
  553. border-radius: 50%;
  554. background: #F97316;
  555. display: flex;
  556. align-items: center;
  557. justify-content: center;
  558. }
  559. .mark-icon {
  560. font-size: 14rpx;
  561. color: #FFFFFF;
  562. font-weight: bold;
  563. }
  564. /* ===== 操作菜单 ===== */
  565. .action-overlay {
  566. position: fixed;
  567. top: 0; left: 0; right: 0; bottom: 0;
  568. background: rgba(0,0,0,0.4);
  569. z-index: 1001;
  570. }
  571. .action-sheet {
  572. position: fixed;
  573. bottom: 0;
  574. left: 0;
  575. right: 0;
  576. background: #FFFFFF;
  577. border-radius: 24rpx 24rpx 0 0;
  578. z-index: 1002;
  579. padding: 20rpx 0;
  580. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  581. animation: sheet-up 0.3s ease;
  582. }
  583. @keyframes sheet-up {
  584. from { transform: translateY(100%); }
  585. to { transform: translateY(0); }
  586. }
  587. .action-header {
  588. display: flex;
  589. align-items: center;
  590. padding: 16rpx 30rpx 20rpx;
  591. border-bottom: 1rpx solid #F3F4F6;
  592. gap: 16rpx;
  593. }
  594. .action-avatar-wrap {
  595. width: 56rpx;
  596. height: 56rpx;
  597. border-radius: 50%;
  598. overflow: hidden;
  599. flex-shrink: 0;
  600. }
  601. .action-avatar {
  602. width: 56rpx;
  603. height: 56rpx;
  604. border-radius: 50%;
  605. }
  606. .action-info {
  607. display: flex;
  608. flex-direction: column;
  609. gap: 2rpx;
  610. }
  611. .action-title {
  612. font-size: 28rpx;
  613. font-weight: 600;
  614. color: #1F2937;
  615. }
  616. .action-relation {
  617. font-size: 22rpx;
  618. color: #9CA3AF;
  619. }
  620. .action-item {
  621. display: flex;
  622. align-items: center;
  623. padding: 22rpx 30rpx;
  624. border-bottom: 1rpx solid #F9FAFB;
  625. }
  626. .action-item:active {
  627. background: #F3F4F6;
  628. }
  629. .action-item-icon {
  630. font-size: 30rpx;
  631. margin-right: 16rpx;
  632. }
  633. .action-item-text {
  634. font-size: 26rpx;
  635. color: #374151;
  636. }
  637. .action-danger .action-item-text {
  638. color: #EF4444;
  639. }
  640. .action-cancel {
  641. justify-content: center;
  642. margin-top: 8rpx;
  643. border-bottom: none;
  644. }
  645. .action-cancel .action-item-text {
  646. color: #6B7280;
  647. }
  648. /* ===== 待审批请求 ===== */
  649. .request-overlay {
  650. position: fixed;
  651. top: 0; left: 0; right: 0; bottom: 0;
  652. background: rgba(0,0,0,0.4);
  653. z-index: 1003;
  654. }
  655. .request-sheet {
  656. position: fixed;
  657. bottom: 0;
  658. left: 0;
  659. right: 0;
  660. background: #FFFFFF;
  661. border-radius: 24rpx 24rpx 0 0;
  662. z-index: 1004;
  663. padding: 24rpx 0;
  664. padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
  665. animation: sheet-up 0.3s ease;
  666. max-height: 60vh;
  667. overflow-y: auto;
  668. }
  669. .sheet-header {
  670. display: flex;
  671. justify-content: space-between;
  672. align-items: center;
  673. padding: 0 30rpx 16rpx;
  674. border-bottom: 1rpx solid #F3F4F6;
  675. }
  676. .sheet-title {
  677. font-size: 28rpx;
  678. font-weight: 600;
  679. color: #1F2937;
  680. }
  681. .sheet-close {
  682. font-size: 32rpx;
  683. color: #999;
  684. }
  685. .empty-requests {
  686. padding: 60rpx 0;
  687. text-align: center;
  688. font-size: 24rpx;
  689. color: #999;
  690. }
  691. .request-item {
  692. display: flex;
  693. align-items: center;
  694. padding: 20rpx 30rpx;
  695. gap: 16rpx;
  696. }
  697. .req-avatar {
  698. width: 56rpx;
  699. height: 56rpx;
  700. border-radius: 50%;
  701. flex-shrink: 0;
  702. }
  703. .req-info {
  704. flex: 1;
  705. display: flex;
  706. flex-direction: column;
  707. gap: 2rpx;
  708. }
  709. .req-name {
  710. font-size: 26rpx;
  711. color: #333;
  712. }
  713. .req-time {
  714. font-size: 20rpx;
  715. color: #999;
  716. }
  717. .req-actions {
  718. display: flex;
  719. gap: 12rpx;
  720. }
  721. .req-btn {
  722. padding: 8rpx 20rpx;
  723. border-radius: 20rpx;
  724. font-size: 22rpx;
  725. font-weight: 500;
  726. }
  727. .btn-approve {
  728. background: #10B981;
  729. color: #FFFFFF;
  730. }
  731. .btn-reject {
  732. background: #F3F4F6;
  733. color: #666;
  734. }
  735. </style>
  736. .badge-count {
  737. font-size: 26rpx;
  738. font-weight: 700;
  739. color: #EF4444;
  740. }
  741. /* 待审批请求弹窗 */
  742. .request-overlay {
  743. position: fixed;
  744. top: 0; left: 0; right: 0; bottom: 0;
  745. background: rgba(0,0,0,0.4);
  746. z-index: 1500;
  747. }
  748. .request-sheet {
  749. position: fixed;
  750. bottom: 0;
  751. left: 0;
  752. right: 0;
  753. background: #FFFFFF;
  754. border-radius: 24rpx 24rpx 0 0;
  755. z-index: 1501;
  756. max-height: 70vh;
  757. overflow-y: auto;
  758. padding: 20rpx 0;
  759. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  760. }
  761. .sheet-header {
  762. display: flex;
  763. justify-content: space-between;
  764. align-items: center;
  765. padding: 16rpx 30rpx 16rpx;
  766. border-bottom: 1rpx solid #F3F4F6;
  767. }
  768. .sheet-title {
  769. font-size: 32rpx;
  770. font-weight: 700;
  771. color: #1F2937;
  772. }
  773. .sheet-close {
  774. font-size: 40rpx;
  775. color: #9CA3AF;
  776. padding: 8rpx;
  777. line-height: 1;
  778. }
  779. .empty-requests {
  780. text-align: center;
  781. padding: 60rpx 0;
  782. font-size: 28rpx;
  783. color: #9CA3AF;
  784. }
  785. .request-item {
  786. display: flex;
  787. align-items: center;
  788. padding: 20rpx 30rpx;
  789. border-bottom: 1rpx solid #F9FAFB;
  790. }
  791. .req-avatar {
  792. width: 80rpx;
  793. height: 80rpx;
  794. border-radius: 50%;
  795. margin-right: 16rpx;
  796. flex-shrink: 0;
  797. }
  798. .req-info {
  799. flex: 1;
  800. min-width: 0;
  801. }
  802. .req-name {
  803. font-size: 30rpx;
  804. font-weight: 600;
  805. color: #1F2937;
  806. display: block;
  807. }
  808. .req-time {
  809. font-size: 24rpx;
  810. color: #9CA3AF;
  811. display: block;
  812. margin-top: 4rpx;
  813. }
  814. .req-actions {
  815. display: flex;
  816. gap: 12rpx;
  817. }
  818. .req-btn {
  819. padding: 12rpx 24rpx;
  820. border-radius: 8rpx;
  821. font-size: 24rpx;
  822. font-weight: 600;
  823. border: none;
  824. }
  825. .btn-approve {
  826. background: #16A34A;
  827. color: #FFFFFF;
  828. }
  829. .btn-reject {
  830. background: #EF4444;
  831. color: #FFFFFF;
  832. }
  833. </style>