Browse Source

feat: add like function for student work

1. add internationalized text for like success in cn/hk/en json files
2. add thumbs up icon and register it
3. add likeWork api service and export it
4. add like switch and like button in choice question detail dialog
5. add like logic and update work array with like count and status
6. add success prompt and refresh logic after like
lsc 5 days ago
parent
commit
e5d3ce7404

+ 2 - 0
src/plugins/icon.ts

@@ -131,6 +131,7 @@ import {
   LoadingFour, // 引入loadingIcon
   UpTwo,
   Refresh,
+  ThumbsUp
 } from '@icon-park/vue-next'
 
 export interface Icons {
@@ -267,6 +268,7 @@ export const icons: Icons = {
   IconLoading: LoadingFour, // 添加loadingIcon
   UpTwo: UpTwo,
   IconRefresh: Refresh,
+  IconThumbsUp: ThumbsUp,
 }
 
 export default {

+ 14 - 1
src/services/course.ts

@@ -199,6 +199,17 @@ export const getWorkPageId = (params: any): Promise<any> => {
 }
 
 
+/**
+ * 
+ * 点赞功能
+ * @param any wid 作业id lid 用户id t 1点赞 2评论 c 评论内容
+ * @returns Promise<any>
+ */
+
+export const likeWork = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}insertComment`, [params])
+}
+
 /**
  * 
  * 获取年级
@@ -229,6 +240,8 @@ export default {
   getAgentData,
   clearDialogue,
   getWorkDetail,
-  getWorkPageId
+  getWorkPageId,
+  likeWork,
+  getClassById,
 }
 

+ 1 - 2
src/views/Student/components/aiChat.vue

@@ -117,11 +117,10 @@ import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2, getAgentCh
 import { useSlidesStore } from '@/store'
 import { lang } from '@/main'
 import MarkdownIt from 'markdown-it'
-import { getWorkPageId } from '@/services/course'
+import { getWorkPageId, getClassById } from '@/services/course'
 import FileInput from '@/components/FileInput2.vue'
 import axios from '@/services/config'
 import message from '@/utils/message'
-import { getClassById } from '@/services/course'
 
 interface ChatMessage {
   uid?: string

+ 65 - 0
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -221,6 +221,10 @@
                 <Switch :value="canValue" @update:value="handleCanChange" />
                 <span>{{ lang.ssShowResult }}</span>
               </div>
+              <div class="switch">
+                <Switch :value="likeValue" @update:value="handleLikeChange" />
+                <span>{{ lang.ssLike }}</span>
+              </div>
             </div>
           </div>
         </div>
@@ -242,6 +246,12 @@
             <div class="c_t15_c_i_bottom">
               <span v-html="item.content.answer"></span>
             </div>
+            <div class="bottom_btn" v-if="likeValue">
+              <div class="bottom_like" :class="{'active': item.isLikes}"  @click.stop="handleLikeClick(item)">
+                <IconThumbsUp />
+                <span>{{ item.likesCount }}</span>
+              </div>
+            </div>
           </div>
         </div>
 
@@ -548,6 +558,7 @@ import echartsDialog from './echartsDialog.vue'
 import { chat_stream, chat_no_stream } from '@/tools/aiChat'
 import axios from '@/services/config'
 import Switch from '@/components/Switch.vue'
+import { likeWork } from '@/services/course'
 
 const props = defineProps<{
   visible: number[];
@@ -571,6 +582,7 @@ const emit = defineEmits<{
   (e: 'update:visible', v: number[]): void;
   (e: 'changeWorkIndex', v: number): void;
   (e: 'setIsResultArray', v: boolean, key: string): void;
+  (e: 'successLike'): void;
 }>()
 
 const visible = computed({
@@ -591,12 +603,39 @@ watch(() => props.resultArray?.can, (newVal) => {
   canValue.value = newVal
 })
 
+const likeValue = ref(props.resultArray?.like ?? false)
+
+watch(() => props.resultArray?.like, (newVal) => {
+  likeValue.value = newVal
+})
+
 const handleCanChange = (value: boolean) => {
   console.log(value)
   console.log(props.resultArray)
   emit('setIsResultArray', value, 'can')
 }
 
+const handleLikeChange = (value: boolean) => {
+  if (value && !canValue.value) {
+    emit('setIsResultArray', true, 'can')
+  }
+  console.log(value)
+  console.log(props.resultArray)
+  emit('setIsResultArray', value, 'like')
+}
+
+import _ from 'lodash'
+
+const handleLikeClick = _.debounce((item: any) => {
+  likeWork({
+    wid: item.id,
+    lid: props.userId,
+    t: 1,
+    c: ''
+  })
+  emit('successLike')
+}, 300)
+
 // 预览图片组件
 const previewImageToolRef = ref<any>(null)
 // 选择用户组件
@@ -3165,6 +3204,32 @@ onUnmounted(() => {
     gap: .5rem;
     font-size: .8rem;
     color: #141517;
+
+    +.switch{
+      margin-left: .5rem;
+    }
+  }
+}
+
+.bottom_btn{
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-weight: 300;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.7);
+
+  .bottom_like{
+    margin-left: auto;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 5px;
+    color: rgba(0, 0, 0, 0.7);
+    
+    &.active{
+      color: rgba(252, 207, 0, 1);
+    }
   }
 }
 </style>

+ 49 - 4
src/views/Student/index.vue

@@ -106,7 +106,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :roleType="props.type" :cid="props.cid" :workId="workId" :workUrl="workUrl" :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale" :resultArray="currentIsResultArray" @setIsResultArray="setIsResultArray2" :isCreator="isCreator"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :roleType="props.type" :cid="props.cid" :workId="workId" :workUrl="workUrl" :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale" :resultArray="currentIsResultArray" @setIsResultArray="setIsResultArray2" :isCreator="isCreator" @successLike="successLike"/>
           <SpeakingClassPanel
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             ref="speakingPanelRef"
@@ -440,7 +440,7 @@ import SpeakingClassPanel from './components/SpeakingClassPanel/index.vue'
 import aiChat from './components/aiChat.vue'
 import messageInstruction from '@/utils/components/messageInstruction.vue'
 
-const messageInstructionRef = ref(null)
+const messageInstructionRef = ref<typeof messageInstruction>()
 
 // 生成标准 UUID v4 格式(36位,符合 [0-9a-fA-F-] 格式)
 const generateUUID = (): string => {
@@ -2497,6 +2497,17 @@ const successSubmit = () => {
   getWork(true)
 }
 
+const successLike = () => {
+  messageInstructionRef.value.success(lang.ssLikeSucc)
+  sendMessage({
+    type: 'like_updated',
+    courseid: props.courseid,
+    slideIndex: slideIndex.value,
+    userid: props.userid
+  })
+  getWork(true)
+}
+
 // 刷新iframe功能
 const handleRefreshPage = () => {
   console.log('刷新iframe按钮被点击')
@@ -2888,12 +2899,35 @@ const getWork = async (isUpdate = false) => {
     const frame = elementList.value.find(element => element.type === ElementTypes.FRAME)
     console.log('frame:', frame)
     const toolType = frame?.toolType ?? ''
+    const likeArray = res[1]
+    console.log('likeArray', likeArray)
     const newWorkArray = props.cid
       ? res[0].filter((work: any) => {
+        
         // console.log(work.ttype == '1' || (work.ttype == '2' && work.classid.includes(props.cid)) && (work.atool === toolType.value || !toolType.value))
         return work.ttype == '1' || (work.ttype == '2' && work.classid.includes(props.cid)) && (work.atool == toolType.value || !toolType.value)
+      }).map((work: any) => {
+        // 计算点赞数量:likeArray中wid等于当前work.id的记录数
+        const likesCount = likeArray.filter((like: any) => like.workId == work.id).length
+        // 判断当前用户是否点赞:likeArray中wid等于当前work.id且likesId等于当前用户id
+        const isLikes = likeArray.some((like: any) => like.workId == work.id && like.likesId == props.userid)
+        return {
+          ...work,
+          likesCount,
+          isLikes
+        }
+      })
+      : res[0].map((work: any) => {
+        // 计算点赞数量:likeArray中wid等于当前work.id的记录数
+        const likesCount = likeArray.filter((like: any) => like.workId == work.id).length
+        // 判断当前用户是否点赞:likeArray中wid等于当前work.id且likesId等于当前用户id
+        const isLikes = likeArray.some((like: any) => like.workId == work.id && like.likesId == props.userid)
+        return {
+          ...work,
+          likesCount,
+          isLikes
+        }
       })
-      : res[0]
     
     // 如果是更新模式,只有当数据真正变化时才更新
     if (isUpdate) {
@@ -2960,7 +2994,7 @@ const checkWorkArrayChanged = (oldArray: WorkItem[], newArray: WorkItem[]): bool
     const oldWork = oldArray[i]
     const newWork = newArray[i]
     
-    if (oldWork.id !== newWork.id || oldWork.name !== newWork.name || oldWork.content !== newWork.content) {
+    if (oldWork.id !== newWork.id || oldWork.name !== newWork.name || oldWork.content !== newWork.content || oldWork.isLikes !== newWork.isLikes || oldWork.likesCount !== newWork.likesCount) {
       return true
     }
   }
@@ -3538,6 +3572,17 @@ const getMessages = (msgObj: any) => {
       }
     }, 1000)
   }
+  
+  // 处理点赞消息 - 当有人点赞时,重新获取点赞数据
+  if (msgObj.type === 'like_updated' && msgObj.courseid === props.courseid) {
+    console.log('收到点赞消息,重新获取点赞数据')
+    // 延迟一点时间,确保后端数据已更新
+    setTimeout(() => {
+      if (currentSlideHasIframe.value) {
+        getWork(true) // 传入true表示是更新模式
+      }
+    }, 1000)
+  }
 
   // 处理英语口语状态更新 - 学生开始/完成对话时,刷新班级答题面板
   if (props.type == '1' && msgObj.type === 'speaking_session_updated' && msgObj.courseid === props.courseid) {

+ 3 - 1
src/views/lang/cn.json

@@ -910,5 +910,7 @@
   "ssCreating": "创建中...",
   "ssWebEditor": "AI 网页编辑器",
   "ssConfirmAdd": "确认添加",
-  "ssShowResult": "展示结果"
+  "ssShowResult": "展示结果",
+  "ssLikeSucc": "点赞成功",
+  "ssLike": "点赞"
 }

+ 3 - 1
src/views/lang/en.json

@@ -910,5 +910,7 @@
   "ssCreating": "Creating...",
   "ssWebEditor": "AI Web Editor",
   "ssConfirmAdd": "Confirm Add",
-  "ssShowResult": "Show Result"
+  "ssShowResult": "Show Result",
+  "ssLikeSucc": "Like Success",
+  "ssLike": "Like"
 }

+ 3 - 1
src/views/lang/hk.json

@@ -910,5 +910,7 @@
   "ssCreating": "創建中...",
   "ssWebEditor": "AI 網頁編輯器",
   "ssConfirmAdd": "確認添加",
-  "ssShowResult": "展示結果"
+  "ssShowResult": "展示結果",
+  "ssLikeSucc": "點贊成功",
+  "ssLike": "點贊"
 }