index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. <template>
  2. <view class="container">
  3. <!-- Tabbar 切换过渡页 -->
  4. <tab-transition v-if="showTabTransition" dimCode="action" waitingFor="action" />
  5. <!-- 品牌头部 -->
  6. <PageBanner theme="action"
  7. tagline="知行合一 · 快乐成长"
  8. quote="千里之行,始于足下" />
  9. <!-- 游客登录引导卡 -->
  10. <template v-if="!isLoggedIn">
  11. <LoginGuideCard
  12. title="登录后查看任务和活动记录"
  13. :items="[
  14. '查看今日任务,完成打卡获取行动能量',
  15. '报名参加热门活动,记录成长足迹',
  16. '查看专属商品和活动价格'
  17. ]" />
  18. </template>
  19. <!-- 登录后:五维能量条 -->
  20. <FamilyEnergyBar
  21. v-if="isLoggedIn"
  22. dimensionCode="action"
  23. :sandboxData="sandboxData"
  24. :dualDimension="dualDimension" />
  25. <!-- 能量沙盘(登录后可见) — Canvas 五行星图 -->
  26. <WuxingSandbox
  27. v-if="isLoggedIn && sandboxData"
  28. mode="live"
  29. :scores="{
  30. body: sandboxData.bodyScore || 0,
  31. mind: sandboxData.mindScore || 0,
  32. wisdom: sandboxData.wisdomScore || 0,
  33. action: sandboxData.actionScore || 0,
  34. wealth: sandboxData.wealthScore || 0
  35. }"
  36. :overallScore="sandboxData.overallScore || 0"
  37. :members="sandboxData.members || []"
  38. showDimensionBar
  39. starHeight="480rpx" />
  40. <!-- 关系图谱(登录后可见) — 家庭关系网络 -->
  41. <FamilyRelationGraph
  42. v-if="isLoggedIn && familyMembersVisible.length > 0"
  43. dimensionCode="action"
  44. :selfId="activeChildId"
  45. :members="familyMembersVisible"
  46. :energyMap="energyMapForGraph"
  47. :intimacyMap="intimacyMapForGraph"
  48. @memberTap="goMemberDetail" />
  49. <!-- ===== 维度任务/活动/商品 ===== -->
  50. <DimensionTasks
  51. v-if="isLoggedIn"
  52. dimensionCode="action"
  53. :tasks="dimensionTasks"
  54. @taskClick="onTaskClick"
  55. @moreTasks="goTasks" />
  56. <DimensionActivities
  57. dimensionCode="action"
  58. :activities="dimensionActivities"
  59. :isLoggedIn="isLoggedIn"
  60. @activityClick="goActivityDetail"
  61. @moreActivities="goMoreActivities" />
  62. <DimensionProducts
  63. dimensionCode="action"
  64. :products="dimensionProducts"
  65. :isLoggedIn="isLoggedIn"
  66. @productClick="goProductDetail"
  67. @moreProducts="goMoreProducts" />
  68. <!-- 推荐阅读 -->
  69. <DimensionArticles
  70. dimensionCode="action"
  71. :articles="actionArticles"
  72. :isLoggedIn="isLoggedIn"
  73. @articleClick="goArticleDetail"
  74. @moreArticles="goMoreArticles" />
  75. <!-- ===== 重要关系维护 + 关系健康概览 ===== -->
  76. <OctopusGraph
  77. :contactList="contactList"
  78. :healthAlerts="healthAlerts"
  79. :milestones="milestones"
  80. :isLoggedIn="isLoggedIn" />
  81. <!-- 功能入口(4个:活动、商品、课程) -->
  82. <view class="func-section" v-if="sectionVisible('func_entries')">
  83. <view class="func-grid">
  84. <view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
  85. <view class="func-icon-wrap">
  86. <text class="func-icon">{{ item.icon }}</text>
  87. </view>
  88. <text class="func-label">{{ item.label }}</text>
  89. </view>
  90. </view>
  91. </view>
  92. <!-- 导入联系人弹窗 -->
  93. <ContactImport
  94. :visible="showImportModal"
  95. @close="onCloseImport"
  96. @success="onImportSuccess" />
  97. <!-- 底部占位 -->
  98. <view class="bottom-spacer"></view>
  99. </view>
  100. </template>
  101. <script>
  102. import TabTransition from '../../components/tab-transition.vue'
  103. import PageBanner from '../../components/PageBanner.vue'
  104. import LoginGuideCard from '../../components/LoginGuideCard.vue'
  105. import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
  106. import DimensionTasks from '../../components/DimensionTasks.vue'
  107. import DimensionActivities from '../../components/DimensionActivities.vue'
  108. import DimensionProducts from '../../components/DimensionProducts.vue'
  109. import DimensionArticles from '../../components/DimensionArticles.vue'
  110. import ContactCard from '../../components/ContactCard.vue'
  111. import ContactImport from '../../components/ContactImport.vue'
  112. import OctopusGraph from '../../components/OctopusGraph.vue'
  113. import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
  114. import WuxingSandbox from '../../components/wuxing-sandbox.vue'
  115. import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers, getFeaturedArticles, getHealthAlerts, getMilestones } from '../../utils/api.js'
  116. export default {
  117. components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, ContactCard, ContactImport, FamilyRelationGraph, OctopusGraph, WuxingSandbox },
  118. data() {
  119. return {
  120. showTabTransition: true,
  121. isLoggedIn: false,
  122. role: null,
  123. currentChildId: null,
  124. activeChildId: null,
  125. currentChildName: '',
  126. userName: '',
  127. children: [],
  128. familyMembersVisible: [],
  129. sandboxData: null,
  130. dualDimension: null,
  131. visibleSections: [],
  132. dimensionTasks: [],
  133. dimensionActivities: [],
  134. dimensionProducts: [],
  135. articles: [],
  136. contactList: [],
  137. showImportModal: false,
  138. healthAlerts: [],
  139. milestones: [],
  140. funcList: [
  141. { icon: '\u{1F3CB}', label: '任务', needLogin: true, page: '/pages/tasks/tasks' },
  142. { icon: '\u{1F3C0}', label: '活动', needLogin: false, page: '/pages/discover/index?type=activity' },
  143. { icon: '\u{1F6CD}', label: '商城', needLogin: false, page: '/pages/discover/index?type=product' },
  144. { icon: '\u{1F3EB}', label: '课程', needLogin: false, page: '/pages/discover/index?type=course' }
  145. ]
  146. }
  147. },
  148. computed: {
  149. energyMapForGraph: function() {
  150. var map = {}
  151. if (this.sandboxData && this.sandboxData.members) {
  152. for (var i = 0; i < this.sandboxData.members.length; i++) {
  153. var m = this.sandboxData.members[i]
  154. map[m.memberId || m.id] = {
  155. bodyScore: m.bodyScore || 0,
  156. mindScore: m.mindScore || 0,
  157. actionScore: m.actionScore || 0
  158. }
  159. }
  160. }
  161. return map
  162. },
  163. intimacyMapForGraph: function() {
  164. return {}
  165. },
  166. actionArticles: function() {
  167. if (!this.articles || this.articles.length === 0) return []
  168. var result = []
  169. for (var i = 0; i < this.articles.length; i++) {
  170. var a = this.articles[i]
  171. if (a.relatedDimensions && a.relatedDimensions.indexOf('action') !== -1) {
  172. result.push(a)
  173. }
  174. }
  175. if (result.length === 0) return this.articles.slice(0, 5)
  176. return result.slice(0, 5)
  177. }
  178. },
  179. onShow() {
  180. this.showTabTransition = true
  181. var token = uni.getStorageSync('token')
  182. this.isLoggedIn = !!token
  183. if (this.isLoggedIn) {
  184. this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  185. this.currentChildId = uni.getStorageSync('currentChildId') || null
  186. this.userName = uni.getStorageSync('nickname') || uni.getStorageSync('userName') || '家长'
  187. }
  188. this.loadSectionConfig()
  189. if (this.isLoggedIn) {
  190. var self = this
  191. this.loadChildren().finally(function() {
  192. uni.setStorageSync('actionReadyTime', Date.now())
  193. })
  194. this.loadFamilyMembersVisible()
  195. this.loadRelationshipHealth()
  196. }
  197. // 游客也能浏览活动和商品
  198. this.loadDimensionActivities()
  199. this.loadDimensionProducts()
  200. // 游客也能浏览文章
  201. this.loadFeaturedArticles()
  202. this._watchPageReady()
  203. },
  204. methods: {
  205. loadSectionConfig: function() {
  206. var self = this
  207. var baseSections = ['func_entries']
  208. if (!uni.getStorageSync('token')) {
  209. self.visibleSections = baseSections
  210. return
  211. }
  212. var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
  213. getVisibleSections({ pageKey: 'action', role: role }).then(function(res) {
  214. if (res.code === 200 && res.data) {
  215. var keys = res.data.map(function(s) { return s.sectionKey })
  216. var merged = baseSections.slice()
  217. keys.forEach(function(k) { if (merged.indexOf(k) === -1) merged.push(k) })
  218. self.visibleSections = merged
  219. }
  220. }).catch(function() {
  221. self.visibleSections = ['func_entries']
  222. })
  223. },
  224. loadChildren: function() {
  225. var self = this
  226. getChildren().then(function(res) {
  227. if (res.code === 200 && res.data) {
  228. self.children = res.data
  229. var savedChildId = uni.getStorageSync('currentChildId')
  230. var found = false
  231. for (var i = 0; i < self.children.length; i++) {
  232. if (self.children[i].childId === savedChildId) {
  233. self.currentChildId = savedChildId
  234. self.activeChildId = savedChildId
  235. self.currentChildName = self.children[i].name || self.children[i].nickname || '孩子'
  236. found = true
  237. break
  238. }
  239. }
  240. if (!found && self.children.length > 0) {
  241. self.currentChildId = self.children[0].childId
  242. self.activeChildId = self.children[0].childId
  243. self.currentChildName = self.children[0].name || self.children[0].nickname || '孩子'
  244. uni.setStorageSync('currentChildId', self.activeChildId)
  245. }
  246. if (self.currentChildId) {
  247. self.loadDimensionData()
  248. }
  249. self.loadContacts()
  250. }
  251. }).catch(function(e) {
  252. console.log('获取孩子列表失败', e)
  253. })
  254. },
  255. loadDimensionData: function() {
  256. this.loadEnergyData()
  257. this.loadSandboxData()
  258. this.loadDimensionTasks()
  259. this.loadDimensionActivities()
  260. this.loadDimensionProducts()
  261. },
  262. loadEnergyData: function() {
  263. var self = this
  264. if (!this.currentChildId) return
  265. getEnergyOverview(this.currentChildId).then(function(res) {
  266. if (res && res.data) {
  267. var dims = res.data.dimensions
  268. if (dims && dims.length > 0) {
  269. for (var i = 0; i < dims.length; i++) {
  270. if (dims[i].code === 'action') {
  271. self.dualDimension = dims[i]
  272. break
  273. }
  274. }
  275. }
  276. }
  277. }).catch(function(e) {
  278. console.log('获取能量概览失败', e)
  279. })
  280. },
  281. loadSandboxData: function() {
  282. var self = this
  283. getFamilyEnergySandbox().then(function(res) {
  284. if (res && res.data) {
  285. self.sandboxData = res.data
  286. }
  287. }).catch(function() {})
  288. },
  289. loadDimensionTasks: function() {
  290. var self = this
  291. if (!this.currentChildId) return
  292. getTodayTasksByCategory(this.currentChildId, 'action').then(function(res) {
  293. if (res && res.data) {
  294. self.dimensionTasks = Array.isArray(res.data) ? res.data.slice(0, 5) : []
  295. }
  296. }).catch(function() { self.dimensionTasks = [] })
  297. },
  298. loadDimensionActivities: function() {
  299. var self = this
  300. getActivityList({ dimensionCode: 'action', page: 1, size: 3 }).then(function(res) {
  301. if (res && res.data) {
  302. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  303. self.dimensionActivities = list.slice(0, 3)
  304. }
  305. }).catch(function() { self.dimensionActivities = [] })
  306. },
  307. loadDimensionProducts: function() {
  308. var self = this
  309. getProductsByDomain('action', 1, 4).then(function(res) {
  310. if (res && res.data) {
  311. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  312. self.dimensionProducts = list.slice(0, 4)
  313. }
  314. }).catch(function() { self.dimensionProducts = [] })
  315. },
  316. loadContacts: function() {
  317. var self = this
  318. getContactList({}).then(function(res) {
  319. if (res.code === 200 && res.data) {
  320. self.contactList = Array.isArray(res.data) ? res.data : []
  321. }
  322. }).catch(function(e) {
  323. console.log('获取联系人失败', e)
  324. })
  325. },
  326. onContactClick: function(item) {
  327. uni.navigateTo({ url: '/pages/action-detail/contact-detail?id=' + item.id })
  328. },
  329. onShowImport: function() {
  330. this.showImportModal = true
  331. },
  332. onCloseImport: function() {
  333. this.showImportModal = false
  334. },
  335. onImportSuccess: function() {
  336. this.showImportModal = false
  337. this.loadContacts()
  338. },
  339. onChildChanged: function(child) {
  340. this.currentChildId = child.childId
  341. this.currentChildName = child.name || child.nickname || '孩子'
  342. uni.setStorageSync('currentChildId', child.childId)
  343. this.loadDimensionData()
  344. },
  345. loadFamilyMembersVisible: function() {
  346. var self = this
  347. getVisibleFamilyMembers().then(function(res) {
  348. self.familyMembersVisible = res.data || []
  349. }).catch(function(e) {
  350. console.log('获取可见家庭成员失败', e)
  351. self.familyMembersVisible = []
  352. })
  353. },
  354. scrollToSection: function(section) {
  355. uni.pageScrollTo({ selector: '#' + section, duration: 300 })
  356. },
  357. sectionVisible: function(key) {
  358. return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
  359. },
  360. onTaskClick: function(task) {
  361. uni.navigateTo({ url: '/pages/tasks/tasks' })
  362. },
  363. goActivityDetail: function(act) {
  364. if (!act || !act.id) return
  365. uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
  366. },
  367. goMoreActivities: function() {
  368. uni.navigateTo({ url: '/pages/activity/index' })
  369. },
  370. goProductDetail: function(prod) {
  371. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
  372. },
  373. goMoreProducts: function() {
  374. uni.navigateTo({ url: '/pages/shop/index' })
  375. },
  376. onFuncClick: function(item) {
  377. if (item.needLogin && !this.isLoggedIn) {
  378. uni.showModal({
  379. title: '提示',
  380. content: '请先登录后使用此功能',
  381. success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
  382. })
  383. return
  384. }
  385. if (item.page) {
  386. var nativeTabs = ['/pages/tasks/tasks', '/pages/index/index', '/pages/body/index', '/pages/mind/index', '/pages/action/index', '/pages/profile/profile']
  387. if (nativeTabs.indexOf(item.page) !== -1) {
  388. uni.switchTab({ url: item.page })
  389. } else {
  390. uni.navigateTo({ url: item.page })
  391. }
  392. }
  393. },
  394. goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
  395. goMemberDetail: function(member) {
  396. if (!member || !member.memberId) return
  397. uni.navigateTo({
  398. url: '/pages/member-detail/member-detail?memberId=' + member.memberId + '&memberType=' + (member.memberType || 'child') + '&entrySource=relation-graph&dimensionCode=action'
  399. })
  400. },
  401. goLogin: function() { uni.navigateTo({ url: '/pages/login/login' }) },
  402. parseRelatedDimensions: function(val) {
  403. if (!val) return []
  404. if (Array.isArray(val)) return val
  405. if (typeof val === 'string') {
  406. try {
  407. var parsed = JSON.parse(val)
  408. return Array.isArray(parsed) ? parsed : []
  409. } catch (e) {
  410. // 逗号分隔格式 fallback: "action,mind"
  411. return val.split(',').map(function(s) { return s.trim() }).filter(function(s) { return s })
  412. }
  413. }
  414. return []
  415. },
  416. loadFeaturedArticles: function() {
  417. var self = this
  418. getFeaturedArticles({ size: 5, dimensionCode: 'action' }).then(function(res) {
  419. if (res.code === 200 && res.data) {
  420. var gradientColors = [
  421. 'linear-gradient(135deg, #10B981, #34D399)',
  422. 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
  423. 'linear-gradient(135deg, #8B5CF6, #C084FC)',
  424. 'linear-gradient(135deg, #F97316, #FB923C)',
  425. 'linear-gradient(135deg, #6366F1, #818CF8)'
  426. ]
  427. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  428. self.articles = list.map(function(item, index) {
  429. return {
  430. id: item.id,
  431. category: item.categoryName || '行动文章',
  432. categoryColor: gradientColors[index % gradientColors.length],
  433. title: item.title || '',
  434. summary: item.summary || '',
  435. views: item.viewCount ? String(item.viewCount) : '0',
  436. likes: '0',
  437. date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
  438. relatedDimensions: self.parseRelatedDimensions(item.relatedDimensions)
  439. }
  440. })
  441. }
  442. }).catch(function() {})
  443. },
  444. goArticleDetail: function(article) {
  445. var id = article.id || article
  446. uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
  447. },
  448. goMoreArticles: function() {
  449. uni.navigateTo({ url: '/pages/article-center/index?dimension=action' })
  450. },
  451. loadRelationshipHealth: function() {
  452. var self = this
  453. getHealthAlerts({}).then(function(res) {
  454. if (res.code === 200 && res.data) {
  455. self.healthAlerts = Array.isArray(res.data) ? res.data : []
  456. }
  457. }).catch(function() {
  458. self.healthAlerts = []
  459. })
  460. getMilestones({}, 30).then(function(res) {
  461. if (res.code === 200 && res.data) {
  462. self.milestones = Array.isArray(res.data) ? res.data : []
  463. }
  464. }).catch(function() {
  465. self.milestones = []
  466. })
  467. },
  468. goInteractionLog: function(memberId) {
  469. if (memberId) {
  470. uni.navigateTo({ url: '/pages/action-detail/interaction-log?memberId=' + memberId })
  471. } else {
  472. uni.navigateTo({ url: '/pages/action-detail/interaction-log' })
  473. }
  474. },
  475. goRelationshipQuestionnaire: function() {
  476. uni.navigateTo({ url: '/pages/action-detail/relationship-questionnaire' })
  477. },
  478. _watchPageReady() {
  479. var self = this
  480. var unwatch = this.$watch(function() {
  481. if (self.isLoggedIn) {
  482. return !!(self.dualDimension || self.sandboxData || (self.dimensionTasks && self.dimensionTasks.length > 0))
  483. }
  484. return !!(self.dimensionActivities && self.dimensionActivities.length > 0) ||
  485. !!(self.dimensionProducts && self.dimensionProducts.length > 0)
  486. }, function(val) {
  487. if (val) {
  488. if (unwatch) unwatch()
  489. self.showTabTransition = false
  490. }
  491. })
  492. setTimeout(function() {
  493. if (unwatch) unwatch()
  494. self.showTabTransition = false
  495. }, 5000)
  496. }
  497. }
  498. }
  499. </script>
  500. <style scoped>
  501. .container {
  502. min-height: 100vh;
  503. background: #f5f7fa;
  504. padding-bottom: 120rpx;
  505. }
  506. /* ===== 功能入口 ===== */
  507. .func-section {
  508. margin: -40rpx 20rpx 0;
  509. position: relative;
  510. z-index: 2;
  511. }
  512. .func-grid {
  513. background: #fff;
  514. border-radius: 24rpx;
  515. padding: 30rpx 16rpx;
  516. display: flex;
  517. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
  518. }
  519. .func-item {
  520. flex: 1;
  521. display: flex;
  522. flex-direction: column;
  523. align-items: center;
  524. }
  525. .func-item:active {
  526. opacity: 0.7;
  527. }
  528. .func-icon-wrap {
  529. width: 88rpx;
  530. height: 88rpx;
  531. border-radius: 50%;
  532. background: #FFF7ED;
  533. display: flex;
  534. align-items: center;
  535. justify-content: center;
  536. margin-bottom: 10rpx;
  537. }
  538. .func-icon {
  539. font-size: 40rpx;
  540. }
  541. .func-label {
  542. font-size: 24rpx;
  543. color: #333;
  544. font-weight: 500;
  545. }
  546. /* ===== 重要关系 ===== */
  547. .contact-section {
  548. margin: 20rpx 20rpx 0;
  549. }
  550. .contact-header {
  551. display: flex;
  552. justify-content: space-between;
  553. align-items: center;
  554. margin-bottom: 16rpx;
  555. }
  556. .contact-section-title {
  557. font-size: 30rpx;
  558. font-weight: 600;
  559. color: #333;
  560. }
  561. .contact-header-right {
  562. display: flex;
  563. gap: 16rpx;
  564. align-items: center;
  565. }
  566. .contact-more {
  567. font-size: 24rpx;
  568. color: #999;
  569. }
  570. .contact-import-btn {
  571. font-size: 24rpx;
  572. color: #F97316;
  573. font-weight: 500;
  574. }
  575. .contact-scroll {
  576. white-space: nowrap;
  577. overflow: hidden;
  578. }
  579. .contact-scroll-inner {
  580. display: flex;
  581. flex-direction: row;
  582. gap: 16rpx;
  583. padding: 8rpx 0 16rpx;
  584. }
  585. .contact-card-wrap {
  586. flex-shrink: 0;
  587. width: 220rpx;
  588. }
  589. .contact-card-wrap:active {
  590. opacity: 0.8;
  591. }
  592. .contact-add-card {
  593. width: 160rpx;
  594. }
  595. .add-card-inner {
  596. width: 160rpx;
  597. height: 280rpx;
  598. background: #fff;
  599. border-radius: 16rpx;
  600. border: 2rpx dashed #ddd;
  601. display: flex;
  602. flex-direction: column;
  603. align-items: center;
  604. justify-content: center;
  605. gap: 12rpx;
  606. }
  607. .add-card-icon {
  608. font-size: 48rpx;
  609. color: #ccc;
  610. }
  611. .add-card-label {
  612. font-size: 22rpx;
  613. color: #999;
  614. }
  615. .contact-empty {
  616. padding: 40rpx 0;
  617. text-align: center;
  618. }
  619. .contact-empty-text {
  620. font-size: 26rpx;
  621. color: #ccc;
  622. }
  623. /* ===== 关系健康概览 ===== */
  624. .rq-section {
  625. margin: 20rpx 20rpx 0;
  626. }
  627. .rq-header {
  628. display: flex;
  629. justify-content: space-between;
  630. align-items: center;
  631. margin-bottom: 16rpx;
  632. }
  633. .rq-title {
  634. font-size: 30rpx;
  635. font-weight: 600;
  636. color: #333;
  637. }
  638. .rq-more {
  639. font-size: 24rpx;
  640. color: #F97316;
  641. font-weight: 500;
  642. }
  643. /* 健康预警卡片 */
  644. .rq-alerts {
  645. display: flex;
  646. flex-direction: column;
  647. gap: 12rpx;
  648. margin-bottom: 16rpx;
  649. }
  650. .rq-alert-card {
  651. background: #FFF7ED;
  652. border-radius: 16rpx;
  653. padding: 20rpx;
  654. display: flex;
  655. align-items: center;
  656. gap: 16rpx;
  657. border: 1rpx solid #FED7AA;
  658. }
  659. .rq-alert-card:active {
  660. opacity: 0.8;
  661. }
  662. .rq-alert-icon {
  663. width: 64rpx;
  664. height: 64rpx;
  665. border-radius: 50%;
  666. background: #fff;
  667. display: flex;
  668. align-items: center;
  669. justify-content: center;
  670. flex-shrink: 0;
  671. }
  672. .rq-alert-emoji {
  673. font-size: 32rpx;
  674. }
  675. .rq-alert-info {
  676. flex: 1;
  677. display: flex;
  678. flex-direction: column;
  679. gap: 4rpx;
  680. }
  681. .rq-alert-name {
  682. font-size: 28rpx;
  683. font-weight: 600;
  684. color: #333;
  685. }
  686. .rq-alert-desc {
  687. font-size: 24rpx;
  688. color: #F97316;
  689. }
  690. .rq-alert-action {
  691. flex-shrink: 0;
  692. }
  693. .rq-alert-btn {
  694. font-size: 24rpx;
  695. color: #fff;
  696. background: #F97316;
  697. padding: 8rpx 20rpx;
  698. border-radius: 20rpx;
  699. }
  700. /* 里程碑卡片 */
  701. .rq-milestones {
  702. display: flex;
  703. flex-direction: column;
  704. gap: 12rpx;
  705. margin-bottom: 16rpx;
  706. }
  707. .rq-milestone-card {
  708. background: #F0F9FF;
  709. border-radius: 16rpx;
  710. padding: 20rpx;
  711. display: flex;
  712. align-items: center;
  713. gap: 16rpx;
  714. border: 1rpx solid #BAE6FD;
  715. }
  716. .rq-milestone-icon {
  717. width: 64rpx;
  718. height: 64rpx;
  719. border-radius: 50%;
  720. background: #fff;
  721. display: flex;
  722. align-items: center;
  723. justify-content: center;
  724. flex-shrink: 0;
  725. }
  726. .rq-milestone-emoji {
  727. font-size: 32rpx;
  728. }
  729. .rq-milestone-info {
  730. flex: 1;
  731. display: flex;
  732. flex-direction: column;
  733. gap: 4rpx;
  734. }
  735. .rq-milestone-name {
  736. font-size: 28rpx;
  737. font-weight: 600;
  738. color: #333;
  739. }
  740. .rq-milestone-desc {
  741. font-size: 24rpx;
  742. color: #0EA5E9;
  743. }
  744. /* 快捷操作 */
  745. .rq-quick-actions {
  746. display: flex;
  747. gap: 16rpx;
  748. margin-top: 8rpx;
  749. }
  750. .rq-action-btn {
  751. flex: 1;
  752. background: #fff;
  753. border-radius: 16rpx;
  754. padding: 20rpx;
  755. display: flex;
  756. align-items: center;
  757. justify-content: center;
  758. gap: 10rpx;
  759. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  760. }
  761. .rq-action-btn:active {
  762. opacity: 0.8;
  763. }
  764. .rq-action-icon {
  765. font-size: 36rpx;
  766. }
  767. .rq-action-label {
  768. font-size: 26rpx;
  769. color: #333;
  770. font-weight: 500;
  771. }
  772. /* ===== 底部 ===== */
  773. .bottom-spacer {
  774. height: 20rpx;
  775. }
  776. </style>