Skip to content

Commit 219c388

Browse files
committed
feat: 支持粘贴图片,修复更改个人名称和修改徽章,消息列表个人信息未更新,支持图片消息复制。
1 parent 2336abc commit 219c388

File tree

9 files changed

+173
-17
lines changed

9 files changed

+173
-17
lines changed

.eslintrc.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ module.exports = {
2828
'@typescript-eslint/no-explicit-any': 0, // 允许any类型
2929
'no-param-reassign': 0, // 允许修改函数参数
3030
'prefer-regex-literals': 0, // 允许使用new RegExp
31+
'no-unused-vars': 2, // 禁止未使用的变量
3132
},
3233
}

src/components/UserSettingBox/index.vue

+19-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useRequest } from 'alova'
44
import { ElMessage } from 'element-plus'
55
import { Select, CloseBold, EditPen } from '@element-plus/icons-vue'
66
import { useUserStore } from '@/stores/user'
7+
import { useCachedStore } from '@/stores/cached'
78
import { SexEnum, IsYetEnum } from '@/enums'
89
import type { BadgeType } from '@/services/types'
910
import apis from '@/services/apis'
@@ -30,6 +31,7 @@ const editName = reactive({
3031
})
3132
3233
const userStore = useUserStore()
34+
const cachedStore = useCachedStore()
3335
3436
const userInfo = computed(() => userStore.userInfo)
3537
const { send: handlerGetBadgeList, data: badgeList } = useRequest(apis.getBadgeList, {
@@ -47,12 +49,21 @@ const currentBadge = computed(() =>
4749
badgeList.value.find((item) => item.obtain === IsYetEnum.YES && item.wearing === IsYetEnum.YES),
4850
)
4951
52+
// 更新缓存里面的用户信息
53+
const updateCurrentUserCache = (key: 'name' | 'wearingItemId', value: any) => {
54+
const currentUser = userStore.userInfo.uid && cachedStore.userCachedList[userStore.userInfo.uid]
55+
if (currentUser) {
56+
currentUser[key] = value // 更新缓存里面的用户信息
57+
}
58+
}
59+
5060
// 佩戴卸下徽章
5161
const toggleWarningBadge = async (badge: BadgeType) => {
5262
if (!badge?.id) return
5363
await apis.setUserBadge(badge.id).send()
5464
handlerGetBadgeList()
5565
badge.img && (userInfo.value.badge = badge.img)
66+
updateCurrentUserCache('wearingItemId', badge.id) // 更新缓存里面的用户徽章
5667
}
5768
5869
// 编辑用户名
@@ -73,13 +84,15 @@ const onSaveUserName = async () => {
7384
return
7485
}
7586
editName.saving = true
76-
await apis.modifyUserName(editName.tempName).send()
77-
userStore.userInfo.name = editName.tempName
78-
editName.saving = false
79-
editName.isEdit = false
80-
editName.tempName = ''
87+
88+
await apis.modifyUserName(editName.tempName).send() // 更改用户名
89+
userStore.userInfo.name = editName.tempName // 更新用户信息里面的用户名
90+
updateCurrentUserCache('name', editName.tempName) // 更新缓存里面的用户信息
91+
// 重置状态
92+
onCancelEditName()
93+
// 没有更名机会就不走下去
8194
if (!userInfo.value?.modifyNameChance || userInfo.value.modifyNameChance === 0) return
82-
userInfo.value.modifyNameChance = userInfo.value?.modifyNameChance - 1
95+
userInfo.value.modifyNameChance = userInfo.value?.modifyNameChance - 1 // 减少更名次数
8396
}
8497
// 确认保存用户名
8598
const onCancelEditName = async () => {

src/utils/copy.ts

+27
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,30 @@ export const copyToClip = (text: string) => {
1515
}
1616
})
1717
}
18+
19+
export const handleCopyImg = (imgUrl: string) => {
20+
const canvas = document.createElement('canvas')
21+
const ctx = canvas.getContext('2d')
22+
const img = new Image()
23+
img.crossOrigin = 'Anonymous'
24+
img.src = imgUrl
25+
img.onload = () => {
26+
if (!ctx) return
27+
canvas.width = img.width
28+
canvas.height = img.height
29+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
30+
ctx.drawImage(img, 0, 0) // 将canvas转为blob
31+
canvas.toBlob(async (blob) => {
32+
if (!blob) return
33+
const data = [new ClipboardItem({ [blob.type]: blob })] // https://w3c.github.io/clipboard-apis/#dom-clipboard-write
34+
try {
35+
await navigator.clipboard.write(data)
36+
// console.log('Copied to clipboard successfully!')
37+
} catch (error) {
38+
// console.error('Unable to write to clipboard.')
39+
} finally {
40+
canvas.remove()
41+
}
42+
})
43+
}
44+
}

