index.vue 31 KB

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