challenge-manage.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. <template>
  2. <view class="page">
  3. <BaseLoading :loading="loading" text="加载中..." />
  4. <view v-if="!loading" class="content">
  5. <!-- 首页挑战状态概览 -->
  6. <view class="overview-card" v-if="activeChallenges.length > 0">
  7. <view class="overview-header">
  8. <text class="overview-title">🔥 进行中</text>
  9. <text class="overview-count">{{ activeChallenges.length }} 个挑战</text>
  10. </view>
  11. <view class="active-list">
  12. <view class="active-item" v-for="ch in activeChallenges.slice(0, 2)" :key="ch.id" @click="viewChallenge(ch)">
  13. <text class="active-item-title">{{ ch.title }}</text>
  14. <text class="active-item-meta">{{ ch.targetValue }}/{{ ch.durationDays }}天</text>
  15. </view>
  16. </view>
  17. </view>
  18. <!-- 操作栏 -->
  19. <view class="action-bar">
  20. <button class="btn-new" @click="openCreateModal">
  21. <text class="btn-new-icon">+</text>
  22. <text>新建挑战</text>
  23. </button>
  24. </view>
  25. <!-- 挑战分类列表 -->
  26. <view class="section">
  27. <view class="section-header">
  28. <text class="section-title">我的挑战</text>
  29. </view>
  30. <!-- 进行中 -->
  31. <view v-if="activeChallenges.length > 0" class="group">
  32. <view class="group-label">进行中</view>
  33. <view class="card-list">
  34. <view class="card" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
  35. <view class="card-main">
  36. <view class="card-header">
  37. <text class="card-title">{{ ch.title }}</text>
  38. <text class="card-status status-active">进行中</text>
  39. </view>
  40. <view class="card-meta">
  41. <text class="meta-item">{{ ch.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
  42. <text class="meta-item">{{ ch.durationDays }}天</text>
  43. <text class="meta-item" v-if="ch.rewardPoints">+{{ ch.rewardPoints }}分</text>
  44. </view>
  45. </view>
  46. <view class="card-actions">
  47. <text class="action-btn edit" @click.stop="openEditModal(ch)">编辑</text>
  48. <text class="action-btn del" @click.stop="deleteChallenge(ch.id)">删除</text>
  49. </view>
  50. </view>
  51. </view>
  52. </view>
  53. <!-- 已完成 -->
  54. <view v-if="historyChallenges.length > 0" class="group">
  55. <view class="group-label">已完成</view>
  56. <view class="card-list">
  57. <view class="card" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
  58. <view class="card-main">
  59. <view class="card-header">
  60. <text class="card-title">{{ ch.title }}</text>
  61. <text class="card-status status-done">已完成</text>
  62. </view>
  63. <view class="card-meta">
  64. <text class="meta-item">{{ ch.durationDays }}天</text>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. </view>
  70. <!-- 空状态 -->
  71. <view v-if="activeChallenges.length === 0 && historyChallenges.length === 0" class="empty">
  72. <text class="empty-icon">🎯</text>
  73. <text class="empty-title">还没有挑战</text>
  74. <text class="empty-desc">创建挑战,让全家一起动起来!</text>
  75. <button class="btn-empty" @click="openCreateModal">立即创建</button>
  76. </view>
  77. </view>
  78. </view>
  79. <!-- 创建/编辑弹窗 -->
  80. <view class="overlay" v-if="showModal" @click="closeModal">
  81. <view class="modal" @click.stop>
  82. <view class="modal-header">
  83. <text class="modal-title">{{ editMode ? '编辑挑战' : '新建挑战' }}</text>
  84. <text class="modal-close" @click="closeModal">×</text>
  85. </view>
  86. <!-- 模板选择(仅创建时) -->
  87. <view v-if="!editMode" class="template-row">
  88. <text class="tpl-label">快速模板</text>
  89. <scroll-view scroll-x class="tpl-scroll">
  90. <view class="tpl-list">
  91. <view
  92. class="tpl-item"
  93. :class="{ active: form.challengeType === 'custom' }"
  94. @click="selectTemplate(null)"
  95. >
  96. <text class="tpl-icon">✏️</text>
  97. <text class="tpl-name">自定义</text>
  98. </view>
  99. <view
  100. class="tpl-item"
  101. v-for="tpl in templates"
  102. :key="tpl.type"
  103. :class="{ active: form.challengeType === tpl.type }"
  104. @click="selectTemplate(tpl)"
  105. >
  106. <text class="tpl-icon">{{ tpl.emoji }}</text>
  107. <text class="tpl-name">{{ tpl.title }}</text>
  108. </view>
  109. </view>
  110. </scroll-view>
  111. </view>
  112. <view class="form-body">
  113. <view class="field">
  114. <text class="field-label">挑战标题 <text class="required">*</text></text>
  115. <input class="field-input" v-model="form.title" placeholder="给挑战起个名字" maxlength="50" />
  116. </view>
  117. <view class="field">
  118. <text class="field-label">挑战描述</text>
  119. <textarea class="field-textarea" v-model="form.description" placeholder="描述挑战内容(可选)" maxlength="200" />
  120. </view>
  121. <view class="field-row">
  122. <view class="field half">
  123. <text class="field-label">目标模式</text>
  124. <view class="radio-group">
  125. <view class="radio-btn" :class="{ active: form.targetMode === 'aggregate' }" @click="form.targetMode = 'aggregate'">全家合计</view>
  126. <view class="radio-btn" :class="{ active: form.targetMode === 'all_members' }" @click="form.targetMode = 'all_members'">每人目标</view>
  127. </view>
  128. </view>
  129. <view class="field half">
  130. <text class="field-label">目标值 <text class="required">*</text></text>
  131. <input class="field-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
  132. </view>
  133. </view>
  134. <view class="field-row">
  135. <view class="field half">
  136. <text class="field-label">持续天数</text>
  137. <input class="field-input" type="number" v-model="form.durationDays" placeholder="7" />
  138. </view>
  139. <view class="field half">
  140. <text class="field-label">奖励积分</text>
  141. <input class="field-input" type="number" v-model="form.rewardPoints" placeholder="50" />
  142. </view>
  143. </view>
  144. <view class="field" v-if="!editMode">
  145. <text class="field-label">参与成员</text>
  146. <view class="member-grid">
  147. <view
  148. class="member-item"
  149. :class="{ active: isMemberSelected(m.id) }"
  150. v-for="m in familyMembers"
  151. :key="m.id"
  152. @click="toggleMember(m.id)"
  153. >
  154. <text>{{ m.nickname || m.phone || '成员' }}</text>
  155. </view>
  156. </view>
  157. </view>
  158. </view>
  159. <view class="modal-footer">
  160. <button class="btn-cancel" @click="closeModal">取消</button>
  161. <button class="btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</button>
  162. </view>
  163. </view>
  164. </view>
  165. </view>
  166. </template>
  167. <script>
  168. import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, listFamilyMembers } from '../../utils/api.js'
  169. export default {
  170. data() {
  171. return {
  172. loading: true,
  173. templates: [],
  174. activeChallenges: [],
  175. historyChallenges: [],
  176. familyMembers: [],
  177. selectedMemberIds: [],
  178. showModal: false,
  179. editMode: false,
  180. editingId: null,
  181. form: {
  182. title: '',
  183. description: '',
  184. challengeType: 'custom',
  185. targetMode: 'all_members',
  186. targetValue: '7',
  187. durationDays: '7',
  188. rewardPoints: '50'
  189. }
  190. }
  191. },
  192. onLoad() {
  193. this.loadTemplates()
  194. this.loadMembers()
  195. this.loadData()
  196. },
  197. onShow() {
  198. this.loadData()
  199. },
  200. methods: {
  201. async loadData() {
  202. this.loading = true
  203. try {
  204. const [res1, res2] = await Promise.all([
  205. getChallengeList({}),
  206. getChallengeHistory({})
  207. ])
  208. this.activeChallenges = Array.isArray(res1.data) ? res1.data : []
  209. this.historyChallenges = Array.isArray(res2.data) ? res2.data : []
  210. } catch (e) {
  211. this.activeChallenges = []
  212. this.historyChallenges = []
  213. } finally {
  214. this.loading = false
  215. }
  216. },
  217. async loadTemplates() {
  218. try {
  219. const res = await getChallengeTemplates()
  220. if (res.code === 200) {
  221. this.templates = (res.data || []).map(t => ({
  222. ...t,
  223. emoji: this.getEmoji(t.type)
  224. }))
  225. }
  226. } catch (e) {
  227. this.templates = [
  228. { type: 'full_checkin', title: '全员打卡', emoji: '✅', targetMode: 'all_members', targetValue: 3, durationDays: 3, rewardPoints: 50 },
  229. { type: 'sports', title: '运动PK', emoji: '🏃', targetMode: 'aggregate', targetValue: 300, durationDays: 7, rewardPoints: 100 },
  230. { type: 'reading', title: '阅读挑战', emoji: '📚', targetMode: 'aggregate', targetValue: 200, durationDays: 7, rewardPoints: 80 },
  231. { type: 'gratitude', title: '感恩日记', emoji: '🙏', targetMode: 'all_members', targetValue: 7, durationDays: 7, rewardPoints: 40 }
  232. ]
  233. }
  234. },
  235. getEmoji(type) {
  236. const map = {
  237. full_checkin: '✅', sports: '🏃', reading: '📚',
  238. gratitude: '🙏', no_screen: '📵', custom: '✏️'
  239. }
  240. return map[type] || '🎯'
  241. },
  242. async loadMembers() {
  243. try {
  244. const res = await listFamilyMembers()
  245. this.familyMembers = Array.isArray(res.data) ? res.data : []
  246. this.selectedMemberIds = (this.familyMembers || []).map(m => m.id)
  247. } catch (e) {
  248. this.familyMembers = []
  249. this.selectedMemberIds = []
  250. }
  251. },
  252. isMemberSelected(id) {
  253. return this.selectedMemberIds.indexOf(id) >= 0
  254. },
  255. toggleMember(id) {
  256. var idx = this.selectedMemberIds.indexOf(id)
  257. if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
  258. else this.selectedMemberIds.push(id)
  259. },
  260. selectTemplate(tpl) {
  261. this.form.challengeType = tpl ? tpl.type : 'custom'
  262. if (tpl) {
  263. this.form.targetMode = tpl.targetMode || 'all_members'
  264. this.form.targetValue = String(tpl.targetValue || 7)
  265. this.form.durationDays = String(tpl.durationDays || 7)
  266. this.form.rewardPoints = String(tpl.rewardPoints || 50)
  267. this.form.title = tpl.title || ''
  268. this.form.description = tpl.description || ''
  269. } else {
  270. this.form.targetMode = 'all_members'
  271. this.form.targetValue = '7'
  272. this.form.durationDays = '7'
  273. this.form.rewardPoints = '50'
  274. this.form.title = ''
  275. this.form.description = ''
  276. }
  277. },
  278. openCreateModal() {
  279. this.editMode = false
  280. this.editingId = null
  281. this.form = {
  282. title: '', description: '', challengeType: 'custom',
  283. targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50'
  284. }
  285. this.showModal = true
  286. },
  287. openEditModal(ch) {
  288. this.editMode = true
  289. this.editingId = ch.id
  290. this.form = {
  291. title: ch.title || '',
  292. description: ch.description || '',
  293. challengeType: ch.challengeType || 'custom',
  294. targetMode: ch.targetMode || 'all_members',
  295. targetValue: String(ch.targetValue || 7),
  296. durationDays: String(ch.durationDays || 7),
  297. rewardPoints: String(ch.rewardPoints || 50)
  298. }
  299. this.showModal = true
  300. },
  301. closeModal() {
  302. this.showModal = false
  303. },
  304. async submitForm() {
  305. if (!this.form.title.trim()) {
  306. uni.showToast({ title: '请填写挑战标题', icon: 'none' })
  307. return
  308. }
  309. var targetValue = parseInt(this.form.targetValue || 0, 10)
  310. if (targetValue <= 0) {
  311. uni.showToast({ title: '目标值必须大于0', icon: 'none' })
  312. return
  313. }
  314. var data = {
  315. title: this.form.title.trim(),
  316. description: this.form.description.trim(),
  317. challengeType: this.form.challengeType,
  318. targetMode: this.form.targetMode,
  319. targetValue: targetValue,
  320. durationDays: parseInt(this.form.durationDays || 7, 10),
  321. rewardPoints: parseInt(this.form.rewardPoints || 0, 10)
  322. }
  323. if (!this.editMode) data.participantMemberIds = this.selectedMemberIds
  324. this.loading = true
  325. try {
  326. var promise
  327. if (this.editMode) {
  328. promise = updateChallenge(this.editingId, data)
  329. } else {
  330. promise = createChallenge(data)
  331. }
  332. var res = await promise
  333. if (res.code === 200) {
  334. uni.showToast({ title: this.editMode ? '保存成功' : '创建成功', icon: 'success' })
  335. this.closeModal()
  336. this.loadData()
  337. } else {
  338. uni.showToast({ title: res.message || '操作失败', icon: 'none' })
  339. }
  340. } catch (e) {
  341. uni.showToast({ title: '操作失败', icon: 'none' })
  342. } finally {
  343. this.loading = false
  344. }
  345. },
  346. deleteChallenge(id) {
  347. uni.showModal({
  348. title: '确认删除',
  349. content: '确定要取消这个挑战吗?',
  350. success: (r) => {
  351. if (r.confirm) {
  352. deleteChallenge(id).then((res) => {
  353. if (res.code === 200) {
  354. uni.showToast({ title: '已删除', icon: 'success' })
  355. this.loadData()
  356. } else {
  357. uni.showToast({ title: res.message || '删除失败', icon: 'none' })
  358. }
  359. }).catch(() => uni.showToast({ title: '删除失败', icon: 'none' }))
  360. }
  361. }
  362. })
  363. },
  364. viewChallenge(ch) {
  365. // 跳转到挑战详情
  366. uni.navigateTo({ url: '/pages/activity/index' })
  367. }
  368. }
  369. }
  370. </script>
  371. <style scoped>
  372. .page {
  373. min-height: 100vh;
  374. background: #F5F7FA;
  375. padding: 24rpx;
  376. padding-bottom: 60rpx;
  377. }
  378. .content {
  379. display: flex;
  380. flex-direction: column;
  381. gap: 24rpx;
  382. }
  383. /* 概览卡片 */
  384. .overview-card {
  385. background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
  386. border-radius: 20rpx;
  387. padding: 28rpx;
  388. border: 2rpx solid #FED7AA;
  389. }
  390. .overview-header {
  391. display: flex;
  392. justify-content: space-between;
  393. align-items: center;
  394. margin-bottom: 20rpx;
  395. }
  396. .overview-title {
  397. font-size: 30rpx;
  398. font-weight: 700;
  399. color: #92400E;
  400. }
  401. .overview-count {
  402. font-size: 24rpx;
  403. color: #B45309;
  404. }
  405. .active-list {
  406. display: flex;
  407. flex-direction: column;
  408. gap: 16rpx;
  409. }
  410. .active-item {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. background: rgba(255,255,255,0.7);
  415. border-radius: 12rpx;
  416. padding: 16rpx 20rpx;
  417. }
  418. .active-item-title {
  419. font-size: 28rpx;
  420. font-weight: 600;
  421. color: #7C2D12;
  422. }
  423. .active-item-meta {
  424. font-size: 24rpx;
  425. color: #B45309;
  426. }
  427. /* 操作栏 */
  428. .action-bar {
  429. display: flex;
  430. justify-content: flex-end;
  431. }
  432. .btn-new {
  433. display: flex;
  434. align-items: center;
  435. gap: 8rpx;
  436. background: linear-gradient(135deg, #10B981, #34D399);
  437. color: #fff;
  438. font-size: 26rpx;
  439. font-weight: 600;
  440. padding: 16rpx 32rpx;
  441. border-radius: 40rpx;
  442. border: none;
  443. }
  444. .btn-new-icon {
  445. font-size: 32rpx;
  446. line-height: 1;
  447. }
  448. /* 列表区 */
  449. .section {
  450. display: flex;
  451. flex-direction: column;
  452. gap: 20rpx;
  453. }
  454. .section-header {
  455. display: flex;
  456. justify-content: space-between;
  457. align-items: center;
  458. }
  459. .section-title {
  460. font-size: 30rpx;
  461. font-weight: 700;
  462. color: #1E293B;
  463. }
  464. .group-label {
  465. font-size: 26rpx;
  466. font-weight: 600;
  467. color: #64748B;
  468. margin-bottom: 12rpx;
  469. }
  470. .card-list {
  471. display: flex;
  472. flex-direction: column;
  473. gap: 16rpx;
  474. }
  475. .card {
  476. background: #fff;
  477. border-radius: 16rpx;
  478. padding: 24rpx;
  479. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  480. display: flex;
  481. justify-content: space-between;
  482. align-items: center;
  483. }
  484. .card-main {
  485. flex: 1;
  486. }
  487. .card-header {
  488. display: flex;
  489. justify-content: space-between;
  490. align-items: center;
  491. margin-bottom: 10rpx;
  492. }
  493. .card-title {
  494. font-size: 28rpx;
  495. font-weight: 700;
  496. color: #1E293B;
  497. }
  498. .card-status {
  499. font-size: 22rpx;
  500. padding: 4rpx 12rpx;
  501. border-radius: 20rpx;
  502. }
  503. .status-active {
  504. background: #DCFCE7;
  505. color: #16A34A;
  506. }
  507. .status-done {
  508. background: #F1F5F9;
  509. color: #64748B;
  510. }
  511. .card-meta {
  512. display: flex;
  513. gap: 16rpx;
  514. flex-wrap: wrap;
  515. }
  516. .meta-item {
  517. font-size: 22rpx;
  518. color: #64748B;
  519. background: #F8FAFC;
  520. padding: 4rpx 12rpx;
  521. border-radius: 8rpx;
  522. }
  523. .card-actions {
  524. display: flex;
  525. gap: 12rpx;
  526. margin-left: 16rpx;
  527. }
  528. .action-btn {
  529. font-size: 24rpx;
  530. padding: 8rpx 16rpx;
  531. border-radius: 8rpx;
  532. }
  533. .action-btn.edit {
  534. color: #5B9BD5;
  535. background: rgba(91,155,213,0.1);
  536. }
  537. .action-btn.del {
  538. color: #EF4444;
  539. background: rgba(239,68,68,0.1);
  540. }
  541. /* 空状态 */
  542. .empty {
  543. display: flex;
  544. flex-direction: column;
  545. align-items: center;
  546. padding: 80rpx 0;
  547. }
  548. .empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
  549. .empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
  550. .empty-desc { font-size: 26rpx; color: #94A3B8; margin: 8rpx 0 32rpx; }
  551. .btn-empty {
  552. background: #10B981;
  553. color: #fff;
  554. font-size: 28rpx;
  555. font-weight: 600;
  556. padding: 16rpx 48rpx;
  557. border-radius: 40rpx;
  558. border: none;
  559. }
  560. /* 弹窗 */
  561. .overlay {
  562. position: fixed;
  563. top: 0; left: 0; right: 0; bottom: 0;
  564. background: rgba(0,0,0,0.5);
  565. display: flex;
  566. align-items: flex-end;
  567. z-index: 999;
  568. }
  569. .modal {
  570. width: 100%;
  571. background: #fff;
  572. border-radius: 32rpx 32rpx 0 0;
  573. padding: 32rpx 28rpx 40rpx;
  574. box-sizing: border-box;
  575. max-height: 85vh;
  576. display: flex;
  577. flex-direction: column;
  578. }
  579. .modal-header {
  580. display: flex;
  581. justify-content: space-between;
  582. align-items: center;
  583. margin-bottom: 24rpx;
  584. }
  585. .modal-title {
  586. font-size: 32rpx;
  587. font-weight: 700;
  588. color: #1E293B;
  589. }
  590. .modal-close {
  591. font-size: 44rpx;
  592. color: #94A3B8;
  593. line-height: 1;
  594. }
  595. .form-body {
  596. display: flex;
  597. flex-direction: column;
  598. gap: 20rpx;
  599. flex: 1;
  600. overflow-y: auto;
  601. margin-bottom: 24rpx;
  602. }
  603. /* 模板行 */
  604. .template-row {
  605. display: flex;
  606. align-items: center;
  607. gap: 12rpx;
  608. margin-bottom: 4rpx;
  609. }
  610. .tpl-label {
  611. font-size: 26rpx;
  612. color: #475569;
  613. font-weight: 600;
  614. white-space: nowrap;
  615. }
  616. .tpl-scroll {
  617. flex: 1;
  618. }
  619. .tpl-list {
  620. display: flex;
  621. gap: 12rpx;
  622. }
  623. .tpl-item {
  624. flex-shrink: 0;
  625. display: flex;
  626. flex-direction: column;
  627. align-items: center;
  628. gap: 6rpx;
  629. padding: 14rpx 18rpx;
  630. background: #F8FAFC;
  631. border-radius: 14rpx;
  632. border: 2rpx solid transparent;
  633. }
  634. .tpl-item.active {
  635. border-color: #10B981;
  636. background: #DCFCE7;
  637. }
  638. .tpl-icon { font-size: 32rpx; }
  639. .tpl-name { font-size: 22rpx; color: #475569; font-weight: 500; }
  640. .tpl-item.active .tpl-name { color: #16A34A; }
  641. /* 表单字段 */
  642. .field {
  643. display: flex;
  644. flex-direction: column;
  645. gap: 8rpx;
  646. }
  647. .field-label {
  648. font-size: 26rpx;
  649. color: #475569;
  650. font-weight: 600;
  651. }
  652. .required { color: #EF4444; }
  653. .field-input {
  654. background: #F8FAFC;
  655. border: 1rpx solid #E2E8F0;
  656. border-radius: 12rpx;
  657. padding: 16rpx 18rpx;
  658. font-size: 28rpx;
  659. color: #1E293B;
  660. }
  661. .field-textarea {
  662. background: #F8FAFC;
  663. border: 1rpx solid #E2E8F0;
  664. border-radius: 12rpx;
  665. padding: 16rpx 18rpx;
  666. font-size: 28rpx;
  667. color: #1E293B;
  668. height: 100rpx;
  669. width: 100%;
  670. box-sizing: border-box;
  671. }
  672. .field-row {
  673. display: flex;
  674. gap: 16rpx;
  675. }
  676. .field.half { flex: 1; }
  677. /* 单选组 */
  678. .radio-group {
  679. display: flex;
  680. gap: 12rpx;
  681. }
  682. .radio-btn {
  683. flex: 1;
  684. text-align: center;
  685. padding: 12rpx 0;
  686. border-radius: 12rpx;
  687. font-size: 26rpx;
  688. color: #475569;
  689. background: #F1F5F9;
  690. }
  691. .radio-btn.active {
  692. background: #10B981;
  693. color: #fff;
  694. font-weight: 600;
  695. }
  696. /* 成员选择 */
  697. .member-grid {
  698. display: flex;
  699. flex-wrap: wrap;
  700. gap: 12rpx;
  701. }
  702. .member-item {
  703. padding: 10rpx 22rpx;
  704. border-radius: 24rpx;
  705. font-size: 24rpx;
  706. color: #475569;
  707. background: #F1F5F9;
  708. border: 2rpx solid transparent;
  709. }
  710. .member-item.active {
  711. background: #DCFCE7;
  712. color: #16A34A;
  713. border-color: #10B981;
  714. }
  715. /* 底部按钮 */
  716. .modal-footer {
  717. display: flex;
  718. gap: 16rpx;
  719. padding-top: 8rpx;
  720. }
  721. .btn-cancel, .btn-submit {
  722. flex: 1;
  723. height: 84rpx;
  724. line-height: 84rpx;
  725. border-radius: 42rpx;
  726. font-size: 30rpx;
  727. font-weight: 600;
  728. border: none;
  729. }
  730. .btn-cancel {
  731. background: #F1F5F9;
  732. color: #64748B;
  733. }
  734. .btn-submit {
  735. background: linear-gradient(135deg, #10B981, #34D399);
  736. color: #fff;
  737. }
  738. </style>