index.vue 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201
  1. <template>
  2. <view class="activity-container">
  3. <!-- Nav bar with title "活动中心" and admin "+" button -->
  4. <view class="nav-bar">
  5. <text class="nav-title">活动中心</text>
  6. <view class="nav-add-btn" @click="goCreate">
  7. <text class="nav-add-icon">+</text>
  8. </view>
  9. </view>
  10. <!-- Tab bar (4 tabs) -->
  11. <view class="tab-bar">
  12. <view
  13. class="tab-item"
  14. :class="{ active: currentTab === tab.key }"
  15. v-for="tab in tabs"
  16. :key="tab.key"
  17. @click="onTabChange(tab.key)"
  18. >
  19. <text class="tab-label">{{ tab.label }}</text>
  20. </view>
  21. </view>
  22. <!-- ScrollView content area -->
  23. <scroll-view
  24. class="content-scroll"
  25. scroll-y
  26. :refresher-enabled="true"
  27. :refresher-triggered="isRefreshing"
  28. @refresherrefresh="onRefresh"
  29. @scrolltolower="onLoadMore"
  30. >
  31. <!-- Tab 1: mine -->
  32. <view v-if="currentTab === 'mine'">
  33. <!-- Filter chips -->
  34. <scroll-view class="filter-scroll" scroll-x enable-flex>
  35. <view class="filter-chips">
  36. <view
  37. class="filter-chip"
  38. :class="{ active: currentMineFilter === filter.value }"
  39. v-for="filter in mineFilters"
  40. :key="filter.value"
  41. @click="onMineFilterChange(filter.value)"
  42. >
  43. <text class="filter-label">{{ filter.label }}</text>
  44. </view>
  45. </view>
  46. </scroll-view>
  47. <!-- Activity cards -->
  48. <view class="card-list" v-if="myActivities.length > 0">
  49. <view
  50. class="activity-card"
  51. v-for="act in myActivities"
  52. :key="act.id"
  53. @click="goDetail(act)"
  54. >
  55. <image
  56. class="card-cover"
  57. :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
  58. mode="aspectFill"
  59. />
  60. <view class="card-info">
  61. <text class="card-title">{{ act.title }}</text>
  62. <text class="card-meta" v-if="act.registrantName">报名人:{{ act.registrantName }}</text>
  63. <text class="card-meta">{{ act.startTime }}</text>
  64. <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
  65. <view class="card-bottom">
  66. <text class="card-status" :class="'status-' + (act._cls || 'upcoming')">
  67. {{ act._txt || '待开始' }}
  68. </text>
  69. <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
  70. <text class="card-price free" v-else>免费</text>
  71. </view>
  72. <!-- Action buttons for approved status -->
  73. <view
  74. v-if="act.status === 'approved'"
  75. class="card-actions"
  76. >
  77. <button class="btn-cancel" @click.stop="onCancel(act)">取消报名</button>
  78. <button class="btn-checkin" @click.stop="onCheckin(act)">签到</button>
  79. </view>
  80. </view>
  81. </view>
  82. </view>
  83. <!-- Empty state for mine -->
  84. <view class="empty-state" v-else-if="!mineLoading">
  85. <text class="empty-icon">📋</text>
  86. <text class="empty-title">暂无报名的活动</text>
  87. <text class="empty-desc">去发现页探索精彩活动吧</text>
  88. <button class="empty-btn" @click="switchToDiscover">去发现活动</button>
  89. </view>
  90. <!-- Loading state -->
  91. <view class="loading-state" v-if="mineLoading">
  92. <view class="loading-spinner"></view>
  93. <text class="loading-text">加载中...</text>
  94. </view>
  95. <!-- Load more tip -->
  96. <view class="load-more-tip" v-if="myActivities.length > 0 && !hasMore && currentTab === 'mine'">
  97. <text class="load-more-text">没有更多了</text>
  98. </view>
  99. </view>
  100. <!-- Tab 2: discover -->
  101. <view v-if="currentTab === 'discover'">
  102. <!-- Filter chips (dimension) -->
  103. <scroll-view class="filter-scroll" scroll-x enable-flex>
  104. <view class="filter-chips">
  105. <view
  106. class="filter-chip"
  107. :class="{ active: currentDimension === filter.value }"
  108. v-for="filter in dimensionFilters"
  109. :key="filter.value"
  110. @click="onDimensionChange(filter.value)"
  111. >
  112. <text class="filter-label">{{ filter.label }}</text>
  113. </view>
  114. </view>
  115. </scroll-view>
  116. <!-- Activity cards -->
  117. <view class="card-list" v-if="discoverActivities.length > 0">
  118. <view
  119. class="activity-card"
  120. v-for="act in discoverActivities"
  121. :key="act.id"
  122. @click="goDetail(act)"
  123. >
  124. <image
  125. class="card-cover"
  126. :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
  127. mode="aspectFill"
  128. />
  129. <view class="card-info">
  130. <text class="card-title">{{ act.title }}</text>
  131. <text class="card-meta">{{ act.startTime }}</text>
  132. <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
  133. <view class="card-bottom">
  134. <text class="card-status" :class="'status-' + (act.displayStatus || 'open')">
  135. {{ act.displayLabel || '报名中' }}
  136. </text>
  137. <text class="card-price" v-if="hasFee(act)">¥{{ minFee(act) }}</text>
  138. <text class="card-price free" v-else>免费</text>
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. <!-- Empty state for discover -->
  144. <view class="empty-state" v-else-if="!discoverLoading">
  145. <text class="empty-icon">🔍</text>
  146. <text class="empty-title">暂无活动</text>
  147. <text class="empty-desc">稍后再来看看吧</text>
  148. </view>
  149. <!-- Loading state -->
  150. <view class="loading-state" v-if="discoverLoading">
  151. <view class="loading-spinner"></view>
  152. <text class="loading-text">加载中...</text>
  153. </view>
  154. <!-- Load more tip -->
  155. <view class="load-more-tip" v-if="discoverActivities.length > 0 && !hasMore && currentTab === 'discover'">
  156. <text class="load-more-text">没有更多了</text>
  157. </view>
  158. </view>
  159. <!-- Tab 3: history -->
  160. <view v-if="currentTab === 'history'">
  161. <!-- Filter chips (same as mine) -->
  162. <scroll-view class="filter-scroll" scroll-x enable-flex>
  163. <view class="filter-chips">
  164. <view
  165. class="filter-chip"
  166. :class="{ active: currentMineFilter === filter.value }"
  167. v-for="filter in mineFilters"
  168. :key="filter.value"
  169. @click="onMineFilterChange(filter.value)"
  170. >
  171. <text class="filter-label">{{ filter.label }}</text>
  172. </view>
  173. </view>
  174. </scroll-view>
  175. <!-- Activity cards -->
  176. <view class="card-list" v-if="historyActivities.length > 0">
  177. <view
  178. class="activity-card"
  179. v-for="act in historyActivities"
  180. :key="act.id"
  181. @click="goDetail(act)"
  182. >
  183. <image
  184. class="card-cover"
  185. :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
  186. mode="aspectFill"
  187. />
  188. <view class="card-info">
  189. <text class="card-title">{{ act.title }}</text>
  190. <text class="card-meta">{{ act.startTime }}</text>
  191. <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
  192. <view class="card-bottom">
  193. <text class="card-status status-ended">已结束</text>
  194. <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
  195. <text class="card-price free" v-else>免费</text>
  196. </view>
  197. </view>
  198. </view>
  199. </view>
  200. <!-- Empty state for history -->
  201. <view class="empty-state" v-else-if="!historyLoading">
  202. <text class="empty-icon">📜</text>
  203. <text class="empty-title">暂无历史活动</text>
  204. <text class="empty-desc">参加过的活动会显示在这里</text>
  205. </view>
  206. <!-- Loading state -->
  207. <view class="loading-state" v-if="historyLoading">
  208. <view class="loading-spinner"></view>
  209. <text class="loading-text">加载中...</text>
  210. </view>
  211. <!-- Load more tip -->
  212. <view class="load-more-tip" v-if="historyActivities.length > 0 && !hasMore && currentTab === 'history'">
  213. <text class="load-more-text">没有更多了</text>
  214. </view>
  215. </view>
  216. <!-- Tab 4: manage (admin only) -->
  217. <view v-if="currentTab === 'manage' && isAdmin">
  218. <!-- Manage overview stats -->
  219. <view class="manage-overview">
  220. <view class="manage-stat-card">
  221. <text class="manage-stat-num">{{ manageStats.total }}</text>
  222. <text class="manage-stat-label">全部活动</text>
  223. </view>
  224. <view class="manage-stat-card">
  225. <text class="manage-stat-num manage-published">{{ manageStats.published }}</text>
  226. <text class="manage-stat-label">已发布</text>
  227. </view>
  228. <view class="manage-stat-card">
  229. <text class="manage-stat-num manage-draft">{{ manageStats.draft }}</text>
  230. <text class="manage-stat-label">草稿</text>
  231. </view>
  232. <view class="manage-stat-card">
  233. <text class="manage-stat-num manage-ended">{{ manageStats.ended }}</text>
  234. <text class="manage-stat-label">已结束</text>
  235. </view>
  236. </view>
  237. <!-- Quick actions -->
  238. <view class="manage-actions">
  239. <view class="manage-action-btn" @click="goCreate">
  240. <text class="manage-action-icon">+</text>
  241. <text class="manage-action-text">创建活动</text>
  242. </view>
  243. </view>
  244. <!-- My managed activities list -->
  245. <view class="manage-section-title">
  246. <text>我的活动管理</text>
  247. </view>
  248. <view class="card-list" v-if="manageActivities.length > 0">
  249. <view
  250. class="activity-card"
  251. v-for="act in manageActivities"
  252. :key="act.id"
  253. @click="goEdit(act)"
  254. >
  255. <image
  256. class="card-cover"
  257. :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
  258. mode="aspectFill"
  259. />
  260. <view class="card-info">
  261. <text class="card-title">{{ act.title }}</text>
  262. <text class="card-meta">{{ act.startTime }}</text>
  263. <view class="card-bottom">
  264. <text class="card-status" :class="'status-' + (act.status || 'draft')">
  265. {{ statusLabel(act.status) }}
  266. </text>
  267. <view class="manage-action-group">
  268. <text class="manage-action-small" v-if="act.status === 'draft'" @click.stop="doPublish(act.id)">{{ isAdmin ? '发布' : '提交审核' }}</text>
  269. <text class="manage-action-small" v-if="act.status === 'rejected'" @click.stop="doPublish(act.id)">重新提交</text>
  270. <text class="manage-action-small" v-if="act.status === 'published'" @click.stop="doEnd(act.id)">结束</text>
  271. </view>
  272. </view>
  273. </view>
  274. </view>
  275. </view>
  276. <view class="empty-state" v-else-if="!manageLoading">
  277. <text class="empty-icon">📊</text>
  278. <text class="empty-title">暂无管理的活动</text>
  279. <text class="empty-desc">点击上方「创建活动」开始</text>
  280. </view>
  281. <view class="loading-state" v-if="manageLoading">
  282. <view class="loading-spinner"></view>
  283. <text class="loading-text">加载中...</text>
  284. </view>
  285. </view>
  286. </scroll-view>
  287. </view>
  288. </template>
  289. <script>
  290. import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration, adminListMyActivities, adminPublishActivity, submitActivityForReview } from '@/utils/api.js'
  291. import config from '@/config.js'
  292. import { parseDate } from '../../utils/format.js'
  293. export default {
  294. computed: {
  295. isAdmin: function() {
  296. var userInfo = uni.getStorageSync('userInfo')
  297. return userInfo && (userInfo.role === 'admin' || userInfo.role === 'activity_admin' || (userInfo.roles && userInfo.roles.indexOf('activity_admin') >= 0))
  298. }
  299. },
  300. data() {
  301. return {
  302. tabs: [
  303. { key: 'mine', label: '我的活动' },
  304. { key: 'discover', label: '发现活动' },
  305. { key: 'history', label: '历史记录' },
  306. { key: 'manage', label: '管理' }
  307. ],
  308. currentTab: 'mine',
  309. mineFilters: [
  310. { label: '全部', value: '' },
  311. { label: '待开始', value: 'upcoming' },
  312. { label: '待审核', value: 'pending' },
  313. { label: '已签到', value: 'checked_in' }
  314. ],
  315. dimensionFilters: [
  316. { label: '全部', value: '' },
  317. { label: '身', value: 'body' },
  318. { label: '心', value: 'mind' },
  319. { label: '智', value: 'wisdom' },
  320. { label: '行', value: 'action' },
  321. { label: '富', value: 'wealth' }
  322. ],
  323. currentMineFilter: '',
  324. currentDimension: '',
  325. myActivities: [],
  326. discoverActivities: [],
  327. historyActivities: [],
  328. mineLoading: false,
  329. discoverLoading: false,
  330. historyLoading: false,
  331. minePage: 1,
  332. discoverPage: 1,
  333. historyPage: 1,
  334. pageSize: 10,
  335. hasMore: true,
  336. isRefreshing: false,
  337. // Manage tab data
  338. manageLoading: false,
  339. manageStats: { total: 0, published: 0, draft: 0, ended: 0 },
  340. manageActivities: []
  341. }
  342. },
  343. onLoad: function(options) {
  344. if (options && options.tab) {
  345. this.currentTab = options.tab
  346. this.loadData()
  347. }
  348. },
  349. onShow: function() {
  350. if (this.currentTab === 'mine') {
  351. this.loadMyActivities(true)
  352. } else if (this.currentTab === 'manage') {
  353. this.loadManageData()
  354. }
  355. },
  356. methods: {
  357. getImageUrl: function(path) {
  358. if (!path) return ''
  359. if (path.indexOf('http://') === 0 || path.indexOf('https://') === 0) return path
  360. return config.API_BASE_URL + path
  361. },
  362. onTabChange: function(key) {
  363. this.currentTab = key
  364. this.hasMore = true
  365. if (key === 'mine') {
  366. this.minePage = 1
  367. this.myActivities = []
  368. } else if (key === 'discover') {
  369. this.discoverPage = 1
  370. this.discoverActivities = []
  371. } else if (key === 'history') {
  372. this.historyPage = 1
  373. this.historyActivities = []
  374. } else if (key === 'manage') {
  375. this.loadManageData()
  376. }
  377. this.loadData()
  378. },
  379. loadData: function() {
  380. if (this.currentTab === 'mine') {
  381. this.loadMyActivities()
  382. } else if (this.currentTab === 'discover') {
  383. this.loadDiscoverActivities()
  384. } else if (this.currentTab === 'history') {
  385. this.loadHistoryActivities()
  386. }
  387. },
  388. loadManageData: function() {
  389. var self = this
  390. this.manageLoading = true
  391. adminListMyActivities({ page: 1, size: 100 }).then(function(res) {
  392. self.manageLoading = false
  393. if (res && res.code === 200 && res.data) {
  394. var records = res.data.records || []
  395. self.manageActivities = records.slice(0, 20)
  396. var total = records.length
  397. var published = 0
  398. var draft = 0
  399. var ended = 0
  400. for (var i = 0; i < records.length; i++) {
  401. var s = records[i].status
  402. if (s === 'published') published++
  403. else if (s === 'draft') draft++
  404. else if (s === 'ended' || s === 'cancelled') ended++
  405. }
  406. self.manageStats = { total: total, published: published, draft: draft, ended: ended }
  407. } else {
  408. self.manageActivities = []
  409. self.manageStats = { total: 0, published: 0, draft: 0, ended: 0 }
  410. }
  411. }).catch(function() {
  412. self.manageLoading = false
  413. self.manageActivities = []
  414. self.manageStats = { total: 0, published: 0, draft: 0, ended: 0 }
  415. })
  416. },
  417. goCreate: function() {
  418. uni.navigateTo({ url: '/pages/activity/create' })
  419. },
  420. goEdit: function(act) {
  421. uni.navigateTo({ url: '/pages/activity/create?id=' + act.id })
  422. },
  423. statusLabel: function(status) {
  424. var map = {
  425. draft: '草稿',
  426. pending: '待审核',
  427. rejected: '已驳回',
  428. published: '进行中',
  429. ended: '已结束',
  430. cancelled: '已取消'
  431. }
  432. return map[status] || status
  433. },
  434. doPublish: function(id) {
  435. var self = this
  436. uni.showModal({
  437. title: '提示',
  438. content: '确认发布此活动吗?发布后将对外可见。',
  439. success: function(res) {
  440. if (res.confirm) {
  441. uni.showLoading({ title: '发布中...' })
  442. if (self.isAdmin) {
  443. adminPublishActivity(id).then(function(r) {
  444. uni.hideLoading()
  445. if (r && r.code === 200) {
  446. uni.showToast({ title: '发布成功', icon: 'success' })
  447. self.loadManageData()
  448. } else {
  449. uni.showToast({ title: (r && r.message) || '发布失败', icon: 'none' })
  450. }
  451. }).catch(function() {
  452. uni.hideLoading()
  453. uni.showToast({ title: '发布失败', icon: 'none' })
  454. })
  455. } else {
  456. submitActivityForReview(id).then(function(r) {
  457. uni.hideLoading()
  458. if (r && r.code === 200) {
  459. uni.showToast({ title: '已提交审核', icon: 'success' })
  460. self.loadManageData()
  461. } else {
  462. uni.showToast({ title: (r && r.message) || '提交审核失败', icon: 'none' })
  463. }
  464. }).catch(function() {
  465. uni.hideLoading()
  466. uni.showToast({ title: '提交审核失败', icon: 'none' })
  467. })
  468. }
  469. }
  470. }
  471. })
  472. },
  473. doEnd: function(id) {
  474. var self = this
  475. uni.showModal({
  476. title: '提示',
  477. content: '确认结束此活动吗?结束后不再接受新参与者。',
  478. success: function(res) {
  479. if (res.confirm) {
  480. uni.showLoading({ title: '操作中...' })
  481. setTimeout(function() {
  482. uni.hideLoading()
  483. uni.showToast({ title: '操作成功', icon: 'success' })
  484. self.loadManageData()
  485. }, 500)
  486. }
  487. }
  488. })
  489. },
  490. onMineFilterChange: function(value) {
  491. this.currentMineFilter = value
  492. this.minePage = 1
  493. this.myActivities = []
  494. this.hasMore = true
  495. if (this.currentTab === 'mine') {
  496. this.loadMyActivities()
  497. } else if (this.currentTab === 'history') {
  498. this.loadHistoryActivities()
  499. }
  500. },
  501. onDimensionChange: function(value) {
  502. this.currentDimension = value
  503. this.discoverPage = 1
  504. this.discoverActivities = []
  505. this.hasMore = true
  506. this.loadDiscoverActivities()
  507. },
  508. onLoadMore: function() {
  509. if (!this.hasMore) return
  510. if (this.currentTab === 'mine' && !this.mineLoading) {
  511. this.minePage++
  512. this.loadMyActivities()
  513. } else if (this.currentTab === 'discover' && !this.discoverLoading) {
  514. this.discoverPage++
  515. this.loadDiscoverActivities()
  516. } else if (this.currentTab === 'history' && !this.historyLoading) {
  517. this.historyPage++
  518. this.loadHistoryActivities()
  519. }
  520. },
  521. onRefresh: function() {
  522. var self = this
  523. this.isRefreshing = true
  524. if (this.currentTab === 'mine') {
  525. this.loadMyActivities(true).finally(function() {
  526. self.isRefreshing = false
  527. })
  528. } else if (this.currentTab === 'discover') {
  529. this.loadDiscoverActivities(true).finally(function() {
  530. self.isRefreshing = false
  531. })
  532. } else if (this.currentTab === 'history') {
  533. this.loadHistoryActivities(true).finally(function() {
  534. self.isRefreshing = false
  535. })
  536. } else {
  537. this.isRefreshing = false
  538. }
  539. },
  540. loadMyActivities: function(refresh) {
  541. var self = this
  542. if (refresh) {
  543. this.minePage = 1
  544. this.myActivities = []
  545. }
  546. this.mineLoading = true
  547. var memberId = uni.getStorageSync('currentChildId')
  548. if (!memberId) {
  549. this.mineLoading = false
  550. return
  551. }
  552. var params = {
  553. memberId: memberId,
  554. page: this.minePage,
  555. size: this.pageSize
  556. }
  557. if (this.currentMineFilter && this.currentMineFilter !== 'upcoming') {
  558. params.status = this.currentMineFilter
  559. }
  560. getMyActivityRegistrations(params).then(function(res) {
  561. self.mineLoading = false
  562. if (res && res.code === 200 && res.data) {
  563. var records = res.data.records || []
  564. if (self.currentMineFilter === 'upcoming') {
  565. var now = new Date()
  566. var filtered = []
  567. for (var i = 0; i < records.length; i++) {
  568. var act = records[i]
  569. if (act.startTime) {
  570. var startTime = parseDate(act.startTime)
  571. if (!startTime) continue
  572. if (startTime > now && act.status === 'approved') {
  573. filtered.push(act)
  574. }
  575. }
  576. }
  577. records = filtered
  578. }
  579. for (var di = 0; di < records.length; di++) {
  580. self.decorateActivity(records[di])
  581. }
  582. if (refresh) {
  583. self.myActivities = records
  584. } else {
  585. self.myActivities = self.myActivities.concat(records)
  586. }
  587. self.hasMore = records.length >= self.pageSize
  588. }
  589. }).catch(function() {
  590. self.mineLoading = false
  591. })
  592. },
  593. loadDiscoverActivities: function(refresh) {
  594. var self = this
  595. if (refresh) {
  596. this.discoverPage = 1
  597. this.discoverActivities = []
  598. }
  599. this.discoverLoading = true
  600. var params = {
  601. page: this.discoverPage,
  602. size: this.pageSize
  603. }
  604. if (this.currentDimension) {
  605. params.dimensionCode = this.currentDimension
  606. }
  607. getActivityList(params).then(function(res) {
  608. self.discoverLoading = false
  609. if (res && res.code === 200 && res.data) {
  610. var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
  611. if (refresh) {
  612. self.discoverActivities = list
  613. } else {
  614. self.discoverActivities = self.discoverActivities.concat(list)
  615. }
  616. self.hasMore = list.length >= self.pageSize
  617. }
  618. }).catch(function() {
  619. self.discoverLoading = false
  620. })
  621. },
  622. loadHistoryActivities: function(refresh) {
  623. var self = this
  624. if (refresh) {
  625. this.historyPage = 1
  626. this.historyActivities = []
  627. }
  628. this.historyLoading = true
  629. var memberId = uni.getStorageSync('currentChildId')
  630. if (!memberId) {
  631. this.historyLoading = false
  632. return
  633. }
  634. var params = {
  635. memberId: memberId,
  636. page: this.historyPage,
  637. size: this.pageSize
  638. }
  639. getMyActivityRegistrations(params).then(function(res) {
  640. self.historyLoading = false
  641. if (res && res.code === 200 && res.data) {
  642. var records = res.data.records || []
  643. var now = new Date()
  644. var ended = []
  645. for (var i = 0; i < records.length; i++) {
  646. var act = records[i]
  647. if (act.endTime) {
  648. var endTime = parseDate(act.endTime)
  649. if ((endTime && endTime < now) || act.status === 'cancelled' || act.status === 'rejected') {
  650. ended.push(act)
  651. }
  652. } else {
  653. ended.push(act)
  654. }
  655. }
  656. if (refresh) {
  657. self.historyActivities = ended
  658. } else {
  659. self.historyActivities = self.historyActivities.concat(ended)
  660. }
  661. self.hasMore = records.length >= self.pageSize
  662. }
  663. }).catch(function() {
  664. self.historyLoading = false
  665. })
  666. },
  667. goDetail: function(act) {
  668. uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + act.id })
  669. },
  670. switchToDiscover: function() {
  671. this.currentTab = 'discover'
  672. this.loadData()
  673. },
  674. formatPrice: function(price) {
  675. return (price / 100).toFixed(2)
  676. },
  677. hasFee: function(act) {
  678. return (act.childFee > 0) || (act.childMemberFee > 0) || (act.parentFee > 0) || (act.parentMemberFee > 0)
  679. },
  680. minFee: function(act) {
  681. var fees = [act.childFee, act.childMemberFee, act.parentFee, act.parentMemberFee]
  682. var min = 0
  683. for (var i = 0; i < fees.length; i++) {
  684. if (fees[i] > 0 && (min === 0 || fees[i] < min)) min = fees[i]
  685. }
  686. return this.formatPrice(min)
  687. },
  688. decorateActivity: function(act) {
  689. if (act.status) {
  690. act._cls = act.status
  691. var map = { pending: '待审核', approved: '报名成功', rejected: '已拒绝', cancelled: '已取消', checked_in: '已签到' }
  692. act._txt = map[act.status] || act.status
  693. } else {
  694. if (act.endTime) {
  695. var endTime = parseDate(act.endTime)
  696. if (endTime && endTime < new Date()) {
  697. act._cls = 'ended'
  698. act._txt = '已结束'
  699. return act
  700. }
  701. }
  702. act._cls = 'upcoming'
  703. act._txt = '待开始'
  704. }
  705. return act
  706. },
  707. onCheckin: function(act) {
  708. var self = this
  709. var memberId = uni.getStorageSync('currentChildId')
  710. if (!memberId) {
  711. uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
  712. return
  713. }
  714. activityCheckin({ id: act.activityId || act.id, memberId: memberId }).then(function(res) {
  715. if (res && res.code === 200) {
  716. uni.showToast({ title: '签到成功', icon: 'success' })
  717. self.loadMyActivities(true)
  718. } else {
  719. uni.showToast({ title: (res && res.message) || '签到失败', icon: 'none' })
  720. }
  721. }).catch(function() {
  722. uni.showToast({ title: '网络异常', icon: 'none' })
  723. })
  724. },
  725. onCancel: function(act) {
  726. var self = this
  727. uni.showModal({
  728. title: '提示',
  729. content: '确定要取消报名吗?',
  730. success: function(res) {
  731. if (res.confirm) {
  732. var memberId = uni.getStorageSync('currentChildId')
  733. if (!memberId) {
  734. uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
  735. return
  736. }
  737. cancelActivityRegistration({ id: act.activityId || act.id, memberId: memberId }).then(function(res) {
  738. if (res && res.code === 200) {
  739. uni.showToast({ title: '已取消报名', icon: 'success' })
  740. self.loadMyActivities(true)
  741. } else {
  742. uni.showToast({ title: (res && res.message) || '取消失败', icon: 'none' })
  743. }
  744. }).catch(function() {
  745. uni.showToast({ title: '网络异常', icon: 'none' })
  746. })
  747. }
  748. }
  749. })
  750. }
  751. }
  752. }
  753. </script>
  754. <style scoped>
  755. .activity-container {
  756. min-height: 100vh;
  757. background: #F5F5F5;
  758. display: flex;
  759. flex-direction: column;
  760. }
  761. .nav-bar {
  762. background: #1A0F00;
  763. padding: 20rpx 0;
  764. text-align: center;
  765. }
  766. .nav-title {
  767. color: #fff;
  768. font-size: 32rpx;
  769. font-weight: 600;
  770. }
  771. .tab-bar {
  772. background: #fff;
  773. display: flex;
  774. flex-direction: row;
  775. border-bottom: 1rpx solid #eee;
  776. }
  777. .tab-item {
  778. flex: 1;
  779. padding: 24rpx 0;
  780. text-align: center;
  781. border-bottom: 4rpx solid transparent;
  782. }
  783. .tab-item.active {
  784. border-bottom-color: #F97316;
  785. }
  786. .tab-label {
  787. font-size: 28rpx;
  788. color: #666;
  789. }
  790. .tab-item.active .tab-label {
  791. color: #F97316;
  792. font-weight: 600;
  793. }
  794. .content-scroll {
  795. flex: 1;
  796. height: calc(100vh - 200rpx);
  797. }
  798. .filter-scroll {
  799. padding: 20rpx 24rpx;
  800. background: #fff;
  801. }
  802. .filter-chips {
  803. display: flex;
  804. flex-direction: row;
  805. gap: 16rpx;
  806. }
  807. .filter-chip {
  808. padding: 12rpx 24rpx;
  809. border-radius: 16px;
  810. background: #fff;
  811. border: 1rpx solid #ddd;
  812. flex-shrink: 0;
  813. }
  814. .filter-chip.active {
  815. background: #FFF7ED;
  816. border-color: #F97316;
  817. }
  818. .filter-label {
  819. font-size: 24rpx;
  820. color: #666;
  821. }
  822. .filter-chip.active .filter-label {
  823. color: #F97316;
  824. font-weight: 500;
  825. }
  826. .card-list {
  827. padding: 0 24rpx 24rpx;
  828. display: flex;
  829. flex-direction: column;
  830. gap: 20rpx;
  831. }
  832. .activity-card {
  833. background: #fff;
  834. border-radius: 12rpx;
  835. padding: 20rpx;
  836. display: flex;
  837. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  838. }
  839. .card-cover {
  840. width: 144rpx;
  841. height: 144rpx;
  842. border-radius: 8rpx;
  843. flex-shrink: 0;
  844. background: #f0f0f0;
  845. }
  846. .card-info {
  847. flex: 1;
  848. margin-left: 20rpx;
  849. display: flex;
  850. flex-direction: column;
  851. }
  852. .card-title {
  853. font-size: 28rpx;
  854. font-weight: 600;
  855. color: #333;
  856. line-height: 1.4;
  857. overflow: hidden;
  858. text-overflow: ellipsis;
  859. white-space: nowrap;
  860. }
  861. .card-meta {
  862. font-size: 24rpx;
  863. color: #999;
  864. margin-top: 8rpx;
  865. }
  866. .card-bottom {
  867. display: flex;
  868. flex-direction: row;
  869. align-items: center;
  870. justify-content: space-between;
  871. margin-top: auto;
  872. padding-top: 12rpx;
  873. }
  874. .card-status {
  875. font-size: 22rpx;
  876. padding: 4rpx 12rpx;
  877. border-radius: 6rpx;
  878. }
  879. .status-pending {
  880. background: #FFF7ED;
  881. color: #F97316;
  882. }
  883. .status-approved {
  884. background: #F0FDF4;
  885. color: #22C55E;
  886. }
  887. .status-rejected {
  888. background: #FEF2F2;
  889. color: #EF4444;
  890. }
  891. .status-cancelled {
  892. background: #F5F5F5;
  893. color: #999;
  894. }
  895. .status-checked_in {
  896. background: #DBEAFE;
  897. color: #3B82F6;
  898. }
  899. .status-upcoming {
  900. background: #FFF7ED;
  901. color: #F97316;
  902. }
  903. .status-ended {
  904. background: #F5F5F5;
  905. color: #999;
  906. }
  907. .status-published {
  908. background: #F0FDF4;
  909. color: #22C55E;
  910. }
  911. .status-draft {
  912. background: #FFF7ED;
  913. color: #F97316;
  914. }
  915. .card-price {
  916. font-size: 28rpx;
  917. color: #F97316;
  918. font-weight: 600;
  919. }
  920. .card-price.free {
  921. color: #22C55E;
  922. }
  923. .card-actions {
  924. display: flex;
  925. flex-direction: row;
  926. gap: 12rpx;
  927. margin-top: 12rpx;
  928. }
  929. .btn-cancel {
  930. padding: 8rpx 20rpx;
  931. font-size: 22rpx;
  932. color: #999;
  933. background: #fff;
  934. border: 1rpx solid #ddd;
  935. border-radius: 20rpx;
  936. line-height: 1.5;
  937. }
  938. .btn-checkin {
  939. padding: 8rpx 20rpx;
  940. font-size: 22rpx;
  941. color: #fff;
  942. background: linear-gradient(135deg, #F97316, #FB923C);
  943. border: none;
  944. border-radius: 20rpx;
  945. line-height: 1.5;
  946. }
  947. .empty-state {
  948. display: flex;
  949. flex-direction: column;
  950. align-items: center;
  951. justify-content: center;
  952. padding: 120rpx 40rpx;
  953. }
  954. .empty-icon {
  955. font-size: 80rpx;
  956. margin-bottom: 20rpx;
  957. }
  958. .empty-title {
  959. font-size: 32rpx;
  960. font-weight: 600;
  961. color: #333;
  962. margin-bottom: 12rpx;
  963. }
  964. .empty-desc {
  965. font-size: 26rpx;
  966. color: #999;
  967. margin-bottom: 40rpx;
  968. }
  969. .empty-btn {
  970. padding: 16rpx 48rpx;
  971. font-size: 28rpx;
  972. color: #fff;
  973. background: linear-gradient(135deg, #F97316, #FB923C);
  974. border: none;
  975. border-radius: 40rpx;
  976. }
  977. .loading-state {
  978. display: flex;
  979. flex-direction: column;
  980. align-items: center;
  981. justify-content: center;
  982. padding: 80rpx 40rpx;
  983. }
  984. .loading-spinner {
  985. width: 60rpx;
  986. height: 60rpx;
  987. border: 4rpx solid #FDE68A;
  988. border-top-color: #F97316;
  989. border-radius: 50%;
  990. animation: spin 0.8s linear infinite;
  991. margin-bottom: 20rpx;
  992. }
  993. @keyframes spin {
  994. to { transform: rotate(360deg); }
  995. }
  996. .loading-text {
  997. font-size: 26rpx;
  998. color: #999;
  999. }
  1000. .load-more-tip {
  1001. text-align: center;
  1002. padding: 30rpx;
  1003. }
  1004. .load-more-text {
  1005. font-size: 24rpx;
  1006. color: #999;
  1007. }
  1008. /* Nav add button */
  1009. .nav-add-btn {
  1010. position: absolute;
  1011. right: 24rpx;
  1012. top: 50%;
  1013. transform: translateY(-50%);
  1014. width: 56rpx;
  1015. height: 56rpx;
  1016. background: rgba(255, 255, 255, 0.15);
  1017. border-radius: 50%;
  1018. display: flex;
  1019. align-items: center;
  1020. justify-content: center;
  1021. }
  1022. .nav-add-icon {
  1023. color: #fff;
  1024. font-size: 36rpx;
  1025. font-weight: 300;
  1026. line-height: 1;
  1027. }
  1028. /* Manage overview stats */
  1029. .manage-overview {
  1030. display: flex;
  1031. flex-direction: row;
  1032. padding: 24rpx;
  1033. gap: 16rpx;
  1034. background: #fff;
  1035. margin: 24rpx 24rpx 0;
  1036. border-radius: 12rpx;
  1037. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  1038. }
  1039. .manage-stat-card {
  1040. flex: 1;
  1041. display: flex;
  1042. flex-direction: column;
  1043. align-items: center;
  1044. padding: 16rpx 0;
  1045. }
  1046. .manage-stat-num {
  1047. font-size: 40rpx;
  1048. font-weight: 700;
  1049. color: #333;
  1050. line-height: 1.2;
  1051. }
  1052. .manage-published {
  1053. color: #22C55E;
  1054. }
  1055. .manage-draft {
  1056. color: #F97316;
  1057. }
  1058. .manage-ended {
  1059. color: #999;
  1060. }
  1061. .manage-stat-label {
  1062. font-size: 22rpx;
  1063. color: #999;
  1064. margin-top: 8rpx;
  1065. }
  1066. /* Quick action button */
  1067. .manage-actions {
  1068. padding: 24rpx 24rpx 0;
  1069. }
  1070. .manage-action-btn {
  1071. display: flex;
  1072. flex-direction: row;
  1073. align-items: center;
  1074. justify-content: center;
  1075. gap: 12rpx;
  1076. padding: 24rpx 0;
  1077. background: linear-gradient(135deg, #F97316, #FB923C);
  1078. border-radius: 12rpx;
  1079. margin: 0 24rpx;
  1080. }
  1081. .manage-action-icon {
  1082. color: #fff;
  1083. font-size: 36rpx;
  1084. font-weight: 300;
  1085. }
  1086. .manage-action-text {
  1087. color: #fff;
  1088. font-size: 28rpx;
  1089. font-weight: 600;
  1090. }
  1091. /* Section title */
  1092. .manage-section-title {
  1093. padding: 24rpx 24rpx 12rpx;
  1094. }
  1095. .manage-section-title text {
  1096. font-size: 28rpx;
  1097. font-weight: 600;
  1098. color: #333;
  1099. }
  1100. /* Manage action group in card */
  1101. .manage-action-group {
  1102. display: flex;
  1103. flex-direction: row;
  1104. gap: 16rpx;
  1105. align-items: center;
  1106. }
  1107. .manage-action-small {
  1108. font-size: 22rpx;
  1109. padding: 4rpx 16rpx;
  1110. border-radius: 20rpx;
  1111. background: #F97316;
  1112. color: #fff;
  1113. font-weight: 500;
  1114. }
  1115. </style>