diff --git a/package.json b/package.json
index 6dd2b056..c5984095 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"dependencies": {
"@alova/scene-vue": "^1.0.4",
"@element-plus/icons-vue": "^2.1.0",
+ "@imengyu/vue3-context-menu": "^1.2.10",
"alova": "^2.3.0",
"dayjs": "^1.11.7",
"dompurify": "^3.0.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index baf1a6bc..ce86b788 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,4 +1,8 @@
-lockfileVersion: '6.0'
+lockfileVersion: '6.1'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
dependencies:
'@alova/scene-vue':
@@ -7,6 +11,9 @@ dependencies:
'@element-plus/icons-vue':
specifier: ^2.1.0
version: registry.npmmirror.com/@element-plus/icons-vue@2.1.0(vue@3.2.47)
+ '@imengyu/vue3-context-menu':
+ specifier: ^1.2.10
+ version: registry.npmmirror.com/@imengyu/vue3-context-menu@1.2.10
alova:
specifier: ^2.3.0
version: registry.npmmirror.com/alova@2.3.0
@@ -1166,6 +1173,12 @@ packages:
- supports-color
dev: true
+ registry.npmmirror.com/@imengyu/vue3-context-menu@1.2.10:
+ resolution: {integrity: sha512-L+qIoqcewQ8SPk2PMvsaMD9HWxFouij0JAqYwn19Fz7giqz0rLpWMf9aJRpQ3ysOySEKPdilg8dLYYB96QmGHA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@imengyu/vue3-context-menu/-/vue3-context-menu-1.2.10.tgz}
+ name: '@imengyu/vue3-context-menu'
+ version: 1.2.10
+ dev: false
+
registry.npmmirror.com/@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz}
name: '@jridgewell/gen-mapping'
diff --git a/src/assets/operate-icons/to_top.svg b/src/assets/operate-icons/to_top.svg
new file mode 100644
index 00000000..78645853
--- /dev/null
+++ b/src/assets/operate-icons/to_top.svg
@@ -0,0 +1 @@
+
diff --git a/src/components.d.ts b/src/components.d.ts
index 990a61fa..a611c034 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -19,8 +19,10 @@ declare module '@vue/runtime-core' {
ElPopover: typeof import('element-plus/es')['ElPopover']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default']
+ IconDislike: typeof import('./components/icons/iconDislike.vue')['default']
IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./components/icons/IconEcosystem.vue')['default']
+ IconLike: typeof import('./components/icons/iconLike.vue')['default']
IconSupport: typeof import('./components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./components/icons/IconTooling.vue')['default']
IEpArrowDownBold: typeof import('~icons/ep/arrow-down-bold')['default']
@@ -33,6 +35,7 @@ declare module '@vue/runtime-core' {
IEpLock: typeof import('~icons/ep/lock')['default']
IEpMale: typeof import('~icons/ep/male')['default']
IEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
+ LikeButton: typeof import('./components/LikeButton/index.vue')['default']
LoginBox: typeof import('./components/LoginBox/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
diff --git a/src/components/VirtualList/index.tsx b/src/components/VirtualList/index.tsx
index e5c97397..0add0b96 100644
--- a/src/components/VirtualList/index.tsx
+++ b/src/components/VirtualList/index.tsx
@@ -178,12 +178,12 @@ export default defineComponent({
* @param index 索引值
* @description 如果索引值大于等于数据长度说明到底了则滚动到底部
*/
- const scrollToIndex = (index: number) => {
+ const scrollToIndex = (index: number, smooth?: boolean) => {
if (index >= props.data.length - 1) {
scrollToBottom()
} else {
const offset = virtual.getOffset(index)
- scrollToOffset(offset)
+ scrollToOffset(offset, smooth)
}
}
@@ -191,9 +191,10 @@ export default defineComponent({
* 滚动到指定偏移量
* @param offset 滚动条偏移量
*/
- const scrollToOffset = (offset: number) => {
+ const scrollToOffset = (offset: number, smooth = false) => {
if (rootRef.value) {
- rootRef.value.scrollTop = offset
+ // rootRef.value.scrollTop = offset
+ rootRef.value.scroll({ left: 0, top: offset, behavior: smooth ? 'smooth' : 'auto' })
}
}
@@ -236,13 +237,13 @@ export default defineComponent({
}
// 滚动到底部
- const scrollToBottom = () => {
+ const scrollToBottom = (smooth?: boolean) => {
if (shepherd.value) {
const offset = shepherd.value.offsetTop
- scrollToOffset(offset)
+ scrollToOffset(offset, smooth)
setTimeout(() => {
if (getOffset() + getClientSize() < getScrollSize()) {
- scrollToBottom()
+ scrollToBottom(smooth)
}
}, 3)
}
diff --git a/src/components/icons/iconDislike.vue b/src/components/icons/iconDislike.vue
new file mode 100644
index 00000000..4f4e1519
--- /dev/null
+++ b/src/components/icons/iconDislike.vue
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/components/icons/iconLike.vue b/src/components/icons/iconLike.vue
new file mode 100644
index 00000000..41fd66a3
--- /dev/null
+++ b/src/components/icons/iconLike.vue
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/hooks/useLikeToggle.ts b/src/hooks/useLikeToggle.ts
new file mode 100644
index 00000000..10f5656c
--- /dev/null
+++ b/src/hooks/useLikeToggle.ts
@@ -0,0 +1,67 @@
+import { computed } from 'vue'
+import apis from '@/services/apis'
+import { ActType, MarkType, IsYet } from '@/services/types'
+import type { MessageItemType } from '@/services/types'
+
+/**
+ * 统一点赞倒赞操作Hook
+ * @param message 消息
+ * @description 引入该Hook后,可直接使用isLike(是否已点赞)、isDisLike(是否已倒赞)、onLike(点赞方法)、onDisLike(倒赞方法)
+ */
+export const useLikeToggle = (message: MessageItemType['message']) => {
+ const isLike = computed(() => message.messageMark.userLike === IsYet.Yes)
+ const isDisLike = computed(() => message.messageMark.userDislike === IsYet.Yes)
+ const likeCount = computed(() => message.messageMark.likeCount)
+ const dislikeCount = computed(() => message.messageMark.dislikeCount)
+
+ /**
+ * 点赞\取消点赞
+ * @description 根据是否已经点赞控制更新点赞状态
+ */
+ const onLike = async () => {
+ const actType = isLike.value ? ActType.Cancel : ActType.Confirm
+ await apis.markMsg({ actType, markType: MarkType.Like, msgId: message.id }).send()
+
+ // 根据actType类型去更新本地点赞状态-点赞数
+ const { likeCount } = message.messageMark
+ const isConfirm = actType === ActType.Confirm
+ message.messageMark.userLike = isConfirm ? IsYet.Yes : IsYet.No
+ message.messageMark.likeCount = isConfirm ? likeCount + 1 : likeCount - 1
+ // 互斥操作
+ if (isDisLike.value) {
+ message.messageMark.userDislike = IsYet.No
+ message.messageMark.dislikeCount = dislikeCount.value - 1
+ }
+ }
+
+ /**
+ * 倒赞\取消倒赞
+ * @description 根据是否已经倒赞控制更新倒赞状态
+ */
+ const onDisLike = async () => {
+ const actType = isDisLike.value ? ActType.Cancel : ActType.Confirm
+ await apis.markMsg({ actType, markType: MarkType.DisLike, msgId: message.id }).send()
+
+ // 根据actType类型去更新本地倒赞状态-倒赞数
+ const { dislikeCount } = message.messageMark
+ const isConfirm = actType === ActType.Confirm
+ message.messageMark.userDislike = isConfirm ? IsYet.Yes : IsYet.No
+ message.messageMark.dislikeCount = isConfirm ? dislikeCount + 1 : dislikeCount - 1
+ // 互斥操作
+ if (isLike.value) {
+ message.messageMark.userLike = IsYet.No
+ message.messageMark.likeCount = likeCount.value - 1
+ }
+ }
+
+ return {
+ isLike,
+ isDisLike,
+ likeCount,
+ dislikeCount,
+ onLike,
+ onDisLike,
+ }
+}
+
+export default useLikeToggle
diff --git a/src/main.ts b/src/main.ts
index b96be10f..f08fd7a0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import weekday from 'dayjs/plugin/weekday'
+import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
// import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
diff --git a/src/services/types.ts b/src/services/types.ts
index 97bcc285..98980923 100644
--- a/src/services/types.ts
+++ b/src/services/types.ts
@@ -70,6 +70,77 @@ export enum MessageType {
Danger,
}
+export type MessageReplyType = {
+ /**
+ * 是否可消息跳转 0否 1是
+ */
+ canCallback: number
+ /**
+ * 是否可消息跳转 0否 1是
+ */
+ content: string
+ /**
+ * 跳转间隔的消息条数
+ */
+ gapCount: number
+ /**
+ * 消息id
+ */
+ id: number
+ /**
+ * 用户名称
+ */
+ username: string
+}
+
+export type MessageItemContentType = {
+ /**
+ * 消息内容
+ */
+ content: string
+ /**
+ * 消息id
+ */
+ id: number
+ /**
+ * 消息标记
+ */
+ messageMark: {
+ /**
+ * 点赞数
+ */
+ likeCount: number
+ /**
+ * 该用户是否已经点赞 0否 1是
+ */
+ userLike: IsYet
+ /**
+ * 点赞数
+ */
+ dislikeCount: number
+ /**
+ * 到赞数
+ */
+ userDislike: IsYet
+ }
+ /**
+ * 父消息,如果没有父消息,返回的是null
+ */
+ reply: MessageReplyType | null
+ /**
+ * 消息发送时间
+ */
+ sendTime: number
+ /**
+ * 消息类型 1正常文本 2.爆赞 (点赞超过10)3.危险发言(举报超5)
+ */
+ type: MessageType
+ /**
+ * 消息中的链接
+ */
+ urlTitleMap: Record
+}
+
export type MessageItemType = {
/**
* 发送者信息
@@ -108,74 +179,8 @@ export type MessageItemType = {
/**
* 消息详情
*/
- message: {
- /**
- * 消息内容
- */
- content: string
- /**
- * 消息id
- */
- id: number
- /**
- * 消息标记
- */
- messageMark: {
- /**
- * 点赞数
- */
- likeCount: number
- /**
- * 该用户是否已经点赞 0否 1是
- */
- userLike: IsYet
- /**
- * 点赞数
- */
- dislikeCount: number
- /**
- * 到赞数
- */
- userDislike: IsYet
- }
- /**
- * 父消息,如果没有父消息,返回的是null
- */
- reply: {
- /**
- * 是否可消息跳转 0否 1是
- */
- canCallback: number
- /**
- * 是否可消息跳转 0否 1是
- */
- content: string
- /**
- * 跳转间隔的消息条数
- */
- gapCount: number
- /**
- * 消息id
- */
- id: number
- /**
- * 用户名称
- */
- username: string
- } | null
- /**
- * 消息发送时间
- */
- sendTime: number
- /**
- * 消息类型 1正常文本 2.爆赞 (点赞超过10)3.危险发言(举报超5)
- */
- type: MessageType
- /**
- * 消息中的链接
- */
- urlTitleMap: Record
- }
+ message: MessageItemContentType
+
// 是否显示时间,有值才显示
timeBlock?: string
}
@@ -260,3 +265,26 @@ export type BadgeType = {
// 是否佩戴 0否 1是
wearing: IsYet
}
+
+export type MarkItemType = {
+ /**
+ * 操作用户
+ */
+ uid: number
+ /**
+ * 消息id
+ */
+ msgId: number
+ /**
+ * 操作类型 1点赞 2举报
+ */
+ markType: MarkType
+ /**
+ * 数量
+ */
+ markCount: number
+ /**
+ * 动作类型 1确认 2取消
+ */
+ actType: ActType
+}
diff --git a/src/stores/chat.ts b/src/stores/chat.ts
index 387d6234..de6c04ba 100644
--- a/src/stores/chat.ts
+++ b/src/stores/chat.ts
@@ -1,7 +1,8 @@
import { ref, reactive } from 'vue'
import { defineStore } from 'pinia'
import apis from '@/services/apis'
-import type { MessageItemType } from '@/services/types'
+import type { MessageItemType, MarkItemType } from '@/services/types'
+import { MarkType } from '@/services/types'
import { computedTimeBlock } from '@/utils/computedTime'
import shakeTitle from '@/utils/shakeTitle'
@@ -15,6 +16,18 @@ export const useChatStore = defineStore('chat', () => {
const isLoading = ref(false) // 是否正在加载
const isStartCount = ref(false) // 是否开始计数
const cursor = ref()
+ const flashMsgId = ref(null) // 记录当前回复的消息id 用于当前消息的闪烁
+ let timer: any = null // 消息闪烁计时器
+
+ // 开始闪烁
+ const startFlash = (replyId: number) => {
+ flashMsgId.value = replyId
+ // 以最后一次为准,清除上一次的定时器
+ clearTimeout(timer)
+ timer = setTimeout(() => {
+ flashMsgId.value = null
+ }, 2000)
+ }
// 新消息计数
const newMsgCount = ref(0)
@@ -22,9 +35,9 @@ export const useChatStore = defineStore('chat', () => {
// 当前消息回复
const currentMsgReply = reactive>({})
- const getMsgList = async () => {
+ const getMsgList = async (size = pageSize) => {
isLoading.value = true
- const data = await apis.getMsgList({ params: { pageSize, cursor: cursor.value, roomId: 1 } }).send()
+ const data = await apis.getMsgList({ params: { pageSize: size, cursor: cursor.value, roomId: 1 } }).send()
if (!data) return
chatMessageList.value = [...computedTimeBlock(data.list), ...chatMessageList.value]
cursor.value = data.cursor
@@ -62,19 +75,44 @@ export const useChatStore = defineStore('chat', () => {
chatMessageList.value = chatMessageList.value.filter((item) => item.fromUser.uid !== uid)
}
- const loadMore = async () => {
+ const loadMore = async (size?: number) => {
if (isLast.value && isLoading.value) return
- await getMsgList()
+ await getMsgList(size)
}
const clearNewMsgCount = () => {
newMsgCount.value = 0
}
+ // 查找消息在列表里面的索引,倒过来的索引
+ const getMsgIndex = (msgId: number) => {
+ if (!msgId || isNaN(Number(msgId))) return -1
+ return chatMessageList.value.findIndex((item) => item.message.id === msgId)
+ }
+
+ // 更新点赞、举报数
+ const updateMarkCount = (markList: MarkItemType[]) => {
+ // 循环更新点赞数
+ markList.forEach((mark: MarkItemType) => {
+ const { msgId, markType, markCount } = mark
+
+ const msgItem = chatMessageList.value.find((item) => item.message.id === msgId)
+ if (msgItem) {
+ if (markType === MarkType.Like) {
+ msgItem.message.messageMark.likeCount = markCount
+ } else if (markType === MarkType.DisLike) {
+ msgItem.message.messageMark.dislikeCount = markCount
+ }
+ }
+ })
+ }
+
return {
+ getMsgIndex,
chatMessageList,
pushMsg,
clearNewMsgCount,
+ updateMarkCount,
chatListToBottomAction,
newMsgCount,
isLoading,
@@ -83,5 +121,7 @@ export const useChatStore = defineStore('chat', () => {
loadMore,
currentMsgReply,
filterUser,
+ startFlash,
+ flashMsgId,
}
})
diff --git a/src/utils/copy.ts b/src/utils/copy.ts
new file mode 100644
index 00000000..f15a5472
--- /dev/null
+++ b/src/utils/copy.ts
@@ -0,0 +1,17 @@
+export function copyToClip(text: string) {
+ return new Promise((resolve, reject) => {
+ try {
+ const input: HTMLTextAreaElement = document.createElement('textarea')
+ input.setAttribute('readonly', 'readonly')
+ input.value = text
+ input.style.zIndex = '-1'
+ document.body.appendChild(input)
+ input.select()
+ if (document.execCommand('copy')) document.execCommand('copy')
+ document.body.removeChild(input)
+ resolve(text)
+ } catch (error) {
+ reject(error)
+ }
+ })
+}
diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts
index 9167e419..b11e8747 100644
--- a/src/utils/websocket.ts
+++ b/src/utils/websocket.ts
@@ -4,7 +4,7 @@ import { useChatStore } from '@/stores/chat'
import { useGroupStore } from '@/stores/group'
import { WsResponseMessageType, WsRequestMsgType } from './wsType'
import type { LoginSuccessResType, LoginInitResType, WsReqMsgContentType, OnStatusChangeType } from './wsType'
-import type { MessageItemType } from '@/services/types'
+import type { MessageItemType, MarkItemType } from '@/services/types'
import { OnlineStatus } from '@/services/types'
import { worker } from './initWorker'
import shakeTitle from '@/utils/shakeTitle'
@@ -177,6 +177,12 @@ class WS {
groupStore.filterUser(data.uid)
break
}
+ // 点赞、倒赞消息通知
+ case WsResponseMessageType.WSMsgMarkItem: {
+ const data = params.data as { markList: MarkItemType[] }
+ chatStore.updateMarkCount(data.markList)
+ break
+ }
default: {
console.log('接收到未处理类型的消息:', params)
break
diff --git a/src/utils/wsType.ts b/src/utils/wsType.ts
index ca8aa2a4..350d8678 100644
--- a/src/utils/wsType.ts
+++ b/src/utils/wsType.ts
@@ -30,6 +30,10 @@ export enum WsResponseMessageType {
* 7.禁用的用户
*/
InValidUser,
+ /**
+ * 8.点赞、倒赞更新通知
+ */
+ WSMsgMarkItem,
}
/**
diff --git a/src/views/Home/components/ChatBox/index.vue b/src/views/Home/components/ChatBox/index.vue
index 7f1c2bcf..b666b9c5 100644
--- a/src/views/Home/components/ChatBox/index.vue
+++ b/src/views/Home/components/ChatBox/index.vue
@@ -140,7 +140,12 @@ const insertText = (emoji: string) => {
diff --git a/src/views/Home/components/ChatList/MsgItem/index.vue b/src/views/Home/components/ChatList/MsgItem/index.vue
index 24403db8..c3d73156 100644
--- a/src/views/Home/components/ChatList/MsgItem/index.vue
+++ b/src/views/Home/components/ChatList/MsgItem/index.vue
@@ -1,13 +1,16 @@
@@ -56,17 +33,11 @@ const isDisLikeActive = computed(() => props.msg.message.messageMark.userDislike
-
+
{{ msg.message.messageMark.likeCount }}
-
+
{{ msg.message.messageMark.dislikeCount }}
diff --git a/src/views/Home/components/ChatList/MsgOption/styles.scss b/src/views/Home/components/ChatList/MsgOption/styles.scss
index a3901bc1..6cb69db1 100644
--- a/src/views/Home/components/ChatList/MsgOption/styles.scss
+++ b/src/views/Home/components/ChatList/MsgOption/styles.scss
@@ -28,20 +28,11 @@
background-image: url('@/assets/icons/icon_reply.svg');
}
- .like {
- background-image: url('@/assets/icons/icon_like.svg');
- }
-
.like-active {
- background-image: url('@/assets/icons/icon_liked.svg');
- }
-
- .dislike {
- background-image: url('@/assets/icons/icon_like.svg');
- transform: rotate(180deg);
+ color: #fe2c55;
}
.dislike-active {
- background-image: url('@/assets/icons/icon_liked.svg');
+ color: #bebebe;
}
}
diff --git a/src/views/Home/components/ChatList/index.vue b/src/views/Home/components/ChatList/index.vue
index f87f7a90..5ce91657 100644
--- a/src/views/Home/components/ChatList/index.vue
+++ b/src/views/Home/components/ChatList/index.vue
@@ -1,5 +1,5 @@