index.vue 41 KB

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