challenge-manage.vue 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321
  1. <template>
  2. <view class="page">
  3. <BaseLoading :loading="loading" text="加载中..." />
  4. <!-- 顶部双 Tab:共同挑战 / 个人目标 -->
  5. <view class="doa-tabs">
  6. <view class="doa-tab active-tab" @click="goToDoa">🎯 共同挑战</view>
  7. <view class="doa-tab" @click="goToDoaIndex">📋 个人目标</view>
  8. </view>
  9. <view v-if="!loading" class="content">
  10. <!-- 首页挑战状态概览 -->
  11. <view class="overview-card" v-if="activeChallenges.length > 0">
  12. <view class="overview-header">
  13. <text class="overview-title">🔥 进行中</text>
  14. <text class="overview-count">{{ activeChallenges.length }} 个挑战</text>
  15. </view>
  16. <view class="active-list">
  17. <view class="active-item" v-for="ch in activeChallenges.slice(0, 2)" :key="ch.id" @click="viewChallenge(ch)">
  18. <text class="active-item-title">{{ ch.title }}</text>
  19. <text class="active-item-meta">{{ ch.targetValue }}/{{ ch.durationDays }}天</text>
  20. </view>
  21. </view>
  22. </view>
  23. <!-- 操作栏 -->
  24. <view class="action-bar">
  25. <button class="btn-new" @click="openCreateModal">
  26. <text class="btn-new-icon">+</text>
  27. <text>新建挑战</text>
  28. </button>
  29. </view>
  30. <!-- 挑战分类列表 -->
  31. <view class="section">
  32. <view class="section-header">
  33. <text class="section-title">我的挑战</text>
  34. </view>
  35. <!-- 进行中 -->
  36. <view v-if="activeChallenges.length > 0" class="group">
  37. <view class="group-label">进行中</view>
  38. <view class="card-list">
  39. <view class="card" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
  40. <view class="card-main">
  41. <view class="card-header">
  42. <text class="card-title">{{ ch.title }}</text>
  43. <text class="card-status status-active">进行中</text>
  44. </view>
  45. <view class="card-meta">
  46. <text class="meta-item">{{ ch.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
  47. <text class="meta-item">{{ ch.durationDays }}天</text>
  48. <text class="meta-item" v-if="ch.rewardPoints">+{{ ch.rewardPoints }}分</text>
  49. </view>
  50. </view>
  51. <view class="card-actions">
  52. <text class="action-btn edit" @click.stop="openEditModal(ch)">编辑</text>
  53. <text class="action-btn del" @click.stop="deleteChallenge(ch.id)">删除</text>
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 已完成 -->
  59. <view v-if="historyChallenges.length > 0" class="group">
  60. <view class="group-label">已完成</view>
  61. <view class="card-list">
  62. <view class="card" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
  63. <view class="card-main">
  64. <view class="card-header">
  65. <text class="card-title">{{ ch.title }}</text>
  66. <text class="card-status status-done">已完成</text>
  67. </view>
  68. <view class="card-meta">
  69. <text class="meta-item">{{ ch.durationDays }}天</text>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. </view>
  75. <!-- 空状态 -->
  76. <view v-if="activeChallenges.length === 0 && historyChallenges.length === 0" class="empty">
  77. <text class="empty-icon">🎯</text>
  78. <text class="empty-title">还没有挑战</text>
  79. <text class="empty-desc">创建挑战,让全家一起动起来!</text>
  80. <button class="btn-empty" @click="openCreateModal">立即创建</button>
  81. </view>
  82. </view>
  83. </view>
  84. <!-- 创建/编辑弹窗 -->
  85. <view class="overlay" v-if="showModal" @click="closeModal">
  86. <view class="modal" @click.stop>
  87. <view class="modal-header">
  88. <text class="modal-title">{{ editMode ? '编辑挑战' : '新建挑战' }}</text>
  89. <text class="modal-close" @click="closeModal">×</text>
  90. </view>
  91. <!-- 模板选择(仅创建时) -->
  92. <view v-if="!editMode" class="template-row">
  93. <text class="tpl-label">快速模板</text>
  94. <scroll-view scroll-x class="tpl-scroll">
  95. <view class="tpl-list">
  96. <view
  97. class="tpl-item"
  98. :class="{ active: form.challengeType === 'custom' }"
  99. @click="selectTemplate(null)"
  100. >
  101. <text class="tpl-icon">✏️</text>
  102. <text class="tpl-name">自定义</text>
  103. </view>
  104. <view
  105. class="tpl-item"
  106. v-for="tpl in templates"
  107. :key="tpl.type"
  108. :class="{ active: form.challengeType === tpl.type }"
  109. @click="selectTemplate(tpl)"
  110. >
  111. <text class="tpl-icon">{{ tpl.emoji }}</text>
  112. <text class="tpl-name">{{ tpl.title }}</text>
  113. </view>
  114. </view>
  115. </scroll-view>
  116. </view>
  117. <view class="form-body">
  118. <view class="field">
  119. <text class="field-label">挑战标题 <text class="required">*</text></text>
  120. <input class="field-input" v-model="form.title" placeholder="给挑战起个名字" maxlength="50" />
  121. </view>
  122. <view class="field">
  123. <text class="field-label">挑战描述</text>
  124. <textarea class="field-textarea" v-model="form.description" placeholder="描述挑战内容(可选)" maxlength="200" />
  125. </view>
  126. <view class="field-row">
  127. <view class="field half">
  128. <text class="field-label">目标模式</text>
  129. <view class="radio-group">
  130. <view class="radio-btn" :class="{ active: form.targetMode === 'aggregate' }" @click="form.targetMode = 'aggregate'">全家合计</view>
  131. <view class="radio-btn" :class="{ active: form.targetMode === 'all_members' }" @click="form.targetMode = 'all_members'">每人目标</view>
  132. </view>
  133. </view>
  134. <view class="field half">
  135. <text class="field-label">目标值 <text class="required">*</text></text>
  136. <input class="field-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
  137. </view>
  138. </view>
  139. <view class="field-row">
  140. <view class="field half">
  141. <text class="field-label">持续天数</text>
  142. <input class="field-input" type="number" v-model="form.durationDays" placeholder="7" />
  143. </view>
  144. <view class="field half">
  145. <text class="field-label">奖励积分</text>
  146. <input class="field-input" type="number" v-model="form.rewardPoints" placeholder="50" />
  147. </view>
  148. </view>
  149. <view class="field-row" v-if="!editMode">
  150. <view class="field half">
  151. <text class="field-label">回复时效</text>
  152. <view class="radio-group">
  153. <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '24' }" @click="form.respondDeadlineHours = '24'">24h</view>
  154. <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '48' }" @click="form.respondDeadlineHours = '48'">48h</view>
  155. <view class="radio-btn" :class="{ active: form.respondDeadlineHours === '72' }" @click="form.respondDeadlineHours = '72'">72h</view>
  156. </view>
  157. </view>
  158. <view class="field half">
  159. <text class="field-label">超时处理</text>
  160. <view class="field-tip">超时未回复的成员将自动视为拒绝</view>
  161. </view>
  162. </view>
  163. <view class="field" v-if="!editMode || editingStatus === 'pending'">
  164. <text class="field-label">参与成员 <text class="required" v-if="editMode && editingStatus === 'pending'">(待同意挑战可修改)</text></text>
  165. <view class="member-grid">
  166. <view
  167. class="member-item"
  168. :class="{ active: isMemberSelected(m.id) }"
  169. v-for="m in familyMembers"
  170. :key="m.id"
  171. @click="toggleMember(m.id)"
  172. >
  173. <text>{{ m.nickname || m.phone || '成员' }}</text>
  174. </view>
  175. </view>
  176. </view>
  177. </view>
  178. <view class="modal-footer">
  179. <button class="btn-cancel" @click="closeModal">取消</button>
  180. <button class="btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</button>
  181. </view>
  182. </view>
  183. </view>
  184. <!-- 挑战详情弹窗 -->
  185. <view class="overlay" v-if="showDetailModal" @click="closeDetailModal">
  186. <view class="modal detail-modal" @click.stop>
  187. <view class="modal-header">
  188. <text class="modal-title">挑战详情</text>
  189. <text class="modal-close" @click="closeDetailModal">×</text>
  190. </view>
  191. <view v-if="detailChallenge" class="detail-body">
  192. <view class="detail-header">
  193. <text class="detail-title">{{ detailChallenge.title }}</text>
  194. <text class="detail-badge" :class="detailStatusClass">{{ detailStatusText }}</text>
  195. </view>
  196. <text class="detail-desc">{{ detailChallenge.description || '' }}</text>
  197. <!-- 待同意:显示确认进度(pending 状态挑战) -->
  198. <view class="pending-section" v-if="detailChallenge.status === 'pending'">
  199. <text class="pending-hint">等待成员确认(已同意 {{ detailAgreedCount }}/{{ detailTotalCount }})</text>
  200. </view>
  201. <!-- 目标展示(两种模式) -->
  202. <view class="detail-target">
  203. <view class="detail-row">
  204. <text class="detail-label">目标模式:</text>
  205. <text>{{ detailChallenge.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
  206. </view>
  207. <view class="detail-row">
  208. <text class="detail-label">目标值:</text>
  209. <text>{{ detailChallenge.targetValue }}</text>
  210. </view>
  211. <view class="detail-row">
  212. <text class="detail-label">持续天数:</text>
  213. <text>{{ detailChallenge.durationDays }}天</text>
  214. </view>
  215. <view class="detail-row" v-if="detailChallenge.startDate">
  216. <text class="detail-label">开始日期:</text>
  217. <text>{{ detailChallenge.startDate.slice(0,10) }}</text>
  218. </view>
  219. <view class="detail-row" v-if="detailChallenge.endDate">
  220. <text class="detail-label">截止日期:</text>
  221. <text>{{ detailChallenge.endDate.slice(0,10) }}</text>
  222. </view>
  223. <view class="detail-row" v-if="detailChallenge.respondDeadline">
  224. <text class="detail-label">回复截止:</text>
  225. <text :class="detailDeadlinePassed ? 'detail-label detail-label-danger' : 'detail-label'">{{ detailChallenge.respondDeadline.slice(0,16).replace('T',' ') }}{{ detailDeadlinePassed ? '(已超时)' : '' }}</text>
  226. </view>
  227. <view class="detail-row" v-if="detailChallenge.rewardPoints">
  228. <text class="detail-label">奖励积分:</text>
  229. <text class="detail-label detail-label-reward">+{{ detailChallenge.rewardPoints }}分</text>
  230. </view>
  231. </view>
  232. <!-- 活动/已完成进度 -->
  233. <view class="detail-progress" v-if="detailChallenge.status !== 'pending'">
  234. <text class="detail-progress-title">进度</text>
  235. <view class="detail-progress-bar-wrap">
  236. <view class="detail-progress-bar-bg">
  237. <view class="detail-progress-bar-fill" :style="{width: detailProgressPercent + '%'}"></view>
  238. </view>
  239. <text class="detail-progress-text">{{ detailChallenge.totalProgress || 0 }}/{{ detailChallenge.targetValue }}</text>
  240. </view>
  241. <view class="detail-member-list" v-if="detailChallenge.memberProgress && detailChallenge.memberProgress.length > 0">
  242. <view class="detail-member-row" v-for="mp in detailChallenge.memberProgress" :key="mp.memberId">
  243. <text class="detail-member-name">{{ mp.nickname }}</text>
  244. <text class="detail-member-val">{{ mp.progressValue || 0 }}/{{ detailChallenge.targetValue }}</text>
  245. <text class="detail-member-status" :class="'member-' + mp.status">{{ detailMemberStatusLabel(mp.status) }}</text>
  246. </view>
  247. </view>
  248. </view>
  249. <!-- 同意/拒绝按钮 -->
  250. <view class="detail-action" v-if="detailChallenge.status === 'pending' && detailChallenge.myStatus === 'pending'">
  251. <view class="respond-btn respond-agree" @click="onDetailRespond('agree')">✅ 同意</view>
  252. <view class="respond-btn respond-reject" @click="onDetailRespond('reject')">✖ 拒绝</view>
  253. </view>
  254. <!-- 编辑/删除按钮(仅创建者) -->
  255. <view class="detail-action" v-if="creatorCanOperate">
  256. <button class="btn-edit-detail" @click="closeDetailModal; openEditModal(detailChallenge)">编辑</button>
  257. <button class="btn-delete-detail" @click="confirmDeleteDetail">删除</button>
  258. </view>
  259. </view>
  260. <view class="modal-footer">
  261. <button class="btn-cancel" @click="closeDetailModal">关闭</button>
  262. </view>
  263. </view>
  264. </view>
  265. <!-- 家庭成员不足提示弹窗 -->
  266. <view class="overlay" v-if="showMemberPrompt" @click="showMemberPrompt = false">
  267. <view class="modal prompt-modal" @click.stop>
  268. <view class="modal-header">
  269. <text class="modal-title">无法创建挑战</text>
  270. <text class="modal-close" @click="showMemberPrompt = false">×</text>
  271. </view>
  272. <view class="prompt-content">
  273. <text class="prompt-icon">👨‍👩‍👧‍👦</text>
  274. <text class="prompt-title">家庭成员不足</text>
  275. <text class="prompt-desc">家庭挑战至少需要 2 名成员参与,请先添加家庭成员</text>
  276. </view>
  277. <view class="modal-footer">
  278. <button class="btn-cancel" @click="showMemberPrompt = false">暂不添加</button>
  279. <button class="btn-submit" @click="goToAddMember">去添加成员</button>
  280. </view>
  281. </view>
  282. </view>
  283. <!-- 免费会员引导添加孩子弹窗 -->
  284. <view class="overlay" v-if="showAddChildPrompt" @click="showAddChildPrompt = false">
  285. <view class="modal prompt-modal" @click.stop>
  286. <view class="modal-header">
  287. <text class="modal-title">免费会员专属提示</text>
  288. <text class="modal-close" @click="showAddChildPrompt = false">×</text>
  289. </view>
  290. <view class="prompt-content">
  291. <text class="prompt-icon">👶</text>
  292. <text class="prompt-title">需先添加孩子成员</text>
  293. <text class="prompt-desc">免费会员只能添加 1 位孩子成员,添加后即可创建家庭挑战。<br/>挑战创建后,可切换到孩子身份同意参与。</text>
  294. </view>
  295. <view class="modal-footer">
  296. <button class="btn-cancel" @click="showAddChildPrompt = false">暂不添加</button>
  297. <button class="btn-submit" @click="goToAddChild">去添加孩子</button>
  298. </view>
  299. </view>
  300. </view>
  301. </view>
  302. </template>
  303. <script>
  304. import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, getFamilyMemberList, getMyMembership, respondChallenge } from '../../utils/api.js'
  305. import store from '../../store/index.js'
  306. export default {
  307. data() {
  308. return {
  309. loading: true,
  310. templates: [],
  311. activeChallenges: [],
  312. historyChallenges: [],
  313. familyMembers: [],
  314. selectedMemberIds: [],
  315. showModal: false,
  316. showDetailModal: false,
  317. detailChallenge: null,
  318. showMemberPrompt: false,
  319. showAddChildPrompt: false,
  320. editMode: false,
  321. editingId: null,
  322. editingStatus: null,
  323. form: {
  324. title: '',
  325. description: '',
  326. challengeType: 'custom',
  327. targetMode: 'all_members',
  328. targetValue: '7',
  329. durationDays: '7',
  330. rewardPoints: '50',
  331. respondDeadlineHours: '48'
  332. },
  333. isFreeMember: false
  334. }
  335. },
  336. computed: {
  337. detailStatusClass: function() {
  338. if (!this.detailChallenge) return ''
  339. if (this.detailChallenge.status === 'active') return 'status-active'
  340. if (this.detailChallenge.status === 'pending') return 'status-pending'
  341. if (this.detailChallenge.status === 'completed') return 'status-completed'
  342. if (this.detailChallenge.status === 'cancelled') return 'status-cancelled'
  343. return 'status-active'
  344. },
  345. detailStatusText: function() {
  346. if (!this.detailChallenge) return ''
  347. if (this.detailChallenge.status === 'active') return '进行中'
  348. if (this.detailChallenge.status === 'pending') return '待同意'
  349. if (this.detailChallenge.status === 'completed') return '已完成'
  350. if (this.detailChallenge.status === 'cancelled') return '已取消'
  351. return '进行中'
  352. },
  353. detailAgreedCount: function() {
  354. if (!this.detailChallenge || !this.detailChallenge.memberProgress) return 0
  355. var count = 0
  356. for (var i = 0; i < this.detailChallenge.memberProgress.length; i++) {
  357. if (this.detailChallenge.memberProgress[i].status === 'agreed') count++
  358. }
  359. return count
  360. },
  361. detailTotalCount: function() {
  362. if (!this.detailChallenge || !this.detailChallenge.memberProgress) return 0
  363. return this.detailChallenge.memberProgress.length
  364. },
  365. detailProgressPercent: function() {
  366. if (!this.detailChallenge || this.detailChallenge.targetValue <= 0) return 0
  367. return Math.round(this.detailChallenge.totalProgress / this.detailChallenge.targetValue * 100)
  368. },
  369. detailDeadlinePassed: function() {
  370. if (!this.detailChallenge || !this.detailChallenge.respondDeadline) return false
  371. return new Date().getTime() > new Date(this.detailChallenge.respondDeadline).getTime()
  372. },
  373. creatorCanOperate: function() {
  374. if (!this.detailChallenge) return false
  375. return String(this.detailChallenge.creatorId) === String(this.$store.state.userId)
  376. },
  377. detailMemberStatusLabel: function(status) {
  378. var map = { pending: '待确认', agreed: '已同意', rejected: '已拒绝' }
  379. return map[status] || status
  380. }
  381. },
  382. onLoad(options) {
  383. this._pendingChallengeId = options && options.id
  384. this.loadTemplates()
  385. this.loadMembers()
  386. this.checkMembership()
  387. this.loadData()
  388. this._onLoadFired = true
  389. },
  390. onShow() {
  391. if (this._onLoadFired) {
  392. this._onLoadFired = false
  393. } else {
  394. this.loadData()
  395. }
  396. },
  397. methods: {
  398. goToDoa() {
  399. // 当前即共同挑战页
  400. },
  401. goToDoaIndex() {
  402. uni.navigateTo({ url: '/pages/health/doa-index' })
  403. },
  404. async loadData() {
  405. this.loading = true
  406. try {
  407. const [res1, res2] = await Promise.all([
  408. getChallengeList({}),
  409. getChallengeHistory({})
  410. ])
  411. this.activeChallenges = Array.isArray(res1.data) ? res1.data : []
  412. this.historyChallenges = Array.isArray(res2.data) ? res2.data : []
  413. this.openPendingChallengeIfNeeded()
  414. } catch (e) {
  415. this.activeChallenges = []
  416. this.historyChallenges = []
  417. } finally {
  418. this.loading = false
  419. }
  420. },
  421. // 从首页卡片点击进入时,自动展开对应挑战详情弹窗
  422. openPendingChallengeIfNeeded() {
  423. var id = this._pendingChallengeId
  424. if (!id) return
  425. this._pendingChallengeId = null
  426. var found = null
  427. for (var i = 0; i < this.activeChallenges.length; i++) {
  428. if (String(this.activeChallenges[i].id) === String(id)) { found = this.activeChallenges[i]; break }
  429. }
  430. if (!found) {
  431. for (var j = 0; j < this.historyChallenges.length; j++) {
  432. if (String(this.historyChallenges[j].id) === String(id)) { found = this.historyChallenges[j]; break }
  433. }
  434. }
  435. if (found) this.viewChallenge(found)
  436. },
  437. async loadTemplates() {
  438. try {
  439. const res = await getChallengeTemplates()
  440. if (res.code === 200) {
  441. this.templates = (res.data || []).map(t => ({
  442. ...t,
  443. emoji: this.getEmoji(t.type)
  444. }))
  445. }
  446. } catch (e) {
  447. this.templates = [
  448. { type: 'full_checkin', title: '全员打卡', emoji: '✅', targetMode: 'all_members', targetValue: 3, durationDays: 3, rewardPoints: 50 },
  449. { type: 'sports', title: '运动PK', emoji: '🏃', targetMode: 'aggregate', targetValue: 300, durationDays: 7, rewardPoints: 100 },
  450. { type: 'reading', title: '阅读挑战', emoji: '📚', targetMode: 'aggregate', targetValue: 200, durationDays: 7, rewardPoints: 80 },
  451. { type: 'gratitude', title: '感恩日记', emoji: '🙏', targetMode: 'all_members', targetValue: 7, durationDays: 7, rewardPoints: 40 }
  452. ]
  453. }
  454. },
  455. getEmoji(type) {
  456. const map = {
  457. full_checkin: '✅', sports: '🏃', reading: '📚',
  458. gratitude: '🙏', no_screen: '📵', custom: '✏️'
  459. }
  460. return map[type] || '🎯'
  461. },
  462. async loadMembers() {
  463. try {
  464. const res = await getFamilyMemberList()
  465. this.familyMembers = Array.isArray(res.data) ? res.data : []
  466. this.selectedMemberIds = (this.familyMembers || []).map(m => m.id)
  467. } catch (e) {
  468. this.familyMembers = []
  469. this.selectedMemberIds = []
  470. }
  471. },
  472. async checkMembership() {
  473. try {
  474. const res = await getMyMembership()
  475. const m = res.data || {}
  476. const level = m.memberLevel || m.levelCode || ''
  477. // 免费会员:level 为空或 FREE
  478. this.isFreeMember = !level || level === 'FREE'
  479. } catch (e) {
  480. this.isFreeMember = true // 默认按免费处理,保守策略
  481. }
  482. },
  483. isMemberSelected(id) {
  484. return this.selectedMemberIds.indexOf(id) >= 0
  485. },
  486. toggleMember(id) {
  487. var idx = this.selectedMemberIds.indexOf(id)
  488. if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
  489. else this.selectedMemberIds.push(id)
  490. },
  491. selectTemplate(tpl) {
  492. this.form.challengeType = tpl ? tpl.type : 'custom'
  493. if (tpl) {
  494. this.form.targetMode = tpl.targetMode || 'all_members'
  495. this.form.targetValue = String(tpl.targetValue || 7)
  496. this.form.durationDays = String(tpl.durationDays || 7)
  497. this.form.rewardPoints = String(tpl.rewardPoints || 50)
  498. this.form.title = tpl.title || ''
  499. this.form.description = tpl.description || ''
  500. } else {
  501. this.form.targetMode = 'all_members'
  502. this.form.targetValue = '7'
  503. this.form.durationDays = '7'
  504. this.form.rewardPoints = '50'
  505. this.form.title = ''
  506. this.form.description = ''
  507. }
  508. },
  509. openCreateModal() {
  510. // 免费会员且只有1个成员(自己)时,引导先添加孩子
  511. if (this.familyMembers.length < 2) {
  512. if (this.isFreeMember && this.familyMembers.length === 1) {
  513. this.showAddChildPrompt = true
  514. } else {
  515. this.showMemberPrompt = true
  516. }
  517. return
  518. }
  519. this.editMode = false
  520. this.editingId = null
  521. this.form = {
  522. title: '', description: '', challengeType: 'custom',
  523. targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50',
  524. respondDeadlineHours: '48'
  525. }
  526. this.selectedMemberIds = this.familyMembers.map(m => m.id)
  527. this.showModal = true
  528. },
  529. openEditModal(ch) {
  530. this.editMode = true
  531. this.editingId = ch.id
  532. this.editingStatus = ch.status
  533. this.form = {
  534. title: ch.title || '',
  535. description: ch.description || '',
  536. challengeType: ch.challengeType || 'custom',
  537. targetMode: ch.targetMode || 'all_members',
  538. targetValue: String(ch.targetValue || 7),
  539. durationDays: String(ch.durationDays || 7),
  540. rewardPoints: String(ch.rewardPoints || 50)
  541. }
  542. // 编辑时预填充参与成员:从 memberProgress 读取所有状态的成员
  543. var selected = []
  544. if (ch.memberProgress && ch.memberProgress.length) {
  545. for (var i = 0; i < ch.memberProgress.length; i++) {
  546. selected.push(ch.memberProgress[i].memberId)
  547. }
  548. }
  549. this.selectedMemberIds = selected
  550. this.showModal = true
  551. },
  552. closeModal() {
  553. this.showModal = false
  554. },
  555. async submitForm() {
  556. if (!this.form.title.trim()) {
  557. uni.showToast({ title: '请填写挑战标题', icon: 'none' })
  558. return
  559. }
  560. var targetValue = parseInt(this.form.targetValue || 0, 10)
  561. if (targetValue <= 0) {
  562. uni.showToast({ title: '目标值必须大于0', icon: 'none' })
  563. return
  564. }
  565. if (!this.editMode && this.selectedMemberIds.length < 2) {
  566. uni.showToast({ title: '家庭挑战至少需要选择2名成员参与', icon: 'none' })
  567. return
  568. }
  569. var data = {
  570. title: this.form.title.trim(),
  571. description: this.form.description.trim(),
  572. challengeType: this.form.challengeType,
  573. targetMode: this.form.targetMode,
  574. targetValue: targetValue,
  575. durationDays: parseInt(this.form.durationDays || 7, 10),
  576. rewardPoints: parseInt(this.form.rewardPoints || 0, 10)
  577. }
  578. if (!this.editMode || this.editingStatus === 'pending') data.participantMemberIds = this.selectedMemberIds
  579. if (!this.editMode && this.form.respondDeadlineHours) data.respondDeadlineHours = parseInt(this.form.respondDeadlineHours, 10)
  580. this.loading = true
  581. try {
  582. var promise
  583. if (this.editMode) {
  584. promise = updateChallenge(this.editingId, data)
  585. } else {
  586. promise = createChallenge(data)
  587. }
  588. var res = await promise
  589. if (res.code === 200) {
  590. uni.showToast({ title: this.editMode ? '保存成功' : '创建成功', icon: 'success' })
  591. this.closeModal()
  592. this.loadData()
  593. } else {
  594. uni.showToast({ title: res.message || '操作失败', icon: 'none' })
  595. }
  596. } catch (e) {
  597. uni.showToast({ title: '操作失败', icon: 'none' })
  598. } finally {
  599. this.loading = false
  600. }
  601. },
  602. deleteChallenge(id) {
  603. uni.showModal({
  604. title: '确认删除',
  605. content: '确定要取消这个挑战吗?',
  606. success: (r) => {
  607. if (r.confirm) {
  608. deleteChallenge(id).then((res) => {
  609. if (res.code === 200) {
  610. uni.showToast({ title: '已删除', icon: 'success' })
  611. this.loadData()
  612. } else {
  613. uni.showToast({ title: res.message || '删除失败', icon: 'none' })
  614. }
  615. }).catch(() => uni.showToast({ title: '删除失败', icon: 'none' }))
  616. }
  617. }
  618. })
  619. },
  620. viewChallenge(ch) {
  621. // 打开挑战详情弹窗
  622. this.detailChallenge = ch
  623. this.showDetailModal = true
  624. },
  625. closeDetailModal() {
  626. this.showDetailModal = false
  627. this.detailChallenge = null
  628. },
  629. async onDetailRespond(action) {
  630. var self = this
  631. uni.showModal({
  632. title: '确认操作',
  633. content: action === 'agree' ? '确认同意参与该挑战吗?' : '确认拒绝该挑战吗?',
  634. success: function(res) {
  635. if (res.confirm) {
  636. respondChallenge({ challengeId: self.detailChallenge.id, action: action }).then(function(resp) {
  637. if (resp && resp.code === 200) {
  638. uni.showToast({ title: action === 'agree' ? '已同意' : '已拒绝', icon: 'success' })
  639. self.closeDetailModal()
  640. self.loadData()
  641. } else {
  642. uni.showToast({ title: (resp && resp.message) || '操作失败', icon: 'none' })
  643. }
  644. }).catch(function() {
  645. uni.showToast({ title: '操作失败,请重试', icon: 'none' })
  646. })
  647. }
  648. }
  649. })
  650. },
  651. confirmDeleteDetail() {
  652. var self = this
  653. if (!this.detailChallenge) return
  654. uni.showModal({
  655. title: '确认删除',
  656. content: '确定要取消这个挑战吗?',
  657. success: function(r) {
  658. if (r.confirm) {
  659. deleteChallenge(self.detailChallenge.id).then(function(res) {
  660. if (res.code === 200) {
  661. uni.showToast({ title: '已删除', icon: 'success' })
  662. self.closeDetailModal()
  663. self.loadData()
  664. } else {
  665. uni.showToast({ title: res.message || '删除失败', icon: 'none' })
  666. }
  667. }).catch(function() {
  668. uni.showToast({ title: '删除失败', icon: 'none' })
  669. })
  670. }
  671. }
  672. })
  673. },
  674. goToAddMember() {
  675. this.showMemberPrompt = false
  676. uni.navigateTo({ url: '/pages/profile-extra/family-members' })
  677. },
  678. goToAddChild() {
  679. this.showAddChildPrompt = false
  680. uni.navigateTo({ url: '/pages/family/add-member' })
  681. }
  682. }
  683. }
  684. </script>
  685. <style scoped>
  686. .doa-tabs {
  687. display: flex;
  688. background: #fff;
  689. border-radius: 16rpx;
  690. margin-bottom: 20rpx;
  691. overflow: hidden;
  692. border: 2rpx solid #E2E8F0;
  693. }
  694. .doa-tab {
  695. flex: 1;
  696. text-align: center;
  697. padding: 20rpx 0;
  698. font-size: 28rpx;
  699. font-weight: 600;
  700. color: #64748B;
  701. background: #fff;
  702. }
  703. .active-tab {
  704. background: #10B981;
  705. color: #fff;
  706. }
  707. .page {
  708. min-height: 100vh;
  709. background: #F5F7FA;
  710. padding: 24rpx;
  711. padding-bottom: 60rpx;
  712. }
  713. .content {
  714. display: flex;
  715. flex-direction: column;
  716. gap: 24rpx;
  717. }
  718. /* 概览卡片 */
  719. .overview-card {
  720. background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
  721. border-radius: 20rpx;
  722. padding: 28rpx;
  723. border: 2rpx solid #FED7AA;
  724. }
  725. .overview-header {
  726. display: flex;
  727. justify-content: space-between;
  728. align-items: center;
  729. margin-bottom: 20rpx;
  730. }
  731. .overview-title {
  732. font-size: 30rpx;
  733. font-weight: 700;
  734. color: #92400E;
  735. }
  736. .overview-count {
  737. font-size: 24rpx;
  738. color: #B45309;
  739. }
  740. .active-list {
  741. display: flex;
  742. flex-direction: column;
  743. gap: 16rpx;
  744. }
  745. .active-item {
  746. display: flex;
  747. justify-content: space-between;
  748. align-items: center;
  749. background: rgba(255,255,255,0.7);
  750. border-radius: 12rpx;
  751. padding: 16rpx 20rpx;
  752. }
  753. .active-item-title {
  754. font-size: 28rpx;
  755. font-weight: 600;
  756. color: #7C2D12;
  757. }
  758. .active-item-meta {
  759. font-size: 24rpx;
  760. color: #B45309;
  761. }
  762. /* 操作栏 */
  763. .action-bar {
  764. display: flex;
  765. justify-content: flex-end;
  766. }
  767. .btn-new {
  768. display: flex;
  769. align-items: center;
  770. gap: 8rpx;
  771. background: linear-gradient(135deg, #10B981, #34D399);
  772. color: #fff;
  773. font-size: 26rpx;
  774. font-weight: 600;
  775. padding: 16rpx 32rpx;
  776. border-radius: 40rpx;
  777. border: none;
  778. }
  779. .btn-new-icon {
  780. font-size: 32rpx;
  781. line-height: 1;
  782. }
  783. /* 列表区 */
  784. .section {
  785. display: flex;
  786. flex-direction: column;
  787. gap: 20rpx;
  788. }
  789. .section-header {
  790. display: flex;
  791. justify-content: space-between;
  792. align-items: center;
  793. }
  794. .section-title {
  795. font-size: 30rpx;
  796. font-weight: 700;
  797. color: #1E293B;
  798. }
  799. .group-label {
  800. font-size: 26rpx;
  801. font-weight: 600;
  802. color: #64748B;
  803. margin-bottom: 12rpx;
  804. }
  805. .card-list {
  806. display: flex;
  807. flex-direction: column;
  808. gap: 16rpx;
  809. }
  810. .card {
  811. background: #fff;
  812. border-radius: 16rpx;
  813. padding: 24rpx;
  814. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  815. display: flex;
  816. justify-content: space-between;
  817. align-items: center;
  818. }
  819. .card-main {
  820. flex: 1;
  821. }
  822. .card-header {
  823. display: flex;
  824. justify-content: space-between;
  825. align-items: center;
  826. margin-bottom: 10rpx;
  827. }
  828. .card-title {
  829. font-size: 28rpx;
  830. font-weight: 700;
  831. color: #1E293B;
  832. }
  833. .card-status {
  834. font-size: 22rpx;
  835. padding: 4rpx 12rpx;
  836. border-radius: 20rpx;
  837. }
  838. .status-active {
  839. background: #DCFCE7;
  840. color: #16A34A;
  841. }
  842. .status-done {
  843. background: #F1F5F9;
  844. color: #64748B;
  845. }
  846. .card-meta {
  847. display: flex;
  848. gap: 16rpx;
  849. flex-wrap: wrap;
  850. }
  851. .meta-item {
  852. font-size: 22rpx;
  853. color: #64748B;
  854. background: #F8FAFC;
  855. padding: 4rpx 12rpx;
  856. border-radius: 8rpx;
  857. }
  858. .card-actions {
  859. display: flex;
  860. gap: 12rpx;
  861. margin-left: 16rpx;
  862. }
  863. .action-btn {
  864. font-size: 24rpx;
  865. padding: 8rpx 16rpx;
  866. border-radius: 8rpx;
  867. }
  868. .action-btn.edit {
  869. color: #5B9BD5;
  870. background: rgba(91,155,213,0.1);
  871. }
  872. .action-btn.del {
  873. color: #EF4444;
  874. background: rgba(239,68,68,0.1);
  875. }
  876. /* 空状态 */
  877. .empty {
  878. display: flex;
  879. flex-direction: column;
  880. align-items: center;
  881. padding: 80rpx 0;
  882. }
  883. .empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
  884. .empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
  885. .empty-desc { font-size: 26rpx; color: #94A3B8; margin: 8rpx 0 32rpx; }
  886. .btn-empty {
  887. background: #10B981;
  888. color: #fff;
  889. font-size: 28rpx;
  890. font-weight: 600;
  891. padding: 16rpx 48rpx;
  892. border-radius: 40rpx;
  893. border: none;
  894. }
  895. /* 弹窗 */
  896. .overlay {
  897. position: fixed;
  898. top: 0; left: 0; right: 0; bottom: 0;
  899. background: rgba(0,0,0,0.5);
  900. display: flex;
  901. align-items: flex-end;
  902. z-index: 999;
  903. }
  904. .modal {
  905. width: 100%;
  906. background: #fff;
  907. border-radius: 32rpx 32rpx 0 0;
  908. padding: 32rpx 28rpx 40rpx;
  909. box-sizing: border-box;
  910. max-height: 85vh;
  911. display: flex;
  912. flex-direction: column;
  913. }
  914. .modal-header {
  915. display: flex;
  916. justify-content: space-between;
  917. align-items: center;
  918. margin-bottom: 24rpx;
  919. }
  920. .modal-title {
  921. font-size: 32rpx;
  922. font-weight: 700;
  923. color: #1E293B;
  924. }
  925. .modal-close {
  926. font-size: 44rpx;
  927. color: #94A3B8;
  928. line-height: 1;
  929. }
  930. .form-body {
  931. display: flex;
  932. flex-direction: column;
  933. gap: 20rpx;
  934. flex: 1;
  935. overflow-y: auto;
  936. margin-bottom: 24rpx;
  937. }
  938. /* 模板行 */
  939. .template-row {
  940. display: flex;
  941. align-items: center;
  942. gap: 12rpx;
  943. margin-bottom: 4rpx;
  944. }
  945. .tpl-label {
  946. font-size: 26rpx;
  947. color: #475569;
  948. font-weight: 600;
  949. white-space: nowrap;
  950. }
  951. .tpl-scroll {
  952. flex: 1;
  953. }
  954. .tpl-list {
  955. display: flex;
  956. gap: 12rpx;
  957. }
  958. .tpl-item {
  959. flex-shrink: 0;
  960. display: flex;
  961. flex-direction: column;
  962. align-items: center;
  963. gap: 6rpx;
  964. padding: 14rpx 18rpx;
  965. background: #F8FAFC;
  966. border-radius: 14rpx;
  967. border: 2rpx solid transparent;
  968. }
  969. .tpl-item.active {
  970. border-color: #10B981;
  971. background: #DCFCE7;
  972. }
  973. .tpl-icon { font-size: 32rpx; }
  974. .tpl-name { font-size: 22rpx; color: #475569; font-weight: 500; }
  975. .tpl-item.active .tpl-name { color: #16A34A; }
  976. /* 表单字段 */
  977. .field {
  978. display: flex;
  979. flex-direction: column;
  980. gap: 8rpx;
  981. }
  982. .field-label {
  983. font-size: 26rpx;
  984. color: #475569;
  985. font-weight: 600;
  986. }
  987. .required { color: #EF4444; }
  988. .field-input {
  989. background: #F8FAFC;
  990. border: 1rpx solid #E2E8F0;
  991. border-radius: 12rpx;
  992. padding: 16rpx 18rpx;
  993. font-size: 28rpx;
  994. color: #1E293B;
  995. }
  996. .field-textarea {
  997. background: #F8FAFC;
  998. border: 1rpx solid #E2E8F0;
  999. border-radius: 12rpx;
  1000. padding: 16rpx 18rpx;
  1001. font-size: 28rpx;
  1002. color: #1E293B;
  1003. height: 100rpx;
  1004. width: 100%;
  1005. box-sizing: border-box;
  1006. }
  1007. .field-row {
  1008. display: flex;
  1009. gap: 16rpx;
  1010. }
  1011. .field.half { flex: 1; }
  1012. /* 单选组 */
  1013. .radio-group {
  1014. display: flex;
  1015. gap: 12rpx;
  1016. }
  1017. .radio-btn {
  1018. flex: 1;
  1019. text-align: center;
  1020. padding: 12rpx 0;
  1021. border-radius: 12rpx;
  1022. font-size: 26rpx;
  1023. color: #475569;
  1024. background: #F1F5F9;
  1025. }
  1026. .radio-btn.active {
  1027. background: #10B981;
  1028. color: #fff;
  1029. font-weight: 600;
  1030. }
  1031. /* 成员选择 */
  1032. .member-grid {
  1033. display: flex;
  1034. flex-wrap: wrap;
  1035. gap: 12rpx;
  1036. }
  1037. .member-item {
  1038. padding: 10rpx 22rpx;
  1039. border-radius: 24rpx;
  1040. font-size: 24rpx;
  1041. color: #475569;
  1042. background: #F1F5F9;
  1043. border: 2rpx solid transparent;
  1044. }
  1045. .member-item.active {
  1046. background: #DCFCE7;
  1047. color: #16A34A;
  1048. border-color: #10B981;
  1049. }
  1050. /* 详情弹窗 */
  1051. .detail-modal {
  1052. max-width: 90vw;
  1053. padding: 32rpx 28rpx;
  1054. background: #fff;
  1055. border-radius: 24rpx;
  1056. box-shadow: 0 8rpx 48rpx rgba(0,0,0,0.12);
  1057. }
  1058. .detail-modal .modal-header {
  1059. display: flex;
  1060. justify-content: space-between;
  1061. align-items: center;
  1062. margin-bottom: 24rpx;
  1063. padding-bottom: 16rpx;
  1064. border-bottom: 2rpx solid #E2E8F0;
  1065. }
  1066. .detail-modal .modal-title {
  1067. font-size: 34rpx;
  1068. font-weight: 700;
  1069. color: #1E293B;
  1070. }
  1071. .detail-modal .modal-close {
  1072. font-size: 30rpx;
  1073. color: #64748B;
  1074. cursor: pointer;
  1075. }
  1076. .detail-modal .modal-close:hover {
  1077. color: #DC2626;
  1078. }
  1079. .detail-body {
  1080. font-size: 26rpx;
  1081. line-height: 1.6;
  1082. color: #475569;
  1083. }
  1084. .detail-header {
  1085. display: flex;
  1086. justify-content: space-between;
  1087. align-items: center;
  1088. margin-bottom: 12rpx;
  1089. }
  1090. .detail-header .detail-title {
  1091. font-size: 30rpx;
  1092. font-weight: 700;
  1093. color: #1E293B;
  1094. }
  1095. .detail-header .detail-badge {
  1096. font-size: 22rpx;
  1097. padding: 2rpx 8rpx;
  1098. border-radius: 999rpx;
  1099. }
  1100. .detail-header .status-active { background: #DCFCE7; color: #16A34A; }
  1101. .detail-header .status-pending { background: #FEF3C7; color: #B45309; }
  1102. .detail-header .status-completed { background: #F1F5F9; color: #64748B; }
  1103. .detail-header .status-cancelled { background: #FEE2E2; color: #DC2626; }
  1104. .detail-header .status-not-launched { background: #E5E7EB; color: #6B7280; }
  1105. .detail-desc {
  1106. font-size: 24rpx;
  1107. color: #64748B;
  1108. line-height: 1.4;
  1109. margin-bottom: 16rpx;
  1110. display: block;
  1111. }
  1112. .detail-target {
  1113. margin-top: 16rpx;
  1114. }
  1115. .detail-row {
  1116. display: flex;
  1117. justify-content: space-between;
  1118. margin-bottom: 8rpx;
  1119. }
  1120. .detail-row .detail-label {
  1121. font-size: 24rpx;
  1122. color: #647489;
  1123. font-weight: 500;
  1124. }
  1125. .detail-row .detail-value {
  1126. font-size: 26rpx;
  1127. color: #10B981;
  1128. font-weight: 600;
  1129. }
  1130. .detail-row .detail-value-danger {
  1131. font-size: 26rpx;
  1132. color: #DC2626;
  1133. }
  1134. .detail-progress {
  1135. margin-top: 16rpx;
  1136. padding-top: 16rpx;
  1137. border-top: 2rpx solid #E2E8F0;
  1138. }
  1139. .detail-progress-title {
  1140. font-size: 24rpx;
  1141. color: #64748B;
  1142. margin-bottom: 12rpx;
  1143. }
  1144. .detail-progress-bar-wrap {
  1145. height: 10rpx;
  1146. background: #E2E8F0;
  1147. border-radius: 999rpx;
  1148. overflow: hidden;
  1149. margin-bottom: 8rpx;
  1150. }
  1151. .detail-progress-bar-fill {
  1152. height: 100%;
  1153. background: linear-gradient(90deg, #10B981, #34D399);
  1154. border-radius: 999rpx;
  1155. transition: width 0.3s ease;
  1156. }
  1157. .detail-progress-text {
  1158. display: flex;
  1159. justify-content: space-between;
  1160. font-size: 22rpx;
  1161. color: #64748B;
  1162. }
  1163. .detail-member-list {
  1164. margin-top: 12rpx;
  1165. }
  1166. .detail-member-row {
  1167. display: flex;
  1168. justify-content: space-between;
  1169. align-items: center;
  1170. padding: 8rpx 0;
  1171. border-bottom: 1rpx solid #F1F5F9;
  1172. font-size: 24rpx;
  1173. }
  1174. .detail-member-name {
  1175. color: #1E293B;
  1176. font-weight: 500;
  1177. }
  1178. .detail-member-val {
  1179. color: #64748B;
  1180. font-weight: 400;
  1181. }
  1182. .detail-member-status {
  1183. padding: 2rpx 6rpx;
  1184. border-radius: 999rpx;
  1185. font-size: 20rpx;
  1186. font-weight: 400;
  1187. }
  1188. .detail-member-pending { background: #FEF3C7; color: #B45309; }
  1189. .detail-member-agreed { background: #DCFCE7; color: #16A34A; }
  1190. .detail-member-rejected { background: #FEE2E2; color: #DC2626; }
  1191. .detail-action {
  1192. display: flex;
  1193. gap: 12rpx;
  1194. margin-top: 20rpx;
  1195. }
  1196. .detail-action .respond-btn {
  1197. flex: 1;
  1198. height: 72rpx;
  1199. line-height: 72rpx;
  1200. font-size: 28rpx;
  1201. font-weight: 500;
  1202. }
  1203. .detail-action .btn-edit-detail {
  1204. flex: 1;
  1205. height: 72rpx;
  1206. line-height: 72rpx;
  1207. font-size: 28rpx;
  1208. background: #F3F4F6;
  1209. color: #374151;
  1210. border-radius: 12rpx;
  1211. }
  1212. .detail-action .btn-delete-detail {
  1213. flex: 1;
  1214. height: 72rpx;
  1215. line-height: 72rpx;
  1216. font-size: 28rpx;
  1217. background: #FEE2E2;
  1218. color: #DC2626;
  1219. border-radius: 12rpx;
  1220. }
  1221. .detail-label-reward {
  1222. color: #F59E0B;
  1223. font-weight: 500;
  1224. }
  1225. .detail-label-danger {
  1226. color: #DC2626;
  1227. }
  1228. /* 底部按钮 */
  1229. .modal-footer {
  1230. display: flex;
  1231. gap: 16rpx;
  1232. padding-top: 8rpx;
  1233. }
  1234. .btn-cancel, .btn-submit {
  1235. flex: 1;
  1236. height: 84rpx;
  1237. line-height: 84rpx;
  1238. border-radius: 42rpx;
  1239. font-size: 30rpx;
  1240. font-weight: 600;
  1241. border: none;
  1242. }
  1243. .btn-cancel {
  1244. background: #F1F5F9;
  1245. color: #64748B;
  1246. }
  1247. .btn-submit {
  1248. background: linear-gradient(135deg, #10B981, #34D399);
  1249. color: #fff;
  1250. }
  1251. /* 家庭成员不足提示弹窗 */
  1252. .prompt-modal {
  1253. max-height: 60vh;
  1254. padding: 32rpx 28rpx 40rpx;
  1255. }
  1256. .prompt-content {
  1257. display: flex;
  1258. flex-direction: column;
  1259. align-items: center;
  1260. padding: 24rpx 0 16rpx;
  1261. }
  1262. .prompt-icon {
  1263. font-size: 80rpx;
  1264. margin-bottom: 16rpx;
  1265. }
  1266. .prompt-title {
  1267. font-size: 32rpx;
  1268. font-weight: 700;
  1269. color: #1E293B;
  1270. margin-bottom: 12rpx;
  1271. }
  1272. .prompt-desc {
  1273. font-size: 26rpx;
  1274. color: #64748B;
  1275. text-align: center;
  1276. line-height: 1.6;
  1277. }
  1278. </style>