post-content-block.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <template>
  2. <view class="pcb">
  3. <!-- 上新 -->
  4. <view v-if="post.type === 'new'" class="pcb-new">
  5. <view class="pcb-date-badge">
  6. <text class="pcb-date-day">{{ post.createtime_date || '—' }}</text>
  7. </view>
  8. <text class="pcb-new-label">今日上新</text>
  9. </view>
  10. <!-- 图文九宫格(列表 + 详情) -->
  11. <post-image-grid
  12. v-if="showGrid"
  13. :images="post.images"
  14. :max="gridMax"
  15. @tap-image="onGridTap"
  16. />
  17. <!-- 视频缩略 -->
  18. <view v-if="post.type === 'video'" class="pcb-video" @tap="$emit('open-video', post)">
  19. <image class="pcb-video-cover" :src="videoCover" mode="aspectFill" />
  20. <view class="pcb-video-play">
  21. <text class="pcb-video-play-icon">▶</text>
  22. </view>
  23. </view>
  24. <!-- 关联商品 -->
  25. <scroll-view
  26. v-if="post.goods && post.goods.length"
  27. class="pcb-goods-scroll"
  28. scroll-x
  29. :show-scrollbar="false"
  30. >
  31. <view
  32. v-for="goods in post.goods"
  33. :key="goods.id"
  34. class="pcb-goods-item"
  35. :class="{ 'pcb-goods-item--solo': post.goods.length === 1 }"
  36. @tap="openGoods(goods.id)"
  37. >
  38. <image class="pcb-goods-img" :src="oss(goods.image, 100, 100)" mode="aspectFill" />
  39. <view class="pcb-goods-meta">
  40. <text class="pcb-goods-title">{{ goods.title }}</text>
  41. <text class="pcb-goods-price">¥{{ goods.price }}</text>
  42. </view>
  43. </view>
  44. </scroll-view>
  45. <!-- 正文 -->
  46. <view v-if="htmlContent" class="pcb-body">
  47. <rich-text v-if="useRichText" :nodes="htmlContent" />
  48. <text v-else class="pcb-body-text">{{ plainContent }}</text>
  49. </view>
  50. </view>
  51. </template>
  52. <script>
  53. import PostImageGrid from '@/components/dynamics/post-image-grid.vue'
  54. import { stripHtml } from '@/features/feed/clip-normalize'
  55. import { absolutizePreviewUrls, GRID_MAX } from '@/features/dynamics/post-media'
  56. export default {
  57. name: 'PostContentBlock',
  58. components: { PostImageGrid },
  59. props: {
  60. post: { type: Object, default: () => ({}) },
  61. isMain: { type: Boolean, default: false }
  62. },
  63. computed: {
  64. hasImages() {
  65. return Array.isArray(this.post.images) && this.post.images.length > 0
  66. },
  67. showGrid() {
  68. return this.post.type !== 'video' && this.hasImages
  69. },
  70. gridMax() {
  71. return GRID_MAX
  72. },
  73. videoCover() {
  74. const cover = (this.post.video && this.post.video.cover_url)
  75. || (this.post.images && this.post.images[0])
  76. || ''
  77. return this.oss(cover, 750, 0)
  78. },
  79. htmlContent() {
  80. return this.post.content || ''
  81. },
  82. plainContent() {
  83. return stripHtml(this.post.content)
  84. },
  85. useRichText() {
  86. return /<[^>]+>/.test(String(this.post.content || ''))
  87. }
  88. },
  89. methods: {
  90. oss(url, w = 400, h = 0) {
  91. if (this.$appKit && typeof this.$appKit.oss === 'function') {
  92. return this.$appKit.oss(url, w, h)
  93. }
  94. return url || ''
  95. },
  96. onGridTap(index) {
  97. if (this.isMain) {
  98. this.previewImages(index)
  99. return
  100. }
  101. this.$emit('open-detail', { id: this.post.id, img: index })
  102. },
  103. previewImages(index) {
  104. const urls = absolutizePreviewUrls(this.post.images, (u) => this.oss(u, 750, 0))
  105. if (!urls.length) return
  106. uni.previewImage({
  107. current: urls[index] || urls[0],
  108. urls
  109. })
  110. },
  111. openGoods(id) {
  112. if (typeof this.$appKit.onGoods === 'function') {
  113. this.$appKit.onGoods(id)
  114. return
  115. }
  116. uni.navigateTo({ url: `/pages/product/goods?id=${id}` })
  117. }
  118. }
  119. }
  120. </script>
  121. <style scoped>
  122. .pcb {
  123. width: 100%;
  124. }
  125. .pcb-new {
  126. display: flex;
  127. flex-direction: row;
  128. align-items: center;
  129. margin-bottom: 20rpx;
  130. }
  131. .pcb-date-badge {
  132. width: 72rpx;
  133. height: 72rpx;
  134. border-radius: 18rpx;
  135. background: linear-gradient(135deg, #4a8cff 0%, #6ec8ff 100%);
  136. display: flex;
  137. align-items: center;
  138. justify-content: center;
  139. margin-right: 16rpx;
  140. }
  141. .pcb-date-day {
  142. color: #ffffff;
  143. font-size: 22rpx;
  144. font-weight: 700;
  145. text-align: center;
  146. line-height: 1.2;
  147. }
  148. .pcb-new-label {
  149. font-size: 30rpx;
  150. font-weight: 700;
  151. color: #1a1d26;
  152. }
  153. .pcb-video {
  154. position: relative;
  155. width: 62%;
  156. height: 520rpx;
  157. border-radius: 20rpx;
  158. overflow: hidden;
  159. background-color: #1a1d26;
  160. }
  161. .pcb-video-cover {
  162. width: 100%;
  163. height: 100%;
  164. }
  165. .pcb-video-play {
  166. position: absolute;
  167. left: 50%;
  168. top: 50%;
  169. transform: translate(-50%, -50%);
  170. width: 88rpx;
  171. height: 88rpx;
  172. border-radius: 44rpx;
  173. background-color: rgba(255, 255, 255, 0.25);
  174. display: flex;
  175. align-items: center;
  176. justify-content: center;
  177. }
  178. .pcb-video-play-icon {
  179. color: #ffffff;
  180. font-size: 32rpx;
  181. margin-left: 6rpx;
  182. }
  183. .pcb-goods-scroll {
  184. width: 100%;
  185. white-space: nowrap;
  186. margin-top: 24rpx;
  187. }
  188. .pcb-goods-item {
  189. display: inline-flex;
  190. flex-direction: row;
  191. align-items: center;
  192. width: 420rpx;
  193. margin-right: 16rpx;
  194. padding: 12rpx;
  195. border-radius: 16rpx;
  196. background-color: #f5f7fb;
  197. vertical-align: top;
  198. }
  199. .pcb-goods-item--solo {
  200. width: 100%;
  201. margin-right: 0;
  202. }
  203. .pcb-goods-img {
  204. width: 96rpx;
  205. height: 96rpx;
  206. border-radius: 12rpx;
  207. background-color: #eef2f8;
  208. flex-shrink: 0;
  209. }
  210. .pcb-goods-meta {
  211. flex: 1;
  212. min-width: 0;
  213. margin-left: 16rpx;
  214. }
  215. .pcb-goods-title {
  216. font-size: 26rpx;
  217. color: #1a1d26;
  218. display: block;
  219. overflow: hidden;
  220. text-overflow: ellipsis;
  221. white-space: nowrap;
  222. }
  223. .pcb-goods-price {
  224. font-size: 26rpx;
  225. color: #ff6b35;
  226. font-weight: 700;
  227. margin-top: 8rpx;
  228. }
  229. .pcb-body {
  230. margin-top: 24rpx;
  231. }
  232. .pcb-body-text {
  233. font-size: 28rpx;
  234. line-height: 44rpx;
  235. color: #3d4250;
  236. }
  237. </style>