src/views/Home/components/ChatBox/MsgInput/index.vue

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
// 艾特功能参考自 https://github.com/MrHGJ/at-mentions
3-
import { ref, reactive, toRefs, watch, watchEffect, type StyleValue, inject, provide } from 'vue'
3+
import { ref, reactive, toRefs, watch, watchEffect, type StyleValue, inject } from 'vue'
44
import type { IMention, INode } from './types'
55
import type { CacheUserItem } from '@/services/types'
66
import { NodeType } from './types'
@@ -14,6 +14,7 @@ import {
1414
import { useCachedStore } from '@/stores/cached'
1515
import VirtualList from '@/components/VirtualList'
1616
import MentionItem from './item.vue'
17+
import PasteImageDialog from '../PasteImageDialog/index.vue'
1718
1819
// 关闭透传 attrs 到组件根节点,传递到子节点 v-bind="$attrs"
1920
defineOptions({ inheritAttrs: false })
@@ -353,7 +354,6 @@ const onInputKeyUp = (e: KeyboardEvent) => {
353354
354355
const handleArrow = (direction: 'up' | 'down') => {
355356
if (!scrollRef.value) return
356-
console.log(scrollRef.value.getOffset(), scrollRef.value.getClientSize())
357357
358358
let newIndex = 0
359359
if (direction === 'up') {
@@ -518,6 +518,8 @@ const onSelectPerson = (uid: number, ignore = false) => {
518518
519519
// 暴露 ref 属性
520520
defineExpose({ input: editorRef, range: editorRange, onSelectPerson })
521+
522+
const getKey = (item: CacheUserItem) => item.uid
521523
</script>
522524

523525
<template>
@@ -553,11 +555,13 @@ defineExpose({ input: editorRef, range: editorRange, onSelectPerson })
553555
dataPropName="item"
554556
:itemProps="{ activeIndex, onSelect: onSelectPerson }"
555557
:data="personList"
556-
data-key="uid"
558+
:data-key="getKey"
557559
:item="MentionItem"
558560
:size="20"
559561
/>
560562
</div>
563+
564+
<PasteImageDialog />
561565
</div>
562566
</template>
563567

src/views/Home/components/ChatBox/MsgInput/item.vue

-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ const props = defineProps({
1717
onSelect: Function,
1818
})
1919
20-
const emit = defineEmits(['select'])
21-
2220
const onClick = () => {
2321
props.onSelect?.(props.item)
2422
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<script setup lang="ts">
2+
import { ref, watchEffect, inject } from 'vue'
3+
import { useEventListener } from '@vueuse/core'
4+
import { MsgEnum } from '@/enums'
5+
import { useUserStore } from '@/stores/user'
6+
7+
const imageBody = ref({ url: '' })
8+
const pasteFile = ref<File>() // 记录input文本内容
9+
const visible = ref(false)
10+
const userStore = useUserStore()
11+
12+
const onChangeMsgType = inject<(msgType: MsgEnum) => void>('onChangeMsgType')
13+
const onChangeFile = inject<(file: File[]) => void>('onChangeFile')
14+
15+
useEventListener(window, 'paste', (e) => {
16+
e.preventDefault()
17+
if (!userStore.isSign) return false // 未登录不支持粘贴图片交互
18+
if (e.clipboardData && e.clipboardData.files?.length) {
19+
const file = e.clipboardData.files[0]
20+
// TODO 可支持粘贴文件。
21+
if (file?.type.includes('image')) {
22+
pasteFile.value = file
23+
}
24+
}
25+
return false
26+
})
27+
28+
watchEffect(() => {
29+
if (pasteFile?.value) {
30+
visible.value = true
31+
imageBody.value = {
32+
url: URL.createObjectURL(pasteFile?.value),
33+
}
34+
} else {
35+
visible.value = false
36+
}
37+
})
38+
const onSend = async () => {
39+
if (!pasteFile?.value) return
40+
// FIXME 如下逻辑可以尝试抽为 hook
41+
onChangeMsgType?.(MsgEnum.IMAGE) // 设置上传类型为图片
42+
await onChangeFile?.([pasteFile?.value]) // 上传文件并发送消息
43+
visible.value = false // 关闭弹窗
44+
URL.revokeObjectURL(imageBody.value.url)
45+
pasteFile.value = undefined
46+
imageBody.value = {
47+
url: '',
48+
}
49+
}
50+
</script>
51+
52+
<template>
53+
<ElDialog
54+
class="image-paste-modal"
55+
title="粘贴图片"
56+
v-model="visible"
57+
:close-on-click-modal="false"
58+
center
59+
>
60+
<img v-if="imageBody.url" :src="imageBody.url" />
61+
62+
<template #footer>
63+
<span class="dialog-footer">
64+
<el-button @click="visible = false">取消</el-button>
65+
<el-button type="primary" @click="onSend"> 发送 </el-button>
66+
</span>
67+
</template>
68+
</ElDialog>
69+
</template>
70+
71+
<style lang="scss" src="./styles.scss" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.image-paste-modal {
2+
display: flex;
3+
flex-direction: column;
4+
width: 600px;
5+
max-height: 80vh;
6+
overflow: hidden;
7+
text-align: center;
8+
9+
.el-dialog__body {
10+
flex: 1;
11+
max-height: 100%;
12+
overflow-y: auto;
13+
}
14+
15+
img {
16+
max-width: 100%;
17+
max-height: 100%;
18+
}
19+
}

src/views/Home/components/ChatBox/index.vue

+12-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const onSelectPerson = (uid: number, ignoreCheck?: boolean) => {
5555
provide('focusMsgInput', focusMsgInput)
5656
provide('onSelectPerson', onSelectPerson)
5757
58+
// 发送消息
5859
const send = (msgType: MsgEnum, body: any, roomId = 1) => {
5960
apis
6061
.sendMsg({ roomId, msgType, body })
@@ -160,17 +161,26 @@ const openFileSelect = (fileType: string) => {
160161
open()
161162
}
162163
163-
onChange((files) => {
164+
const selectAndUploadFile = async (files?: FileList | null) => {
164165
if (!files?.length) return
165166
const file = files[0]
166167
if (nowMsgType.value === MsgEnum.IMAGE) {
167168
if (!file.type.includes('image')) {
168169
return ElMessage.error('请选择图片文件')
169170
}
170171
}
171-
uploadFile(file)
172+
await uploadFile(file)
173+
}
174+
175+
// 选中文件上传并发送消息
176+
provide('onChangeFile', selectAndUploadFile)
177+
// 设置消息类型
178+
provide('onChangeMsgType', (msgType: MsgEnum) => {
179+
nowMsgType.value = msgType
172180
})
173181
182+
onChange(selectAndUploadFile)
183+
174184
onStart(() => {
175185
if (!fileInfo.value) return
176186

src/views/Home/components/ChatList/ContextMenu/index.vue

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { computed, type PropType, inject } from 'vue'
3+
import { ElMessage } from 'element-plus'
34
import apis from '@/services/apis'
45
import {
56
ContextMenu,
@@ -8,7 +9,7 @@ import {
89
type MenuOptions,
910
} from '@imengyu/vue3-context-menu'
1011
import { useUserStore } from '@/stores/user'
11-
import { copyToClip } from '@/utils/copy'
12+
import { copyToClip, handleCopyImg } from '@/utils/copy'
1213
import { useChatStore } from '@/stores/chat'
1314
import type { MessageType } from '@/services/types'
1415
import { MsgEnum, PowerEnum } from '@/enums'
@@ -52,8 +53,16 @@ const onBlockUser = async () => {
5253
5354
// 拷贝内容-(此版本未针对不同Body体进行处理)
5455
const copyContent = () => {
55-
const content = props.msg.message.body?.content
56-
copyToClip(content)
56+
const msg = props.msg.message
57+
if (msg.type === MsgEnum.TEXT) {
58+
const content = msg.body?.content
59+
copyToClip(content)
60+
ElMessage.success('复制成功~')
61+
}
62+
if (msg.type === MsgEnum.IMAGE) {
63+
handleCopyImg(msg.body.url)
64+
ElMessage.success('复制成功~')
65+
}
5766
}
5867
5968
// 下载
@@ -83,7 +92,11 @@ const onDelete = () => chatStore.deleteMsg(props.msg.message.id)
8392
<ContextMenuItem label="艾特Ta" @click="onAtUser?.(msg.fromUser.uid, true)" v-login-show>
8493
<template #icon> <span class="icon">@</span> </template>
8594
</ContextMenuItem>
86-
<ContextMenuItem v-if="msg.message.type === MsgEnum.TEXT" label="复制" @click="copyContent">
95+
<ContextMenuItem
96+
v-if="msg.message.type === MsgEnum.TEXT || msg.message.type === MsgEnum.IMAGE"
97+
label="复制"
98+
@click="copyContent"
99+
>
87100
<template #icon>
88101
<Icon icon="copy" :size="13" />
89102
</template>

0 commit comments

Comments
 (0)