瀏覽代碼

评论功能

yuhongqi 1 天之前
父節點
當前提交
6b3fb680f4

+ 18 - 0
src/api/course/userCourseComment.js

@@ -83,4 +83,22 @@ export function listFeaturedComments(courseId, videoId, query) {
     method: 'get',
     params: { videoId: videoId || undefined, ...query }
   })
+}
+
+// 查询指定小节下的用户留言(排除精选留言)
+export function listSectionUserComments(query) {
+  return request({
+    url: '/course/userCourseComment/sectionUserComments',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改用户留言可见范围:visibleAll 0自己可见 1全部人可见
+export function updateCommentVisibleAll(data) {
+  return request({
+    url: '/course/userCourseComment/updateVisibleAll',
+    method: 'put',
+    data: data
+  })
 }

+ 35 - 1
src/views/components/course/userCourseCatalogDetails.vue

@@ -414,6 +414,7 @@
         </el-form-item>
         <el-form-item label="精选留言">
           <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>
+          <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -601,6 +602,15 @@
                             v-if="commentDialog.open">
       </course-watch-comment>
     </el-dialog>
+    <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
+      <user-course-section-comment
+        v-if="userCommentDialog.open"
+        ref="userCourseSectionComment"
+        :course-id="userCommentDialog.courseId"
+        :video-id="userCommentDialog.videoId"
+      />
+    </el-dialog>
 
 
     <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
@@ -689,6 +699,7 @@ import VideoUpload from "@/components/VideoUpload/index.vue";
 import {listVideoResource, listPublicVideoResource} from '@/api/course/videoResource';
 import {getByIds} from '@/api/course/courseQuestionBank'
 import CourseWatchComment from "./courseWatchComment.vue";
+import UserCourseSectionComment from "./userCourseSectionComment.vue";
 import {getCateListByPid, getCatePidList, getPublicCateListByPid, getPublicCatePidList} from '@/api/course/userCourseCategory'
 import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
 import draggable from 'vuedraggable'
@@ -696,7 +707,7 @@ import { getConfigByKey } from '@/api/system/config'
 
 export default {
   name: "userCourseCatalog",
-  components: {VideoUpload, QuestionBank, CourseWatchComment, CourseProduct, draggable},
+  components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
   props: {
     /** 为 true 时「视频库选择」使用公域分类 + 公域素材接口;默认 false 走原 list / 原分类下拉 */
     usePublicVideoLibrary: {
@@ -851,6 +862,12 @@ export default {
         videoId: null,
         title: ""
       },
+      userCommentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
+      },
       enableRandomRedPacket:false,
       // 批量修改封面
       batchEditCoverDialog: {
@@ -1740,6 +1757,23 @@ export default {
       return isLt10M && isImage;
     },
     // 打开精选留言导入弹窗
+    openUserCommentDialog() {
+      if (!this.form.courseId || !this.form.videoId) {
+        this.$message.warning('请先保存课节后再查看用户留言')
+        return
+      }
+      this.userCommentDialog.courseId = this.form.courseId
+      this.userCommentDialog.videoId = this.form.videoId
+      this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
+      this.userCommentDialog.open = true
+    },
+    onUserCommentDialogOpened() {
+      this.$nextTick(() => {
+        if (this.$refs.userCourseSectionComment) {
+          this.$refs.userCourseSectionComment.handleQuery()
+        }
+      })
+    },
     openCommentImportDialog() {
       this.commentImportDialog.visible = true;
       this.commentImportDialog.file = null;

+ 97 - 3
src/views/components/course/userCourseCatalogDetailsZM.vue

@@ -389,11 +389,25 @@
                 ></el-time-picker>
               </template>
             </el-table-column>
-            <el-table-column label="操作" align="center" width="100px" fixed="right">
+            <el-table-column label="热卖标签" align="center" prop="hotSaleTags" min-width="140">
+              <template slot-scope="scope">
+                <el-tag
+                  v-for="(tag, tagIdx) in getHotSaleTagList(scope.row)"
+                  :key="tagIdx"
+                  size="mini"
+                  type="danger"
+                  style="margin: 2px;"
+                >{{ tag }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="160px" fixed="right">
               <template slot-scope="scope">
                 <el-button size="mini" type="text" icon="el-icon-delete"
                            @click="handleCourseProductDelete(scope.row)">删除
                 </el-button>
+                <el-button size="mini" type="text" icon="el-icon-price-tag"
+                           @click="handleOpenHotSaleTagDialog(scope.row, scope.$index)">添加热卖标签
+                </el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -426,6 +440,7 @@
         </el-form-item>
         <el-form-item label="精选留言">
           <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>
+          <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -492,6 +507,21 @@
     <el-dialog :title="courseProduct.title" :visible.sync="courseProduct.open" width="1000px" append-to-body>
       <course-product ref="courseProduct" @courseProductResult="courseProductResult"></course-product>
     </el-dialog>
+    <el-dialog title="热卖标签" :visible.sync="hotSaleTagDialog.visible" width="480px" append-to-body>
+      <p v-if="hotSaleTagDialog.productName" style="margin-bottom: 10px; color: #606266;">
+        商品:{{ hotSaleTagDialog.productName }}
+      </p>
+      <el-input
+        v-model="hotSaleTagDialog.tagInput"
+        type="textarea"
+        :rows="3"
+        placeholder="多个标签用逗号分隔;留空表示清除标签"
+      />
+      <template slot="footer">
+        <el-button @click="hotSaleTagDialog.visible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmHotSaleTag">确 定</el-button>
+      </template>
+    </el-dialog>
     <el-dialog title="视频库选择" :visible.sync="addBatchData.open" width="900px" append-to-body>
       <!-- 搜索条件 -->
       <el-form :inline="true" :model="addBatchData.queryParams" class="library-search">
@@ -581,6 +611,15 @@
                             v-if="commentDialog.open">
       </course-watch-comment>
     </el-dialog>
+    <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
+      <user-course-section-comment
+        v-if="userCommentDialog.open"
+        ref="userCourseSectionComment"
+        :course-id="userCommentDialog.courseId"
+        :video-id="userCommentDialog.videoId"
+      />
+    </el-dialog>
 
 
     <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
@@ -645,6 +684,7 @@ import VideoUpload from "@/components/VideoUpload/index.vue";
 import {listVideoResource} from '@/api/course/videoResource';
 import {getByIds} from '@/api/course/courseQuestionBank'
 import CourseWatchComment from "./courseWatchComment.vue";
+import UserCourseSectionComment from "./userCourseSectionComment.vue";
 import {getCateListByPid, getCatePidList} from '@/api/course/userCourseCategory'
 import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
 import draggable from 'vuedraggable'
@@ -652,7 +692,7 @@ import { getConfigByKey } from '@/api/system/config'
 
 export default {
   name: "userCourseCatalog",
-  components: {VideoUpload, QuestionBank, CourseWatchComment, CourseProduct, draggable},
+  components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
   props: {
     usePublicVideoLibrary: {
       type: Boolean,
@@ -688,6 +728,12 @@ export default {
         title: '',
         open: false,
       },
+      hotSaleTagDialog: {
+        visible: false,
+        index: -1,
+        productName: '',
+        tagInput: ''
+      },
       isPrivate: null,
       videoUrl: "",
       uploadTypeOptions: [
@@ -798,6 +844,12 @@ export default {
         videoId: null,
         title: ""
       },
+      userCommentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
+      },
       enableRandomRedPacket:false,
       // 精选留言导入弹窗
       commentImportDialog: {
@@ -929,6 +981,7 @@ export default {
         cardPopupTime: val.cardPopupTime || '00:00:00',  // 卡片弹出时间,默认 00:00:00
         cardCloseTime: val.cardCloseTime || '00:00:00',  // 卡片关闭时间,默认 00:00:00
         offShelfTime: val.offShelfTime || '00:00:00',  // 下架时间,默认 00:00:00
+        hotSaleTags: val.hotSaleTags || '',
       };
       // 检查商品是否已存在
       const exists = this.form.courseProducts.some(item => item.productId === val.productId);
@@ -1122,6 +1175,29 @@ export default {
       this.form.questionBankList.splice(this.form.questionBankList.findIndex(item => item.id === row.id), 1)
     },
 
+    getHotSaleTagList(row) {
+      if (!row || !row.hotSaleTags) {
+        return [];
+      }
+      return row.hotSaleTags.split(/[,,]/).map(t => t.trim()).filter(t => t);
+    },
+    handleOpenHotSaleTagDialog(row, index) {
+      this.hotSaleTagDialog.index = index;
+      this.hotSaleTagDialog.productName = row.productName || '';
+      this.hotSaleTagDialog.tagInput = row.hotSaleTags || '';
+      this.hotSaleTagDialog.visible = true;
+    },
+    confirmHotSaleTag() {
+      const tags = (this.hotSaleTagDialog.tagInput || '')
+        .split(/[,,]/)
+        .map(t => t.trim())
+        .filter(t => t);
+      const index = this.hotSaleTagDialog.index;
+      if (index >= 0 && this.form.courseProducts[index]) {
+        this.$set(this.form.courseProducts[index], 'hotSaleTags', tags.length ? tags.join(',') : '');
+      }
+      this.hotSaleTagDialog.visible = false;
+    },
     //删除商品
     handleCourseProductDelete(row) {
       // 优先使用 productId 查找
@@ -1376,7 +1452,8 @@ export default {
                 onShelfTime: product.onShelfTime || '00:00:00',
                 cardPopupTime: product.cardPopupTime || '00:00:00',
                 cardCloseTime: product.cardCloseTime || '00:00:00',
-                offShelfTime: product.offShelfTime || '00:00:00'
+                offShelfTime: product.offShelfTime || '00:00:00',
+                hotSaleTags: product.hotSaleTags || ''
               }));
             } else {
               // 否则按照原有逻辑设置到 packageList
@@ -1825,6 +1902,23 @@ export default {
       }
       return isLt10M && isImage;
     },
+    openUserCommentDialog() {
+      if (!this.form.courseId || !this.form.videoId) {
+        this.$message.warning('请先保存课节后再查看用户留言')
+        return
+      }
+      this.userCommentDialog.courseId = this.form.courseId
+      this.userCommentDialog.videoId = this.form.videoId
+      this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
+      this.userCommentDialog.open = true
+    },
+    onUserCommentDialogOpened() {
+      this.$nextTick(() => {
+        if (this.$refs.userCourseSectionComment) {
+          this.$refs.userCourseSectionComment.handleQuery()
+        }
+      })
+    },
     // 打开精选留言导入弹窗
     openCommentImportDialog() {
       this.commentImportDialog.visible = true;

+ 246 - 0
src/views/components/course/userCourseSectionComment.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="app-container user-section-comment">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="72px">
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="commentList" border>
+      <el-table-column label="用户昵称" prop="nickName" width="120" show-overflow-tooltip />
+      <el-table-column label="类型" width="72" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.type === 2 ? 'info' : ''">
+            {{ scope.row.type === 2 ? '回复' : '评论' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言内容" min-width="360">
+        <template slot-scope="scope">
+          <div class="comment-content-cell">
+            <template v-for="(part, idx) in parseCommentContent(scope.row.content)">
+              <p v-if="part.kind === 'text'" :key="'t-' + scope.row.commentId + '-' + idx" class="comment-text">
+                {{ part.value }}
+              </p>
+              <el-image
+                v-else-if="part.kind === 'image'"
+                :key="'i-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-image"
+                :src="part.value"
+                :preview-src-list="[part.value]"
+                fit="contain"
+              />
+              <video
+                v-else-if="part.kind === 'video'"
+                :key="'v-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-video"
+                :src="part.value"
+                controls
+                preload="metadata"
+              />
+              <a
+                v-else-if="part.kind === 'link'"
+                :key="'l-' + scope.row.commentId + '-' + idx"
+                :href="part.value"
+                target="_blank"
+                rel="noopener"
+                class="comment-link"
+              >{{ part.value }}</a>
+            </template>
+            <span v-if="!scope.row.content" class="comment-empty">—</span>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="点赞" prop="likes" width="70" align="center" />
+      <el-table-column label="可见范围" width="100" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.visibleAll === 1 ? 'success' : 'info'">
+            {{ scope.row.visibleAll === 1 ? '全部人可见' : '自己可见' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言时间" prop="createTime" width="160" align="center" />
+      <el-table-column label="操作" width="180" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 1"
+            @click="handleUpdateVisible(scope.row, 1)"
+          >全部人可见</el-button>
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 0 || scope.row.visibleAll == null"
+            @click="handleUpdateVisible(scope.row, 0)"
+          >自己可见</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listSectionUserComments, updateCommentVisibleAll } from '@/api/course/userCourseComment'
+
+const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i
+const VIDEO_EXT = /\.(mp4|webm|ogg|mov|m4v|m3u8)(\?.*)?$/i
+
+export default {
+  name: 'UserCourseSectionComment',
+  props: {
+    courseId: {
+      type: [String, Number],
+      required: true
+    },
+    videoId: {
+      type: [String, Number],
+      required: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      commentList: [],
+      visibleUpdatingId: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        courseId: null,
+        videoId: null,
+        nickName: null
+      }
+    }
+  },
+  watch: {
+    courseId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.courseId = val
+      }
+    },
+    videoId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.videoId = val
+      }
+    }
+  },
+  methods: {
+    getList() {
+      if (!this.queryParams.courseId || !this.queryParams.videoId) {
+        return
+      }
+      this.loading = true
+      listSectionUserComments(this.queryParams).then(response => {
+        this.commentList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams.nickName = null
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleUpdateVisible(row, visibleAll) {
+      const label = visibleAll === 1 ? '全部人可见' : '自己可见'
+      this.$confirm(`确认将该留言设置为「${label}」?`, '提示', { type: 'warning' }).then(() => {
+        this.visibleUpdatingId = row.commentId
+        updateCommentVisibleAll({
+          commentId: row.commentId,
+          visibleAll: visibleAll
+        }).then(() => {
+          this.$message.success('修改成功')
+          this.getList()
+        }).finally(() => {
+          this.visibleUpdatingId = null
+        })
+      }).catch(() => {})
+    },
+    parseCommentContent(content) {
+      if (!content || !String(content).trim()) {
+        return []
+      }
+      const lines = String(content).split(/\r?\n/).map(s => s.trim()).filter(Boolean)
+      const parts = []
+      lines.forEach(line => {
+        if (!/^https?:\/\//i.test(line)) {
+          parts.push({ kind: 'text', value: line })
+          return
+        }
+        if (VIDEO_EXT.test(line) || /\/comment\/video\//i.test(line)) {
+          parts.push({ kind: 'video', value: line })
+        } else if (IMAGE_EXT.test(line) || /\/comment\/image\//i.test(line)) {
+          parts.push({ kind: 'image', value: line })
+        } else {
+          parts.push({ kind: 'link', value: line })
+        }
+      })
+      return parts
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-section-comment {
+  padding: 0;
+}
+.comment-content-cell {
+  text-align: left;
+  line-height: 1.6;
+}
+.comment-text {
+  margin: 0 0 6px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.comment-media {
+  display: block;
+  margin: 6px 0;
+  max-width: 280px;
+  border-radius: 4px;
+}
+.comment-image {
+  max-height: 160px;
+}
+.comment-video {
+  max-height: 200px;
+  background: #000;
+}
+.comment-link {
+  display: block;
+  margin: 4px 0;
+  color: #409eff;
+  word-break: break-all;
+}
+.comment-empty {
+  color: #909399;
+}
+</style>