瑶都万能墙
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

556 lines
12 KiB

  1. <template>
  2. <view class="comment-detail-container">
  3. <navbar :title="pageTitle" leftClick @leftClick="navigateBack"/>
  4. <!-- 加载状态 -->
  5. <view class="loading-container" v-if="loading">
  6. <view class="loading-spinner"></view>
  7. <text class="loading-text">加载中...</text>
  8. </view>
  9. <!-- 错误状态 -->
  10. <view class="error-container" v-if="!loading && hasError">
  11. <text class="error-text">加载失败请重试</text>
  12. <button class="retry-button" @click="loadCommentDetail">重新加载</button>
  13. </view>
  14. <!-- 主评论内容 -->
  15. <view class="main-comment" v-if="!loading && !hasError && mainComment">
  16. <view class="comment-header">
  17. <image class="avatar" :src="mainComment.userHead || '/static/image/center/default-avatar.png'" mode="aspectFill"></image>
  18. <view class="user-info">
  19. <text class="username">{{mainComment.userName}}</text>
  20. <text class="time">{{formatTime(mainComment.createTime)}}</text>
  21. </view>
  22. </view>
  23. <view class="comment-content">
  24. <text>{{mainComment.userValue}}</text>
  25. </view>
  26. <!-- 评论图片 -->
  27. <view class="comment-images" v-if="mainComment.userImage">
  28. <view class="image-grid">
  29. <image
  30. v-for="(img, index) in mainComment.userImage.split(',')"
  31. :key="index"
  32. :src="img"
  33. mode="aspectFill"
  34. @click="previewImage(mainComment.userImage.split(','), index)"
  35. class="comment-image"
  36. ></image>
  37. </view>
  38. </view>
  39. <view class="comment-footer">
  40. <text class="reply-count">{{totalReplies}}条回复</text>
  41. <view class="reply-btn" @click="showReplyInput(mainComment.id, mainComment.userName)">
  42. <uv-icon name="chat" color="#666" size="32rpx"></uv-icon>
  43. <text>回复</text>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 回复列表 -->
  48. <view class="replies-container" v-if="!loading && !hasError && commentList.length > 0">
  49. <view class="reply-item" v-for="(item, index) in commentList" :key="index">
  50. <view class="reply-header">
  51. <image class="avatar" :src="item.userHead || '/static/image/center/default-avatar.png'" mode="aspectFill"></image>
  52. <view class="user-info">
  53. <text class="username">{{item.userName}}</text>
  54. <text class="time">{{formatTime(item.createTime)}}</text>
  55. </view>
  56. </view>
  57. <view class="reply-content">
  58. <view v-if="item.replyToUserName" class="reply-to">
  59. <text>回复 </text>
  60. <text class="reply-username">@{{item.replyToUserName}}</text>
  61. <text></text>
  62. </view>
  63. <text>{{item.userValue}}</text>
  64. </view>
  65. <!-- 回复图片 -->
  66. <view class="reply-images" v-if="item.userImage">
  67. <view class="image-grid">
  68. <image
  69. v-for="(img, imgIndex) in item.userImage.split(',')"
  70. :key="imgIndex"
  71. :src="img"
  72. mode="aspectFill"
  73. @click="previewImage(item.userImage.split(','), imgIndex)"
  74. class="reply-image"
  75. ></image>
  76. </view>
  77. </view>
  78. <view class="reply-footer">
  79. <view class="reply-info">
  80. <text v-if="item.replyNum > 0" class="sub-reply-count" @click="navigateToSubComment(item)">
  81. {{item.replyNum}}条回复 >
  82. </text>
  83. </view>
  84. <view class="reply-btn" @click="showReplyInput(item.id, item.userName)">
  85. <uv-icon name="chat" color="#666" size="28rpx"></uv-icon>
  86. <text>回复</text>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. <!-- 加载更多 -->
  92. <view class="load-more" v-if="!loading && !hasError && hasMore">
  93. <text @click="loadMoreReplies">加载更多</text>
  94. </view>
  95. <!-- 评论发布组件 -->
  96. <commentPublish ref="commentPublish" :params="commentParams" :placeholder="replyPlaceholder" @success="handleCommentSuccess" />
  97. </view>
  98. </template>
  99. <script>
  100. import commentPublish from '../components/list/comment/commentPublish.vue'
  101. export default {
  102. components: {
  103. commentPublish
  104. },
  105. data() {
  106. return {
  107. commentId: '', // 当前评论ID
  108. parentId: '', // 父评论ID,用于无限层级
  109. sourceType: '', // 评论来源类型(文章、帖子等)
  110. sourceId: '', // 评论来源ID
  111. pageTitle: '评论详情',
  112. loading: true,
  113. hasError: false,
  114. mainComment: null, // 主评论
  115. commentList: [], // 回复列表
  116. page: 1,
  117. pageSize: 20,
  118. hasMore: false,
  119. totalReplies: 0,
  120. // 回复相关
  121. replyToId: '', // 回复的评论ID
  122. replyToUserName: '', // 回复的用户名
  123. replyPlaceholder: '写回复...'
  124. }
  125. },
  126. computed: {
  127. // 评论参数
  128. commentParams() {
  129. return {
  130. type: this.sourceType,
  131. orderId: this.sourceId,
  132. pid: this.commentId,
  133. replyToId: this.replyToId,
  134. replyToUserName: this.replyToUserName
  135. }
  136. }
  137. },
  138. onLoad(options) {
  139. // 获取参数
  140. if (options.id) {
  141. this.commentId = options.id;
  142. }
  143. if (options.parentId) {
  144. this.parentId = options.parentId;
  145. }
  146. if (options.sourceType) {
  147. this.sourceType = options.sourceType;
  148. }
  149. if (options.sourceId) {
  150. this.sourceId = options.sourceId;
  151. }
  152. // 加载评论详情
  153. this.loadCommentDetail();
  154. // 加载回复列表
  155. this.loadReplies();
  156. },
  157. methods: {
  158. // 加载评论详情
  159. loadCommentDetail() {
  160. this.loading = true;
  161. this.hasError = false;
  162. // 调用API获取评论详情
  163. this.$api('getCommentDetail', {
  164. id: this.commentId
  165. }, res => {
  166. this.loading = false;
  167. if (res.code === 200) {
  168. this.mainComment = res.result;
  169. // 获取回复总数
  170. this.totalReplies = res.result.replyNum || 0;
  171. } else {
  172. this.hasError = true;
  173. uni.showToast({
  174. title: res.message || '加载失败',
  175. icon: 'none'
  176. });
  177. }
  178. });
  179. },
  180. // 加载回复列表
  181. loadReplies() {
  182. // 使用现有的评论列表接口,传入pid参数
  183. this.$api('getCommentPage', {
  184. pid: this.commentId,
  185. page: this.page,
  186. pageSize: this.pageSize
  187. }, res => {
  188. if (res.code === 200) {
  189. if (this.page === 1) {
  190. this.commentList = res.result.records || [];
  191. } else {
  192. this.commentList = [...this.commentList, ...(res.result.records || [])];
  193. }
  194. // 判断是否有更多数据
  195. this.hasMore = this.commentList.length < res.result.total;
  196. } else {
  197. uni.showToast({
  198. title: res.message || '加载回复失败',
  199. icon: 'none'
  200. });
  201. }
  202. });
  203. },
  204. // 加载更多回复
  205. loadMoreReplies() {
  206. this.page++;
  207. this.loadReplies();
  208. },
  209. // 显示回复输入框并直接打开弹窗
  210. showReplyInput(commentId, userName = '') {
  211. this.replyToId = commentId;
  212. this.replyToUserName = userName;
  213. this.replyPlaceholder = userName ? `回复 @${userName}` : '写回复...';
  214. // 直接打开评论发布弹窗
  215. this.$refs.commentPublish.open();
  216. },
  217. // 评论发布成功回调
  218. handleCommentSuccess() {
  219. // 重新加载回复列表
  220. this.page = 1;
  221. this.loadReplies();
  222. // 更新回复总数
  223. this.totalReplies++;
  224. },
  225. // 导航到子评论详情
  226. navigateToSubComment(comment) {
  227. if (comment.replyNum > 0) {
  228. uni.navigateTo({
  229. url: `/pages_order/comment/commentDetail?id=${comment.id}&parentId=${this.commentId}&sourceType=${this.sourceType}&sourceId=${this.sourceId}`
  230. });
  231. }
  232. },
  233. // 返回上一页
  234. navigateBack() {
  235. uni.navigateBack();
  236. },
  237. // 预览图片
  238. previewImage(images, index) {
  239. uni.previewImage({
  240. urls: images,
  241. current: index
  242. });
  243. },
  244. // 格式化时间
  245. formatTime(timestamp) {
  246. if (!timestamp) return '';
  247. const now = new Date().getTime();
  248. const diff = now - timestamp;
  249. // 小于1分钟
  250. if (diff < 60000) {
  251. return '刚刚';
  252. }
  253. // 小于1小时
  254. if (diff < 3600000) {
  255. return Math.floor(diff / 60000) + '分钟前';
  256. }
  257. // 小于24小时
  258. if (diff < 86400000) {
  259. return Math.floor(diff / 3600000) + '小时前';
  260. }
  261. // 小于30天
  262. if (diff < 2592000000) {
  263. return Math.floor(diff / 86400000) + '天前';
  264. }
  265. // 大于30天显示具体日期
  266. const date = new Date(timestamp);
  267. return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
  268. }
  269. }
  270. }
  271. </script>
  272. <style lang="scss" scoped>
  273. .comment-detail-container {
  274. display: flex;
  275. flex-direction: column;
  276. min-height: 100vh;
  277. background-color: #f5f5f5;
  278. padding-bottom: 100rpx;
  279. }
  280. .loading-container {
  281. display: flex;
  282. flex-direction: column;
  283. justify-content: center;
  284. align-items: center;
  285. margin-top: 200rpx;
  286. .loading-spinner {
  287. width: 80rpx;
  288. height: 80rpx;
  289. border: 6rpx solid #f3f3f3;
  290. border-top: 6rpx solid $uni-color-primary;
  291. border-radius: 50%;
  292. animation: spin 1s linear infinite;
  293. margin-bottom: 20rpx;
  294. }
  295. .loading-text {
  296. font-size: 28rpx;
  297. color: #666;
  298. }
  299. @keyframes spin {
  300. 0% { transform: rotate(0deg); }
  301. 100% { transform: rotate(360deg); }
  302. }
  303. }
  304. .error-container {
  305. display: flex;
  306. flex-direction: column;
  307. align-items: center;
  308. margin-top: 200rpx;
  309. .error-text {
  310. font-size: 28rpx;
  311. color: #999;
  312. margin-bottom: 30rpx;
  313. }
  314. .retry-button {
  315. background-color: $uni-color-primary;
  316. color: #fff;
  317. font-size: 28rpx;
  318. padding: 16rpx 40rpx;
  319. border-radius: 40rpx;
  320. border: none;
  321. }
  322. }
  323. .main-comment {
  324. background-color: #fff;
  325. padding: 30rpx;
  326. margin-bottom: 20rpx;
  327. .comment-header {
  328. display: flex;
  329. align-items: center;
  330. margin-bottom: 20rpx;
  331. .avatar {
  332. width: 80rpx;
  333. height: 80rpx;
  334. border-radius: 50%;
  335. margin-right: 20rpx;
  336. background-color: #f0f0f0;
  337. }
  338. .user-info {
  339. flex: 1;
  340. .username {
  341. font-size: 28rpx;
  342. font-weight: bold;
  343. color: #333;
  344. margin-bottom: 6rpx;
  345. }
  346. .time {
  347. font-size: 24rpx;
  348. color: #999;
  349. }
  350. }
  351. }
  352. .comment-content {
  353. font-size: 30rpx;
  354. color: #333;
  355. line-height: 1.6;
  356. margin-bottom: 20rpx;
  357. }
  358. .comment-images {
  359. margin-bottom: 20rpx;
  360. .image-grid {
  361. display: flex;
  362. flex-wrap: wrap;
  363. .comment-image {
  364. width: 200rpx;
  365. height: 200rpx;
  366. margin-right: 10rpx;
  367. margin-bottom: 10rpx;
  368. border-radius: 8rpx;
  369. background-color: #f0f0f0;
  370. }
  371. }
  372. }
  373. .comment-footer {
  374. display: flex;
  375. justify-content: space-between;
  376. align-items: center;
  377. padding-top: 20rpx;
  378. border-top: 1px solid #f0f0f0;
  379. .reply-count {
  380. font-size: 26rpx;
  381. color: #666;
  382. }
  383. .reply-btn {
  384. display: flex;
  385. align-items: center;
  386. text {
  387. font-size: 26rpx;
  388. color: #666;
  389. margin-left: 6rpx;
  390. }
  391. }
  392. }
  393. }
  394. .replies-container {
  395. background-color: #fff;
  396. .reply-item {
  397. padding: 30rpx;
  398. border-bottom: 1px solid #f0f0f0;
  399. &:active {
  400. background-color: #f9f9f9;
  401. }
  402. .reply-header {
  403. display: flex;
  404. align-items: center;
  405. margin-bottom: 16rpx;
  406. .avatar {
  407. width: 70rpx;
  408. height: 70rpx;
  409. border-radius: 50%;
  410. margin-right: 16rpx;
  411. background-color: #f0f0f0;
  412. }
  413. .user-info {
  414. flex: 1;
  415. .username {
  416. font-size: 26rpx;
  417. font-weight: bold;
  418. color: #333;
  419. margin-bottom: 4rpx;
  420. }
  421. .time {
  422. font-size: 22rpx;
  423. color: #999;
  424. }
  425. }
  426. }
  427. .reply-content {
  428. font-size: 28rpx;
  429. color: #333;
  430. line-height: 1.5;
  431. margin-bottom: 16rpx;
  432. padding-left: 86rpx;
  433. .reply-to {
  434. display: inline;
  435. .reply-username {
  436. color: $uni-color-primary;
  437. }
  438. }
  439. }
  440. .reply-images {
  441. padding-left: 86rpx;
  442. margin-bottom: 16rpx;
  443. .image-grid {
  444. display: flex;
  445. flex-wrap: wrap;
  446. .reply-image {
  447. width: 150rpx;
  448. height: 150rpx;
  449. margin-right: 10rpx;
  450. margin-bottom: 10rpx;
  451. border-radius: 8rpx;
  452. background-color: #f0f0f0;
  453. }
  454. }
  455. }
  456. .reply-footer {
  457. display: flex;
  458. justify-content: space-between;
  459. align-items: center;
  460. padding-left: 86rpx;
  461. .reply-info {
  462. .sub-reply-count {
  463. font-size: 24rpx;
  464. color: $uni-color-primary;
  465. }
  466. }
  467. .reply-btn {
  468. display: flex;
  469. align-items: center;
  470. text {
  471. font-size: 24rpx;
  472. color: #666;
  473. margin-left: 4rpx;
  474. }
  475. }
  476. }
  477. }
  478. }
  479. .load-more {
  480. text-align: center;
  481. padding: 30rpx 0;
  482. text {
  483. font-size: 26rpx;
  484. color: #666;
  485. }
  486. }
  487. </style>