diff --git a/src/api/im/group/index.ts b/src/api/im/group/index.ts index cd931cd9d..9b353e7a8 100644 --- a/src/api/im/group/index.ts +++ b/src/api/im/group/index.ts @@ -13,7 +13,6 @@ export interface ImGroupRespVO { status: number // 群状态(0=正常,1=已解散) dissolvedTime?: string // 解散时间 createTime?: string // 创建时间 - // TODO @AI:不太对,返回的就是 ImGroupMessageRespVO 数组 pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空) } diff --git a/src/api/im/manager/message/group.ts b/src/api/im/manager/message/group/index.ts similarity index 91% rename from src/api/im/manager/message/group.ts rename to src/api/im/manager/message/group/index.ts index fe89ef36a..a9d16ef89 100644 --- a/src/api/im/manager/message/group.ts +++ b/src/api/im/manager/message/group/index.ts @@ -1,6 +1,5 @@ import request from '@/config/axios' -// TODO @AI:应该是 message/group/xxx,保持和前端一致 export interface ImManagerGroupMessageVO { id: number clientMessageId?: string diff --git a/src/api/im/manager/message/private.ts b/src/api/im/manager/message/private/index.ts similarity index 90% rename from src/api/im/manager/message/private.ts rename to src/api/im/manager/message/private/index.ts index 158e0f700..ed97e3db8 100644 --- a/src/api/im/manager/message/private.ts +++ b/src/api/im/manager/message/private/index.ts @@ -1,6 +1,5 @@ import request from '@/config/axios' -// TODO @AI:应该是 message/group/xxx,保持和前端一致 export interface ImManagerPrivateMessageVO { id: number clientMessageId?: string diff --git a/src/api/im/message/group/index.ts b/src/api/im/message/group/index.ts index 84caab2ca..70e9ebeb1 100644 --- a/src/api/im/message/group/index.ts +++ b/src/api/im/message/group/index.ts @@ -69,5 +69,5 @@ export const getGroupReadUsers = (params: { groupId: number | string messageId: number | string }) => { - return request.get({ url: '/im/message/group/read-users', params }) + return request.get({ url: '/im/message/group/get-read-user-ids', params }) } diff --git a/src/api/im/message/private/index.ts b/src/api/im/message/private/index.ts index df21e2576..c1c53090c 100644 --- a/src/api/im/message/private/index.ts +++ b/src/api/im/message/private/index.ts @@ -38,7 +38,6 @@ export const pullPrivateMessages = (params: { minId: number | string; size: numb } // 查询私聊历史消息 -// TODO @AI:历史消息,是不是通过这个接口? export const getPrivateMessageList = (params: ImPrivateMessageListReqVO) => { return request.get({ url: '/im/message/private/list', params }) } diff --git a/src/utils/domUtils.ts b/src/utils/domUtils.ts index b7bac3b5a..7f236c40f 100644 --- a/src/utils/domUtils.ts +++ b/src/utils/domUtils.ts @@ -288,15 +288,3 @@ export const isInContainer = (el: Element, container: any) => { ) } -// TODO @AI:拿到 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/utils/index.ts;放在 domutils 有点奇怪! -/** HTML 转义函数,防止 XSS */ -export const escapeHtml = (text: string): string => { - const map: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - } - return text.replace(/[&<>"']/g, (char) => map[char]) -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 0bcedb438..692fdeca4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -535,3 +535,15 @@ export const subString = (str: string, start: number, end: number) => { } return str } + +/** HTML 转义函数,防止 XSS */ +export const escapeHtml = (text: string): string => { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + return text.replace(/[&<>"']/g, (char) => map[char]) +} diff --git a/src/views/im/home/components/group/GroupMemberSelector.vue b/src/views/im/home/components/group/GroupMemberSelector.vue index 7221405d3..4ea1e4a63 100644 --- a/src/views/im/home/components/group/GroupMemberSelector.vue +++ b/src/views/im/home/components/group/GroupMemberSelector.vue @@ -128,12 +128,11 @@ watch( /** 重建工作副本:把 checkedIds / lockedIds / hideIds 翻译成每个 member 的 flag */ function rebuild() { - // TODO @AI:member 不要缩写成 m; - workingMembers.value = props.members.map((m) => ({ - ...m, - checked: props.checkedIds.some((id) => id === m.userId), - locked: props.lockedIds.some((id) => id === m.userId), - hide: props.hideIds.some((id) => id === m.userId) + workingMembers.value = props.members.map((member) => ({ + ...member, + checked: props.checkedIds.some((id) => id === member.userId), + locked: props.lockedIds.some((id) => id === member.userId), + hide: props.hideIds.some((id) => id === member.userId) })) } @@ -148,32 +147,28 @@ const showMembers = computed(() => ) /** 已勾选成员:右侧宫格预览 + complete 抛参 */ -const checkedMembers = computed(() => workingMembers.value.filter((m) => m.checked)) +const checkedMembers = computed(() => workingMembers.value.filter((member) => member.checked)) /** 落勾选并校验上限:超过 maxSize 时自动取消并提示,避免出现"勾上但实际不算"的中间态 */ -// TODO @AI:member? -// TODO @AI:val 改成 checked? -function applyCheck(m: GroupMemberFlag, val: boolean) { - m.checked = val +function applyCheck(member: GroupMemberFlag, checked: boolean) { + member.checked = checked if (props.maxSize > 0 && checkedMembers.value.length > props.maxSize) { message.error(`最多选择 ${props.maxSize} 位成员`) - m.checked = false + member.checked = false } } /** 行点击:切换勾选态,locked 的不响应 */ -// TODO @AI:member? -function handleToggleCheck(m: GroupMemberFlag) { - if (m.locked) { +function handleToggleCheck(member: GroupMemberFlag) { + if (member.locked) { return } - applyCheck(m, !m.checked) + applyCheck(member, !member.checked) } -/** checkbox change:直接落 val(locked 已由 disabled 拦截) */ -// TODO @AI:member? -function handleCheckChange(m: GroupMemberFlag, val: boolean) { - applyCheck(m, val) +/** checkbox change:直接落 checked(locked 已由 disabled 拦截) */ +function handleCheckChange(member: GroupMemberFlag, checked: boolean) { + applyCheck(member, checked) } /** 确定:把已勾选成员通过 complete 抛给父侧 */ diff --git a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue index e9f8569d3..17d9b8fe7 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageHistory.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageHistory.vue @@ -219,8 +219,8 @@ @click="openFile(fileOf(message)?.url)" > @@ -320,6 +320,7 @@ import { ImConversationType, ImMessageType, isGroupNotification } from '@/views/ import { parseMessage, resolveTipText, + getFileIconInfo, type TextMessage, type ImageMessage, type FileMessage, @@ -686,42 +687,6 @@ function textSnippetOf(message: Message): string { } } -/** - * 文件类型图标 + 配色(按扩展名分发,与 MessageItem 同款逻辑) - * - * TODO @AI:MessageItem 也有一份完全相同的实现,下次顺手抽到 utils/message.ts 里去重 - */ -function getFileIcon(name: string): { icon: string; color: string } { - const ext = name.split('.').pop()?.toLowerCase() || '' - if (ext === 'pdf') { - return { icon: 'ant-design:file-pdf-filled', color: '#ed5757' } - } - if (['doc', 'docx'].includes(ext)) { - return { icon: 'ant-design:file-word-filled', color: '#2b7cd3' } - } - if (['xls', 'xlsx'].includes(ext)) { - return { icon: 'ant-design:file-excel-filled', color: '#1f7244' } - } - if (['ppt', 'pptx'].includes(ext)) { - return { icon: 'ant-design:file-ppt-filled', color: '#d24726' } - } - if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) { - return { icon: 'ant-design:file-zip-filled', color: '#f0ad4e' } - } - if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) { - return { icon: 'ant-design:file-image-filled', color: '#9c27b0' } - } - if (['mp4', 'mov', 'avi', 'mkv', 'wmv', 'flv'].includes(ext)) { - return { icon: 'ant-design:video-camera-filled', color: '#9c27b0' } - } - if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) { - return { icon: 'ant-design:audio-filled', color: '#9c27b0' } - } - if (['txt', 'md', 'log', 'json', 'xml'].includes(ext)) { - return { icon: 'ant-design:file-text-filled', color: '#909399' } - } - return { icon: 'ant-design:file-filled', color: '#909399' } -} /** 文件点击 → 新窗口打开下载 */ function openFile(url?: string) { diff --git a/src/views/im/home/pages/conversation/components/message/MessageItem.vue b/src/views/im/home/pages/conversation/components/message/MessageItem.vue index 11f1a5b5c..57a8896df 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageItem.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageItem.vue @@ -267,8 +267,6 @@ import ReplyPreview from './ReplyPreview.vue' import UserAvatar from '../../../../components/user/UserAvatar.vue' import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue' -// TODO @AI:参考 /Users/yunai/Java/yudao-all-in-im/yudao-ui-admin-vue3/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue 做下分块? - defineOptions({ name: 'ImMessageItem' }) const props = defineProps<{ @@ -280,6 +278,8 @@ const emit = defineEmits<{ locate: [messageId: number] }>() +// ==================== Stores / Hooks ==================== + const userStore = useUserStore() const conversationStore = useConversationStore() const groupStore = useGroupStore() @@ -290,6 +290,8 @@ const { recall, sendRaw } = useMessageSender() // 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys) const { confirm: confirmDialog, success: successMessage } = useMessage() +// ==================== 消息类型判断 ==================== + /** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */ const isRecall = computed(() => props.message.type === ImMessageType.RECALL) @@ -362,6 +364,8 @@ function formatTipTime(timestamp: number): string { return `${pad(messageDate.getMonth() + 1)}-${pad(messageDate.getDate())} ${hourMinute}` } +// ==================== 消息内容解析 / payload ==================== + /** 文本内容 */ const textContent = computed(() => parseMessage(props.message.content)?.content ?? '') @@ -427,6 +431,8 @@ onBeforeUnmount(() => { voicePlaying.value = false }) +// ==================== 发送人 / 已读 / @ ==================== + // 撤回文案:buildRecallTip 实时算 sender 名(按 conversation 上下文走 WeChat 优先级) const recallTip = computed(() => { const conversation = conversationStore.activeConversation @@ -518,6 +524,8 @@ const isAtMe = computed(() => { return (props.message.atUserIds || []).includes(myId) }) +// ==================== 右键菜单 / 操作 ==================== + /** 右键菜单 key 常量;push 端和分发端从同一处取,typo 编译期就能抓 */ const MENU_KEYS = { REPLY: 'REPLY', @@ -630,7 +638,8 @@ const canPin = computed( isNormalMessage(props.message.type) && !!props.message.id && !isRecall.value && - (myGroupRole.value === ImGroupMemberRole.OWNER || myGroupRole.value === ImGroupMemberRole.ADMIN) && + (myGroupRole.value === ImGroupMemberRole.OWNER || + myGroupRole.value === ImGroupMemberRole.ADMIN) && !currentGroup.value.pinnedMessages?.some((m) => m.id === props.message.id) ) @@ -641,6 +650,7 @@ async function handlePin() { return } try { + // TODO @AI:这个会不会有问题 TS2554: Expected 1-2 arguments, but got 3 await confirmDialog('将在当前群成员的聊天中置顶', '置顶消息', { confirmButtonText: '置顶' }) await apiPinGroupMessage({ groupId: group.id, messageId: props.message.id }) successMessage('已置顶') diff --git a/src/views/im/home/store/groupStore.ts b/src/views/im/home/store/groupStore.ts index 77b350346..612d0d726 100644 --- a/src/views/im/home/store/groupStore.ts +++ b/src/views/im/home/store/groupStore.ts @@ -406,21 +406,19 @@ export const useGroupStore = defineStore('imGroupStore', { if (!group?.members?.length) { return } - // TODO @AI:计算是否更新?【优化注释】 + // 命中目标且角色已变化才标记 changed,避免无变化时整数组重建触发响应式 const idSet = new Set(userIds) let changed = false - // TODO @AI:newMembers 更合适? - // TODO @AI:m 是不是改成 member; - const next = group.members.map((m) => { - if (!idSet.has(m.userId) || m.role === role) { - return m + const newMembers = group.members.map((member) => { + if (!idSet.has(member.userId) || member.role === role) { + return member } changed = true - return { ...m, role } + return { ...member, role } }) - // TODO @AI:有更新则进行替换,补充下注释【优化注释】 + // 有变化才整组替换,让响应式只在真有更新时通知下游 if (changed) { - group.members = next + group.members = newMembers } }, @@ -644,28 +642,23 @@ export const useGroupStore = defineStore('imGroupStore', { if (!group) { return } - // TODO @AI:可以直接使用 payload,简化这样设置。。。 - const message: Message = { - id: payload.message.id, + // 幂等:已存在同 messageId 不重复 push + const existing = group.pinnedMessages || [] + // TODO @AI:TS18048: payload.message is possibly undefined + if (existing.some((m) => m.id === payload.message.id)) { + return + } + // payload.message 字段名跟 Message 一致(仅 sendTime 需要从 ISO 串转毫秒戳,selfSend 按当前用户算) + const newMessage: Message = { + ...payload.message, clientMessageId: payload.message.clientMessageId || '', - type: payload.message.type, - content: payload.message.content, - status: payload.message.status, sendTime: new Date(payload.message.sendTime).getTime(), - senderId: payload.message.senderId, targetId: payload.message.groupId, selfSend: payload.message.senderId === getCurrentUserId(), atUserIds: payload.message.atUserIds || [], - receiverUserIds: payload.message.receiverUserIds || [], - receiptStatus: payload.message.receiptStatus, - readCount: payload.message.readCount + receiverUserIds: payload.message.receiverUserIds || [] } - // 幂等:已存在同 messageId 不重复 push - const existing = group.pinnedMessages || [] - if (existing.some((m) => m.id === message.id)) { - return - } - group.pinnedMessages = [...existing, message] + group.pinnedMessages = [...existing, newMessage] this.saveGroups() }, @@ -678,12 +671,11 @@ export const useGroupStore = defineStore('imGroupStore', { if (!group?.pinnedMessages?.length) { return } - // TODO @AI:不要用 next 这样的单词,大家不好理解。可以用 newXXXX 这样。其它地方也看看。 - const next = group.pinnedMessages.filter((m) => m.id !== payload.messageId) - if (next.length === group.pinnedMessages.length) { + const newPinnedMessages = group.pinnedMessages.filter((m) => m.id !== payload.messageId) + if (newPinnedMessages.length === group.pinnedMessages.length) { return } - group.pinnedMessages = next + group.pinnedMessages = newPinnedMessages this.saveGroups() }, diff --git a/src/views/mes/wm/barcode/components/BarcodeDetail.vue b/src/views/mes/wm/barcode/components/BarcodeDetail.vue index 1657956e1..5ff8c0dfa 100644 --- a/src/views/mes/wm/barcode/components/BarcodeDetail.vue +++ b/src/views/mes/wm/barcode/components/BarcodeDetail.vue @@ -79,7 +79,7 @@ import { ref } from 'vue' import { useMessage } from '@/hooks/web/useMessage' import { DICT_TYPE } from '@/utils/dict' import { formatDate } from '@/utils/formatTime' -import { escapeHtml } from '@/utils/domUtils' +import { escapeHtml } from '@/utils' import download from '@/utils/download' import Barcode from './Barcode.vue' import { WmBarcodeApi, type WmBarcodeVO } from '@/api/mes/wm/barcode'