DimensionActivities.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <template>
  2. <view class="section">
  3. <!-- 头部 -->
  4. <view class="section-header">
  5. <view class="section-header-left">
  6. <view class="section-icon-wrap">
  7. <text class="section-icon">{{ dimensionIcon }}</text>
  8. </view>
  9. <text class="section-title">推荐活动</text>
  10. </view>
  11. <view class="section-more" @click="$emit('moreActivities')">
  12. <text class="section-more-text">更多</text>
  13. <text class="section-more-arrow">&#x203A;</text>
  14. </view>
  15. </view>
  16. <!-- 空状态 -->
  17. <view v-if="!displayActivities || displayActivities.length === 0" class="empty-state">
  18. <text class="empty-icon">{{ dimensionIcon }}</text>
  19. <text class="empty-tip">{{ isLoggedIn ? '暂无相关活动' : '登录后查看更多活动' }}</text>
  20. </view>
  21. <!-- 活动卡片列表 -->
  22. <view class="activity-list" v-if="displayActivities && displayActivities.length > 0">
  23. <view
  24. class="activity-card"
  25. v-for="(act, index) in displayActivities"
  26. :key="act.id"
  27. @click.stop="$emit('activityClick', act)"
  28. >
  29. <!-- 封面图区域 -->
  30. <view class="card-cover">
  31. <image
  32. class="card-cover-img"
  33. :src="act.coverImage || '/static/default-activity.png'"
  34. mode="aspectFill"
  35. />
  36. <!-- 渐变遮罩 -->
  37. <view class="card-cover-mask"></view>
  38. <!-- 序号标签 -->
  39. <view class="card-rank" v-if="displayActivities.length > 1">
  40. <text class="card-rank-num">{{ index + 1 }}</text>
  41. </view>
  42. <!-- 状态标签 -->
  43. <view class="card-status" :class="'status-' + (act.status || 'upcoming')">
  44. <text class="card-status-dot" v-if="act.status === 'ongoing'"></text>
  45. <text class="card-status-text">{{ statusText(act.status) }}</text>
  46. </view>
  47. <!-- 标题 -->
  48. <view class="card-title-wrap">
  49. <text class="card-title">{{ act.title }}</text>
  50. </view>
  51. </view>
  52. <!-- 内容区域 -->
  53. <view class="card-content">
  54. <!-- 时间和维度标签 -->
  55. <view class="card-meta-row">
  56. <view class="card-time">
  57. <text class="meta-icon">&#x1F552;</text>
  58. <text class="meta-text">{{ formatTime(act.startTime) }}</text>
  59. </view>
  60. <view class="card-badges" v-if="act._badges && act._badges.length">
  61. <text
  62. class="card-badge"
  63. v-for="badge in act._badges"
  64. :key="badge.name"
  65. :style="{ color: badge.color, borderColor: badge.color + '40' }"
  66. >
  67. {{ badge.name }}
  68. </text>
  69. </view>
  70. </view>
  71. <!-- 底部:价格和按钮 -->
  72. <view class="card-footer">
  73. <view class="card-price-wrap">
  74. <text class="card-price" v-if="act.priceLabel">{{ act.priceLabel }}</text>
  75. <text class="card-price" v-else-if="act.price && act.price > 0">{{ formatPriceWithSymbol(act.price) }}</text>
  76. <text class="card-price-free" v-else>免费</text>
  77. </view>
  78. <view class="card-action-btn">
  79. <text class="card-action-text">了解详情</text>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </view>
  85. </view>
  86. </template>
  87. <script>
  88. var DIMENSION_MAP = {
  89. body: { name: '身', color: '#FF8C42', icon: '🌏' },
  90. mind: { name: '心', color: '#FF6B9D', icon: '🔥' },
  91. wisdom: { name: '智', color: '#6366F1', icon: '⚡' },
  92. action: { name: '行', color: '#10B981', icon: '🌿' },
  93. wealth: { name: '富', color: '#F59E0B', icon: '💧' }
  94. }
  95. export default {
  96. props: {
  97. dimensionCode: { type: String, required: true },
  98. activities: { type: Array, default: function() { return [] } },
  99. isLoggedIn: { type: Boolean, default: false }
  100. },
  101. data: function() {
  102. return {}
  103. },
  104. computed: {
  105. displayActivities: function() {
  106. var self = this
  107. return (this.activities || []).slice(0, 3).map(function(item) {
  108. item._badges = self.parseWeights(item.dimensionWeights)
  109. return item
  110. })
  111. },
  112. dimensionIcon: function() {
  113. var map = { body: '🌏', mind: '🔥', wisdom: '⚡', action: '🌿', wealth: '💧' }
  114. return map[this.dimensionCode] || '🎯'
  115. }
  116. },
  117. methods: {
  118. parseWeights: function(str) {
  119. if (!str) return []
  120. try {
  121. var obj = JSON.parse(str)
  122. var result = []
  123. for (var key in obj) {
  124. if (obj[key] > 0 && DIMENSION_MAP[key]) {
  125. result.push({ name: DIMENSION_MAP[key].name, color: DIMENSION_MAP[key].color, weight: obj[key] })
  126. }
  127. }
  128. return result
  129. } catch (e) {
  130. return []
  131. }
  132. },
  133. statusText: function(status) {
  134. var map = { upcoming: '即将开始', ongoing: '进行中', ended: '已结束', cancelled: '已取消' }
  135. return map[status] || '即将开始'
  136. },
  137. formatTime: function(timeStr) {
  138. if (!timeStr) return '待定'
  139. var d = new Date(timeStr)
  140. if (isNaN(d.getTime())) return timeStr
  141. var month = d.getMonth() + 1
  142. var day = d.getDate()
  143. var hour = d.getHours()
  144. var minute = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()
  145. return month + '月' + day + '日 ' + hour + ':' + minute
  146. },
  147. formatPriceWithSymbol: function(price) {
  148. if (price == null) return '免费'
  149. return '¥' + (Number(price) / 100).toFixed(0)
  150. }
  151. }
  152. }
  153. </script>
  154. <style scoped>
  155. .section {
  156. margin: 24rpx 20rpx;
  157. }
  158. /* ===== 头部 ===== */
  159. .section-header {
  160. display: flex;
  161. flex-direction: row;
  162. justify-content: space-between;
  163. align-items: center;
  164. margin-bottom: 24rpx;
  165. }
  166. .section-header-left {
  167. display: flex;
  168. flex-direction: row;
  169. align-items: center;
  170. }
  171. .section-icon-wrap {
  172. width: 44rpx;
  173. height: 44rpx;
  174. border-radius: 12rpx;
  175. background: linear-gradient(135deg, #FF6B9D, #FF8FB1);
  176. display: flex;
  177. align-items: center;
  178. justify-content: center;
  179. margin-right: 12rpx;
  180. }
  181. .section-icon {
  182. font-size: 24rpx;
  183. }
  184. .section-title {
  185. font-size: 32rpx;
  186. font-weight: 700;
  187. color: #1E293B;
  188. letter-spacing: 1rpx;
  189. }
  190. .section-more {
  191. display: flex;
  192. flex-direction: row;
  193. align-items: center;
  194. padding: 8rpx 20rpx;
  195. background: #F1F5F9;
  196. border-radius: 24rpx;
  197. }
  198. .section-more-text {
  199. font-size: 24rpx;
  200. color: #64748B;
  201. margin-right: 4rpx;
  202. }
  203. .section-more-arrow {
  204. font-size: 20rpx;
  205. color: #94A3B8;
  206. }
  207. /* ===== 空状态 ===== */
  208. .empty-state {
  209. display: flex;
  210. flex-direction: column;
  211. align-items: center;
  212. padding: 60rpx 0;
  213. }
  214. .empty-icon {
  215. font-size: 64rpx;
  216. margin-bottom: 16rpx;
  217. }
  218. .empty-tip {
  219. font-size: 26rpx;
  220. color: #94A3B8;
  221. }
  222. /* ===== 活动列表 ===== */
  223. .activity-list {
  224. display: flex;
  225. flex-direction: column;
  226. gap: 24rpx;
  227. }
  228. /* ===== 活动卡片 ===== */
  229. .activity-card {
  230. background: #FFFFFF;
  231. border-radius: 24rpx;
  232. overflow: hidden;
  233. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06), 0 1rpx 3rpx rgba(0, 0, 0, 0.04);
  234. transition: transform 0.2s ease;
  235. }
  236. .activity-card:active {
  237. transform: scale(0.98);
  238. }
  239. /* 封面图 */
  240. .card-cover {
  241. position: relative;
  242. width: 100%;
  243. height: 320rpx;
  244. overflow: hidden;
  245. }
  246. .card-cover-img {
  247. width: 100%;
  248. height: 100%;
  249. background: #F1F5F9;
  250. }
  251. .card-cover-mask {
  252. position: absolute;
  253. bottom: 0;
  254. left: 0;
  255. right: 0;
  256. height: 200rpx;
  257. background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
  258. }
  259. .card-rank {
  260. position: absolute;
  261. top: 16rpx;
  262. left: 16rpx;
  263. width: 48rpx;
  264. height: 48rpx;
  265. border-radius: 14rpx;
  266. background: rgba(0, 0, 0, 0.5);
  267. backdrop-filter: blur(10rpx);
  268. display: flex;
  269. align-items: center;
  270. justify-content: center;
  271. }
  272. .card-rank-num {
  273. font-size: 24rpx;
  274. font-weight: 700;
  275. color: #FFFFFF;
  276. }
  277. .card-status {
  278. position: absolute;
  279. top: 16rpx;
  280. right: 16rpx;
  281. display: flex;
  282. flex-direction: row;
  283. align-items: center;
  284. padding: 6rpx 16rpx;
  285. border-radius: 20rpx;
  286. background: rgba(0, 0, 0, 0.4);
  287. backdrop-filter: blur(10rpx);
  288. }
  289. .card-status-dot {
  290. width: 8rpx;
  291. height: 8rpx;
  292. border-radius: 50%;
  293. background: #22C55E;
  294. margin-right: 6rpx;
  295. animation: pulse 2s infinite;
  296. }
  297. @keyframes pulse {
  298. 0%, 100% { opacity: 1; }
  299. 50% { opacity: 0.4; }
  300. }
  301. .card-status-text {
  302. font-size: 22rpx;
  303. color: #FFFFFF;
  304. font-weight: 500;
  305. }
  306. .card-title-wrap {
  307. position: absolute;
  308. bottom: 16rpx;
  309. left: 20rpx;
  310. right: 20rpx;
  311. }
  312. .card-title {
  313. font-size: 32rpx;
  314. font-weight: 700;
  315. color: #FFFFFF;
  316. text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
  317. display: -webkit-box;
  318. -webkit-line-clamp: 2;
  319. -webkit-box-orient: vertical;
  320. overflow: hidden;
  321. }
  322. /* 内容区域 */
  323. .card-content {
  324. padding: 20rpx;
  325. }
  326. .card-meta-row {
  327. display: flex;
  328. flex-direction: row;
  329. align-items: center;
  330. margin-bottom: 16rpx;
  331. }
  332. .card-time {
  333. display: flex;
  334. flex-direction: row;
  335. align-items: center;
  336. margin-right: 16rpx;
  337. }
  338. .meta-icon {
  339. font-size: 22rpx;
  340. margin-right: 6rpx;
  341. }
  342. .meta-text {
  343. font-size: 24rpx;
  344. color: #64748B;
  345. }
  346. .card-badges {
  347. display: flex;
  348. flex-direction: row;
  349. flex-wrap: wrap;
  350. gap: 8rpx;
  351. }
  352. .card-badge {
  353. font-size: 20rpx;
  354. padding: 4rpx 12rpx;
  355. border-radius: 20rpx;
  356. border: 1rpx solid;
  357. font-weight: 500;
  358. }
  359. /* 底部 */
  360. .card-footer {
  361. display: flex;
  362. flex-direction: row;
  363. justify-content: space-between;
  364. align-items: center;
  365. padding-top: 16rpx;
  366. border-top: 1rpx solid #F1F5F9;
  367. }
  368. .card-price-wrap {
  369. display: flex;
  370. flex-direction: row;
  371. align-items: baseline;
  372. }
  373. .card-price {
  374. font-size: 30rpx;
  375. font-weight: 700;
  376. color: #FF6B9D;
  377. }
  378. .card-price-free {
  379. font-size: 28rpx;
  380. font-weight: 600;
  381. color: #22C55E;
  382. }
  383. .card-action-btn {
  384. padding: 10rpx 24rpx;
  385. background: linear-gradient(135deg, #FF6B9D, #FF8FB1);
  386. border-radius: 24rpx;
  387. }
  388. .card-action-text {
  389. font-size: 22rpx;
  390. color: #FFFFFF;
  391. font-weight: 600;
  392. }
  393. .card-action-btn:active {
  394. opacity: 0.8;
  395. }
  396. </style>