index.vue 35 KB

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