family-members.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. <template>
  2. <view class="container">
  3. <!-- ===== 家庭信息头部 (带齿轮入口) ===== -->
  4. <view class="family-header">
  5. <view class="family-header-content">
  6. <view class="family-header-icon">🏠</view>
  7. <view class="family-header-text">
  8. <text class="family-name">{{ familyName || '我的家庭' }}</text>
  9. <text class="family-member-count">{{ members.length }} 位成员</text>
  10. </view>
  11. <view class="header-settings-btn" @click="showSettings = true">
  12. <text class="settings-gear-icon">⚙️</text>
  13. </view>
  14. </view>
  15. </view>
  16. <!-- ===== 关系标签筛选项 ===== -->
  17. <view class="filter-tabs">
  18. <view class="filter-tab" :class="{active: filterType === 'all'}" @click="filterType = 'all'">全部</view>
  19. <view class="filter-tab" :class="{active: filterType === 'parent'}" @click="filterType = 'parent'">家长</view>
  20. <view class="filter-tab" :class="{active: filterType === 'child'}" @click="filterType = 'child'">孩子</view>
  21. </view>
  22. <!-- ===== 成员列表(点击行→详情弹窗) ===== -->
  23. <view class="member-list">
  24. <view class="member-item" v-for="m in filteredMembers" :key="m.id" @click="showMemberDetail(m)">
  25. <image class="member-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
  26. <view class="member-detail">
  27. <text class="member-name">{{ m.nickname || '未设置' }}</text>
  28. <view class="member-tags">
  29. <text class="member-role-tag">{{ m.relativeLabel || m.roleLabel || (m.effectiveRole === 'parent' ? '家长' : '孩子') }}</text>
  30. <text class="member-source-tag" v-if="m.source === 'family_member'">手动添加</text>
  31. <text class="member-extra" v-if="m.age"> {{ m.age }}岁</text>
  32. <text class="member-extra" v-if="m.gender"> · {{ m.gender === 'male' ? '男' : '女' }}</text>
  33. </view>
  34. <!-- 三维评分进度条 -->
  35. <view class="member-quality-scores" v-if="memberScores[m.id]">
  36. <view class="score-item">
  37. <text class="score-label">信任</text>
  38. <view class="score-bar-bg">
  39. <view class="score-bar-fill trust" :style="{width: (memberScores[m.id].trustScore || 0) + '%'}"></view>
  40. </view>
  41. <text class="score-value">{{ Math.round(memberScores[m.id].trustScore || 0) }}</text>
  42. </view>
  43. <view class="score-item">
  44. <text class="score-label">亲密</text>
  45. <view class="score-bar-bg">
  46. <view class="score-bar-fill intimacy" :style="{width: (memberScores[m.id].intimacyScore || 0) + '%'}"></view>
  47. </view>
  48. <text class="score-value">{{ Math.round(memberScores[m.id].intimacyScore || 0) }}</text>
  49. </view>
  50. <view class="score-item">
  51. <text class="score-label">沟通</text>
  52. <view class="score-bar-bg">
  53. <view class="score-bar-fill communication" :style="{width: (memberScores[m.id].communicationScore || 0) + '%'}"></view>
  54. </view>
  55. <text class="score-value">{{ Math.round(memberScores[m.id].communicationScore || 0) }}</text>
  56. </view>
  57. </view>
  58. </view>
  59. <text class="member-arrow">›</text>
  60. </view>
  61. <!-- 空状态:还没有家庭成员 -->
  62. <view v-if="members.length === 0" class="empty-state-card">
  63. <text class="empty-icon">👨‍👩‍👧‍👦</text>
  64. <text class="empty-title">还没有家庭成员</text>
  65. <view class="empty-actions">
  66. <view class="empty-action-btn add-btn" @click="onAddMember">+ 添加</view>
  67. <view class="empty-action-btn invite-btn" @click="onInvite">📨 邀请</view>
  68. </view>
  69. </view>
  70. <view v-else-if="filteredMembers.length === 0" class="empty-row">
  71. <text class="empty-text">暂无{{ filterType === 'parent' ? '家长' : '孩子' }}成员</text>
  72. </view>
  73. </view>
  74. <!-- ===== Settings 弹窗(齿轮入口) ===== -->
  75. <view class="modal-mask" v-if="showSettings" @click="showSettings = false">
  76. <view class="settings-card" @click.stop>
  77. <view class="settings-header">
  78. <text class="settings-title">成员管理</text>
  79. <text class="settings-close" @click="showSettings = false">×</text>
  80. </view>
  81. <view class="settings-body">
  82. <view class="settings-item" @click="onAddMember">
  83. <text class="settings-item-icon">+</text>
  84. <text class="settings-item-text">添加成员</text>
  85. </view>
  86. <view class="settings-item" @click="onInvite">
  87. <text class="settings-item-icon">📨</text>
  88. <text class="settings-item-text">邀请成员</text>
  89. </view>
  90. </view>
  91. </view>
  92. </view>
  93. <!-- ===== 邀请卡片弹窗 ===== -->
  94. <view class="modal-mask" v-if="showInviteCard" @click="showInviteCard = false">
  95. <view class="invite-card" @click.stop>
  96. <text class="invite-card-title">邀请家庭成员</text>
  97. <text class="invite-card-family">{{ familyName }}</text>
  98. <view class="invite-qr-wrap" v-if="inviteQrCode">
  99. <image class="invite-qr" :src="inviteQrCode" mode="widthFix" />
  100. </view>
  101. <view class="invite-qr-wrap loading-qr" v-else>
  102. <text class="loading-text">生成二维码中...</text>
  103. </view>
  104. <view class="invite-code-section">
  105. <text class="invite-code-label">邀请码</text>
  106. <view class="invite-code-row">
  107. <text class="invite-code">{{ inviteCode }}</text>
  108. <button class="copy-btn" @click="onCopyCode">复制</button>
  109. </view>
  110. </view>
  111. <text class="invite-tip">让好友在「我的」页面输入邀请码加入家庭</text>
  112. <button class="close-btn" @click="showInviteCard = false">关闭</button>
  113. </view>
  114. </view>
  115. <!-- ===== 成员详情弹窗 ===== -->
  116. <view class="modal-mask" v-if="showMemberDetailModal" @click="showMemberDetailModal = false">
  117. <view class="member-detail-card" @click.stop>
  118. <view class="detail-header">
  119. <image class="detail-avatar" :src="detailMember && (detailMember.avatar || '/static/default-avatar.png')" mode="aspectFill"></image>
  120. <view class="detail-info">
  121. <text class="detail-name">{{ detailMember && detailMember.nickname || '' }}</text>
  122. <text class="detail-relation">{{ detailMember && (detailMember.relativeLabel || detailMember.roleLabel || '') }}</text>
  123. </view>
  124. <text class="detail-close" @click="showMemberDetailModal = false">×</text>
  125. </view>
  126. <!-- 详情:性别/年龄/手机号 -->
  127. <view class="detail-meta">
  128. <text class="detail-meta-item" v-if="detailMember && detailMember.gender">性别: {{ detailMember.gender === 'male' ? '男' : '女' }}</text>
  129. <text class="detail-meta-item" v-if="detailMember && detailMember.age">{{ detailMember.age }}岁</text>
  130. <text class="detail-meta-item" v-if="detailMember && detailMember.phone">{{ detailMember.phone }}</text>
  131. </view>
  132. <!-- 三维评分展示 -->
  133. <view class="detail-scores">
  134. <text class="detail-section-title">关系质量</text>
  135. <view class="detail-score-item">
  136. <text class="detail-score-label">信任</text>
  137. <view class="detail-score-bar-bg">
  138. <view class="detail-score-bar-fill trust" :style="{width: (detailMemberScores.trustScore || 0) + '%'}"></view>
  139. </view>
  140. <text class="detail-score-value">{{ Math.round(detailMemberScores.trustScore || 0) }}</text>
  141. </view>
  142. <view class="detail-score-item">
  143. <text class="detail-score-label">亲密</text>
  144. <view class="detail-score-bar-bg">
  145. <view class="detail-score-bar-fill intimacy" :style="{width: (detailMemberScores.intimacyScore || 0) + '%'}"></view>
  146. </view>
  147. <text class="detail-score-value">{{ Math.round(detailMemberScores.intimacyScore || 0) }}</text>
  148. </view>
  149. <view class="detail-score-item">
  150. <text class="detail-score-label">沟通</text>
  151. <view class="detail-score-bar-bg">
  152. <view class="detail-score-bar-fill communication" :style="{width: (detailMemberScores.communicationScore || 0) + '%'}"></view>
  153. </view>
  154. <text class="detail-score-value">{{ Math.round(detailMemberScores.communicationScore || 0) }}</text>
  155. </view>
  156. </view>
  157. <!-- 操作按钮 -->
  158. <view class="detail-actions">
  159. <button class="detail-action-btn action-switch" @click="onSwitchMember(detailMember)" v-if="!(detailMember.source === 'parent' && detailMember.userId === userId)">切换身份</button>
  160. <button class="detail-action-btn action-edit" @click="onEditRelation(detailMember)" v-if="canEditMember(detailMember)">编辑资料</button>
  161. <button class="detail-action-btn action-questionnaire" @click="doQuestionnaire(detailMember)">关系问卷</button>
  162. <button class="detail-action-btn action-interaction" @click="recordInteraction(detailMember)">记录互动</button>
  163. <button class="detail-action-btn action-kick" @click="confirmKick(detailMember)" v-if="isAdmin && detailMember.source === 'family_member'">移出家庭</button>
  164. </view>
  165. </view>
  166. </view>
  167. <!-- ===== 加载状态 ===== -->
  168. <view v-if="loading" class="loading-wrap">
  169. <text class="loading-text">加载中...</text>
  170. </view>
  171. </view>
  172. </template>
  173. <script>
  174. import {
  175. listFamilyMembers,
  176. switchToFamilyMember,
  177. kickFamilyMemberById,
  178. generateFamilyInviteQrCode,
  179. getFamilyMembers,
  180. updateFamilyMember
  181. } from '../../utils/api.js'
  182. import store from '../../store/index.js'
  183. export default {
  184. data() {
  185. return {
  186. userId: 0,
  187. familyId: 0,
  188. familyName: '',
  189. inviteCode: '',
  190. inviteQrCode: '',
  191. creatorId: null,
  192. members: [],
  193. isAdmin: false,
  194. loading: false,
  195. filterType: 'all',
  196. showSettings: false,
  197. showInviteCard: false,
  198. showMemberDetailModal: false,
  199. detailMember: null,
  200. memberScores: {}
  201. }
  202. },
  203. computed: {
  204. filteredMembers() {
  205. var self = this
  206. if (this.filterType === 'all') return this.members
  207. return this.members.filter(function(m) {
  208. return m.effectiveRole === self.filterType
  209. })
  210. },
  211. detailMemberScores() {
  212. if (!this.detailMember) return {}
  213. return this.memberScores[this.detailMember.id] || {}
  214. }
  215. },
  216. onShow() {
  217. var userInfo = uni.getStorageSync('userInfo')
  218. this.userId = (userInfo && userInfo.id) || 0
  219. this.loadMembers()
  220. },
  221. methods: {
  222. async loadMembers() {
  223. this.loading = true
  224. try {
  225. var res = await listFamilyMembers()
  226. if (res.code === 200 && res.data) {
  227. this.members = res.data || []
  228. this.buildScoreMap(res.data)
  229. this.loadFamilyInfo()
  230. }
  231. } catch (e) {
  232. uni.showToast({ title: '加载失败', icon: 'none' })
  233. } finally {
  234. this.loading = false
  235. }
  236. },
  237. buildScoreMap(members) {
  238. var map = {}
  239. if (members && members.length) {
  240. for (var i = 0; i < members.length; i++) {
  241. var m = members[i]
  242. map[m.id] = {
  243. trustScore: m.trustScore || 0,
  244. intimacyScore: m.intimacyScore || 0,
  245. communicationScore: m.communicationScore || 0
  246. }
  247. }
  248. }
  249. this.memberScores = map
  250. },
  251. async loadFamilyInfo() {
  252. try {
  253. var res = await getFamilyMembers()
  254. if (res.code === 200 && res.data) {
  255. var data = res.data
  256. this.familyId = data.familyId
  257. this.familyName = data.familyName
  258. this.inviteCode = data.inviteCode || ''
  259. this.creatorId = data.creatorId
  260. this.isAdmin = this.creatorId === this.userId
  261. }
  262. } catch (e) {
  263. // 静默处理
  264. }
  265. },
  266. showMemberDetail(member) {
  267. this.detailMember = member
  268. this.showMemberDetailModal = true
  269. },
  270. canEditMember(member) {
  271. // 可编辑规则:非登录用户(无userId关联)
  272. if (member.userId) return false
  273. return member.source === 'family_member'
  274. },
  275. onAddMember() {
  276. this.showSettings = false
  277. uni.navigateTo({ url: '/pages/family/add-member' })
  278. },
  279. async onSwitchMember(member) {
  280. this.showMemberDetailModal = false
  281. try {
  282. var res = await switchToFamilyMember(member.id, member.source)
  283. if (res.code === 200 && res.data) {
  284. store.commit('switchToMember', {
  285. memberId: res.data.memberId,
  286. effectiveRole: res.data.effectiveRole
  287. })
  288. uni.showToast({ title: '已切换到' + (member.nickname || '该成员'), icon: 'success' })
  289. setTimeout(function() {
  290. uni.reLaunch({ url: '/pages/index/index' })
  291. }, 1000)
  292. } else {
  293. uni.showToast({ title: res.message || '切换失败', icon: 'none' })
  294. }
  295. } catch (e) {
  296. uni.showToast({ title: '切换失败', icon: 'none' })
  297. }
  298. },
  299. confirmKick(member) {
  300. var self = this
  301. uni.showModal({
  302. title: '移除成员',
  303. content: '确定要将 ' + (member.nickname || '该成员') + ' 移出家庭吗?',
  304. success: function(res) {
  305. if (res.confirm) {
  306. self.onKickMember(member)
  307. }
  308. }
  309. })
  310. },
  311. async onKickMember(member) {
  312. this.showMemberDetailModal = false
  313. try {
  314. var res = await kickFamilyMemberById(member.id)
  315. if (res.code === 200) {
  316. uni.showToast({ title: '已移除', icon: 'success' })
  317. this.loadMembers()
  318. } else {
  319. uni.showToast({ title: res.message || '操作失败', icon: 'none' })
  320. }
  321. } catch (e) {
  322. uni.showToast({ title: '操作失败', icon: 'none' })
  323. }
  324. },
  325. async onInvite() {
  326. this.showSettings = false
  327. this.showInviteCard = true
  328. this.inviteQrCode = ''
  329. try {
  330. var res = await generateFamilyInviteQrCode()
  331. if (res.code === 200 && res.data) {
  332. this.inviteCode = res.data.inviteCode
  333. this.inviteQrCode = res.data.qrCodeBase64
  334. } else {
  335. uni.showToast({ title: res.message || '生成失败', icon: 'none' })
  336. }
  337. } catch (e) {
  338. uni.showToast({ title: '生成失败', icon: 'none' })
  339. }
  340. },
  341. onCopyCode() {
  342. uni.setClipboardData({
  343. data: this.inviteCode,
  344. success: function() {
  345. uni.showToast({ title: '已复制邀请码', icon: 'success' })
  346. }
  347. })
  348. },
  349. onEditRelation(member) {
  350. this.showMemberDetailModal = false
  351. uni.navigateTo({ url: '/pages/profile/user-edit?memberId=' + member.id + '&nickname=' + encodeURIComponent(member.nickname || '') })
  352. },
  353. doQuestionnaire(member) {
  354. this.showMemberDetailModal = false
  355. uni.navigateTo({ url: '/pages/action/relationship-questionnaire?memberId=' + member.id })
  356. },
  357. recordInteraction(member) {
  358. this.showMemberDetailModal = false
  359. uni.navigateTo({ url: '/pages/action/interaction-log?memberId=' + member.id })
  360. }
  361. }
  362. }
  363. </script>
  364. <style>
  365. .container {
  366. padding: 30rpx;
  367. min-height: 100vh;
  368. background: #f5f7fa;
  369. }
  370. /* ===== 家庭信息头部(带齿轮入口) ===== */
  371. .family-header {
  372. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  373. border-radius: 20rpx;
  374. padding: 30rpx 30rpx;
  375. margin-bottom: 20rpx;
  376. color: #fff;
  377. }
  378. .family-header-content {
  379. display: flex;
  380. align-items: center;
  381. }
  382. .family-header-icon {
  383. font-size: 48rpx;
  384. margin-right: 16rpx;
  385. }
  386. .family-header-text {
  387. flex: 1;
  388. }
  389. .family-name {
  390. font-size: 34rpx;
  391. font-weight: bold;
  392. display: block;
  393. }
  394. .family-member-count {
  395. font-size: 24rpx;
  396. opacity: 0.85;
  397. margin-top: 4rpx;
  398. display: block;
  399. }
  400. .header-settings-btn {
  401. width: 64rpx;
  402. height: 64rpx;
  403. display: flex;
  404. align-items: center;
  405. justify-content: center;
  406. border-radius: 50%;
  407. background: rgba(255,255,255,0.2);
  408. }
  409. .header-settings-btn:active {
  410. background: rgba(255,255,255,0.35);
  411. }
  412. .settings-gear-icon {
  413. font-size: 36rpx;
  414. }
  415. /* ===== 筛选标签 ===== */
  416. .filter-tabs {
  417. display: flex;
  418. gap: 12rpx;
  419. margin-bottom: 20rpx;
  420. }
  421. .filter-tab {
  422. padding: 8rpx 28rpx;
  423. background: #fff;
  424. border-radius: 30rpx;
  425. font-size: 26rpx;
  426. color: #666;
  427. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  428. }
  429. .filter-tab.active {
  430. background: #667eea;
  431. color: #fff;
  432. font-weight: 500;
  433. }
  434. .filter-tab:active {
  435. opacity: 0.8;
  436. }
  437. /* ===== 成员列表 ===== */
  438. .member-list {
  439. background: #fff;
  440. border-radius: 16rpx;
  441. overflow: hidden;
  442. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  443. }
  444. .member-item {
  445. display: flex;
  446. align-items: center;
  447. padding: 24rpx 30rpx;
  448. border-bottom: 1rpx solid #f0f0f0;
  449. }
  450. .member-item:last-child {
  451. border-bottom: none;
  452. }
  453. .member-item:active {
  454. background: #f8f8ff;
  455. }
  456. .member-avatar {
  457. width: 72rpx;
  458. height: 72rpx;
  459. border-radius: 50%;
  460. margin-right: 20rpx;
  461. background: #f0f0f0;
  462. flex-shrink: 0;
  463. }
  464. .member-detail {
  465. flex: 1;
  466. min-width: 0;
  467. }
  468. .member-name {
  469. font-size: 30rpx;
  470. font-weight: 500;
  471. display: block;
  472. }
  473. .member-tags {
  474. display: flex;
  475. align-items: center;
  476. margin-top: 6rpx;
  477. gap: 8rpx;
  478. flex-wrap: wrap;
  479. }
  480. .member-role-tag {
  481. font-size: 22rpx;
  482. color: #667eea;
  483. background: #eef0ff;
  484. padding: 2rpx 12rpx;
  485. border-radius: 6rpx;
  486. }
  487. .member-source-tag {
  488. font-size: 22rpx;
  489. color: #F97316;
  490. background: #fff7ed;
  491. padding: 2rpx 12rpx;
  492. border-radius: 6rpx;
  493. }
  494. .member-extra {
  495. font-size: 24rpx;
  496. color: #999;
  497. margin-top: 4rpx;
  498. display: inline;
  499. }
  500. .member-arrow {
  501. font-size: 36rpx;
  502. color: #ccc;
  503. margin-left: 12rpx;
  504. flex-shrink: 0;
  505. }
  506. .empty-row {
  507. padding: 40rpx 0;
  508. text-align: center;
  509. }
  510. .empty-text {
  511. font-size: 26rpx;
  512. color: #bbb;
  513. }
  514. /* ===== 空状态卡片(无家庭成员时显示) ===== */
  515. .empty-state-card {
  516. background: #fff;
  517. border-radius: 16rpx;
  518. padding: 60rpx 30rpx 40rpx;
  519. text-align: center;
  520. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  521. }
  522. .empty-icon {
  523. font-size: 80rpx;
  524. display: block;
  525. margin-bottom: 16rpx;
  526. }
  527. .empty-title {
  528. font-size: 30rpx;
  529. color: #666;
  530. display: block;
  531. margin-bottom: 32rpx;
  532. }
  533. .empty-actions {
  534. display: flex;
  535. justify-content: center;
  536. gap: 20rpx;
  537. }
  538. .empty-action-btn {
  539. padding: 16rpx 40rpx;
  540. border-radius: 12rpx;
  541. font-size: 28rpx;
  542. font-weight: 500;
  543. }
  544. .empty-action-btn:active {
  545. opacity: 0.8;
  546. }
  547. .add-btn {
  548. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  549. color: #fff;
  550. }
  551. .invite-btn {
  552. background: #f0f9ff;
  553. color: #667eea;
  554. }
  555. /* ===== 三维评分进度条(列表行内) ===== */
  556. .member-quality-scores {
  557. display: flex;
  558. flex-direction: column;
  559. gap: 6rpx;
  560. padding: 8rpx 0;
  561. }
  562. .score-item {
  563. display: flex;
  564. align-items: center;
  565. gap: 8rpx;
  566. }
  567. .score-label {
  568. width: 56rpx;
  569. font-size: 20rpx;
  570. color: #999;
  571. }
  572. .score-bar-bg {
  573. flex: 1;
  574. height: 10rpx;
  575. background: #f0f0f0;
  576. border-radius: 5rpx;
  577. overflow: hidden;
  578. }
  579. .score-bar-fill {
  580. height: 100%;
  581. border-radius: 5rpx;
  582. }
  583. .score-bar-fill.trust { background: #10B981; }
  584. .score-bar-fill.intimacy { background: #FF6B9D; }
  585. .score-bar-fill.communication { background: #6366F1; }
  586. .score-value {
  587. width: 44rpx;
  588. font-size: 18rpx;
  589. color: #999;
  590. text-align: right;
  591. }
  592. /* ===== 弹窗遮罩 ===== */
  593. .modal-mask {
  594. position: fixed;
  595. top: 0;
  596. left: 0;
  597. right: 0;
  598. bottom: 0;
  599. background: rgba(0, 0, 0, 0.5);
  600. display: flex;
  601. align-items: center;
  602. justify-content: center;
  603. z-index: 100;
  604. }
  605. /* ===== Settings 弹窗 ===== */
  606. .settings-card {
  607. background: #fff;
  608. border-radius: 24rpx;
  609. width: 500rpx;
  610. overflow: hidden;
  611. }
  612. .settings-header {
  613. display: flex;
  614. align-items: center;
  615. justify-content: space-between;
  616. padding: 30rpx 30rpx 0;
  617. }
  618. .settings-title {
  619. font-size: 32rpx;
  620. font-weight: bold;
  621. }
  622. .settings-close {
  623. font-size: 44rpx;
  624. color: #999;
  625. padding: 4rpx 8rpx;
  626. }
  627. .settings-body {
  628. padding: 20rpx 30rpx 30rpx;
  629. }
  630. .settings-item {
  631. display: flex;
  632. align-items: center;
  633. padding: 22rpx 16rpx;
  634. border-radius: 12rpx;
  635. margin-bottom: 8rpx;
  636. }
  637. .settings-item:active {
  638. background: #f5f5f5;
  639. }
  640. .settings-item-icon {
  641. font-size: 32rpx;
  642. margin-right: 16rpx;
  643. width: 40rpx;
  644. text-align: center;
  645. }
  646. .settings-item-text {
  647. font-size: 28rpx;
  648. color: #333;
  649. }
  650. /* ===== 邀请卡片 ===== */
  651. .invite-card {
  652. background: #fff;
  653. border-radius: 24rpx;
  654. padding: 40rpx;
  655. width: 580rpx;
  656. text-align: center;
  657. }
  658. .invite-card-title {
  659. font-size: 32rpx;
  660. font-weight: bold;
  661. display: block;
  662. }
  663. .invite-card-family {
  664. font-size: 28rpx;
  665. color: #667eea;
  666. display: block;
  667. margin-top: 8rpx;
  668. }
  669. .invite-qr-wrap {
  670. margin: 30rpx auto;
  671. width: 320rpx;
  672. height: 320rpx;
  673. display: flex;
  674. align-items: center;
  675. justify-content: center;
  676. }
  677. .invite-qr {
  678. width: 320rpx;
  679. height: 320rpx;
  680. }
  681. .loading-qr {
  682. background: #f5f5f5;
  683. border-radius: 16rpx;
  684. }
  685. .invite-code-section {
  686. margin: 20rpx 0;
  687. }
  688. .invite-code-label {
  689. font-size: 24rpx;
  690. color: #999;
  691. display: block;
  692. margin-bottom: 8rpx;
  693. }
  694. .invite-code-row {
  695. display: flex;
  696. align-items: center;
  697. justify-content: center;
  698. gap: 16rpx;
  699. }
  700. .invite-code {
  701. font-size: 48rpx;
  702. font-weight: bold;
  703. letter-spacing: 6rpx;
  704. color: #333;
  705. }
  706. .copy-btn {
  707. font-size: 24rpx;
  708. color: #667eea;
  709. background: #eef0ff;
  710. border: none;
  711. padding: 8rpx 20rpx;
  712. border-radius: 8rpx;
  713. line-height: 1.5;
  714. }
  715. .invite-tip {
  716. font-size: 24rpx;
  717. color: #bbb;
  718. display: block;
  719. margin: 20rpx 0;
  720. }
  721. .close-btn {
  722. width: 100%;
  723. background: #f5f5f5;
  724. border: none;
  725. border-radius: 12rpx;
  726. padding: 18rpx 0;
  727. font-size: 28rpx;
  728. color: #666;
  729. }
  730. /* ===== 成员详情弹窗 ===== */
  731. .member-detail-card {
  732. background: #fff;
  733. border-radius: 24rpx;
  734. width: 600rpx;
  735. max-height: 80vh;
  736. overflow-y: auto;
  737. padding: 36rpx 32rpx 32rpx;
  738. }
  739. .detail-header {
  740. display: flex;
  741. align-items: center;
  742. position: relative;
  743. margin-bottom: 20rpx;
  744. }
  745. .detail-avatar {
  746. width: 100rpx;
  747. height: 100rpx;
  748. border-radius: 50%;
  749. margin-right: 20rpx;
  750. background: #f0f0f0;
  751. flex-shrink: 0;
  752. }
  753. .detail-info {
  754. flex: 1;
  755. }
  756. .detail-name {
  757. font-size: 34rpx;
  758. font-weight: bold;
  759. display: block;
  760. }
  761. .detail-relation {
  762. font-size: 26rpx;
  763. color: #667eea;
  764. margin-top: 4rpx;
  765. display: block;
  766. }
  767. .detail-close {
  768. font-size: 44rpx;
  769. color: #999;
  770. padding: 4rpx 8rpx;
  771. position: absolute;
  772. top: -12rpx;
  773. right: -8rpx;
  774. }
  775. .detail-meta {
  776. display: flex;
  777. gap: 16rpx;
  778. flex-wrap: wrap;
  779. margin-bottom: 20rpx;
  780. padding: 16rpx 0;
  781. border-bottom: 1rpx solid #f0f0f0;
  782. }
  783. .detail-meta-item {
  784. font-size: 24rpx;
  785. color: #888;
  786. }
  787. /* 详情三维评分 */
  788. .detail-scores {
  789. margin-bottom: 24rpx;
  790. }
  791. .detail-section-title {
  792. font-size: 28rpx;
  793. font-weight: 600;
  794. display: block;
  795. margin-bottom: 12rpx;
  796. padding-left: 12rpx;
  797. border-left: 4rpx solid #667eea;
  798. }
  799. .detail-score-item {
  800. display: flex;
  801. align-items: center;
  802. gap: 10rpx;
  803. margin-bottom: 8rpx;
  804. }
  805. .detail-score-label {
  806. width: 56rpx;
  807. font-size: 22rpx;
  808. color: #888;
  809. }
  810. .detail-score-bar-bg {
  811. flex: 1;
  812. height: 12rpx;
  813. background: #f0f0f0;
  814. border-radius: 6rpx;
  815. overflow: hidden;
  816. }
  817. .detail-score-bar-fill {
  818. height: 100%;
  819. border-radius: 6rpx;
  820. transition: width 0.3s ease;
  821. }
  822. .detail-score-bar-fill.trust { background: #10B981; }
  823. .detail-score-bar-fill.intimacy { background: #FF6B9D; }
  824. .detail-score-bar-fill.communication { background: #6366F1; }
  825. .detail-score-value {
  826. width: 44rpx;
  827. font-size: 22rpx;
  828. font-weight: 600;
  829. color: #555;
  830. text-align: right;
  831. }
  832. /* ===== 操作按钮区(2列flex网格) ===== */
  833. .detail-actions {
  834. display: flex;
  835. flex-wrap: wrap;
  836. gap: 16rpx;
  837. padding-top: 8rpx;
  838. border-top: 1rpx solid #f0f0f0;
  839. }
  840. .detail-action-btn {
  841. width: calc(50% - 8rpx);
  842. border: none;
  843. border-radius: 12rpx;
  844. padding: 20rpx 0;
  845. font-size: 26rpx;
  846. text-align: center;
  847. line-height: 1.4;
  848. }
  849. .detail-action-btn:active {
  850. opacity: 0.8;
  851. }
  852. .action-switch {
  853. background: #f0f9ff;
  854. color: #0EA5E9;
  855. }
  856. .action-edit {
  857. background: #f0f9eb;
  858. color: #67c23a;
  859. }
  860. .action-questionnaire {
  861. background: #f3e8ff;
  862. color: #8b5cf6;
  863. }
  864. .action-interaction {
  865. background: #fff7ed;
  866. color: #F97316;
  867. }
  868. .action-kick {
  869. background: #fef0f0;
  870. color: #f56c6c;
  871. }
  872. /* ===== 加载 ===== */
  873. .loading-wrap {
  874. text-align: center;
  875. padding: 60rpx 0;
  876. }
  877. .loading-text {
  878. font-size: 28rpx;
  879. color: #999;
  880. }
  881. </style>