fix(im): 批量修复 P1/P2 问题
- 修复管理端消息内容搜索和私聊双向查询 - 加强 RTC 通话并发状态保护,去除重复接口错误提示 - 支持成员永久禁言 - 脱敏群消息 WebSocket 定向收件人字段 - 更新 IM bug 台账,剩余 P1/P2 共 35 个im
parent
8b06efe5ee
commit
f3807e30d5
|
|
@ -63,7 +63,8 @@ const presets = [
|
|||
{ label: '12 小时', value: 43200 },
|
||||
{ label: '1 天', value: 86400 },
|
||||
{ label: '7 天', value: 604800 },
|
||||
{ label: '30 天', value: 2592000 }
|
||||
{ label: '30 天', value: 2592000 },
|
||||
{ label: '永久', value: 0 }
|
||||
]
|
||||
|
||||
/** 打开弹窗 */
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
v-else-if="rtcStore.stage === ImRtcCallStage.INCOMING"
|
||||
:payload="rtcStore.incomingPayload"
|
||||
:is-group="isGroup"
|
||||
:accepting="accepting"
|
||||
:rejecting="rejecting"
|
||||
@accept="handleAccept"
|
||||
@reject="handleReject"
|
||||
/>
|
||||
|
|
@ -42,6 +44,7 @@
|
|||
:local-stream="localStream"
|
||||
:remote-video-stream="remoteVideoStream"
|
||||
:remote-audio-stream="remoteAudioStream"
|
||||
:hanging-up="hangingUp"
|
||||
@hangup="handleHangup"
|
||||
@toggle-mic="toggleMic"
|
||||
@toggle-camera="toggleCamera"
|
||||
|
|
@ -90,6 +93,11 @@ const message = useMessage()
|
|||
const lk = useLiveKitRoom()
|
||||
|
||||
const memberPickerRef = ref<InstanceType<typeof RtcCallMemberPickerDialog>>()
|
||||
const connecting = ref(false)
|
||||
const accepting = ref(false)
|
||||
const rejecting = ref(false)
|
||||
const cancelling = ref(false)
|
||||
const hangingUp = ref(false)
|
||||
|
||||
// ==================== 视图模型 ====================
|
||||
|
||||
|
|
@ -224,17 +232,22 @@ const participants = computed<CallParticipantVM[]>(() => {
|
|||
/** 连入 LiveKit 房间并注册离开回调;INVITING 主叫预连和被叫 accept 后连入共用 */
|
||||
async function connectLiveKit(livekitUrl: string, token: string) {
|
||||
// 幂等:lk.connect 内部进入后就把 room.value 赋值;非空表示已经在连接或已连接;stage 多次切换时重复触发也跳过
|
||||
if (lk.room.value) {
|
||||
if (lk.room.value || connecting.value) {
|
||||
return
|
||||
}
|
||||
// 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪
|
||||
lk.onDisconnected(() => handlePeerDisconnected())
|
||||
lk.onParticipantConnected(maybeEnterRunning)
|
||||
lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId))
|
||||
await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value })
|
||||
// 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING
|
||||
if (lk.remoteParticipants.value.length > 0) {
|
||||
maybeEnterRunning()
|
||||
connecting.value = true
|
||||
try {
|
||||
// 先注册回调,再 connect;信令握手过程会即时推送已在房参与者,业务 handler 必须先就绪
|
||||
lk.onDisconnected(() => handlePeerDisconnected())
|
||||
lk.onParticipantConnected(maybeEnterRunning)
|
||||
lk.onParticipantDisconnected((userId) => rtcStore.markUserLeft(userId))
|
||||
await lk.connect(livekitUrl, token, { audio: true, video: initialCamera.value })
|
||||
// 兜底:connect 期间若已有远端在房,事件可能在 handler 注册前已触发,主动切到 RUNNING
|
||||
if (lk.remoteParticipants.value.length > 0) {
|
||||
maybeEnterRunning()
|
||||
}
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,71 +310,85 @@ watch(
|
|||
|
||||
/** 主叫取消邀请 */
|
||||
async function handleCancel() {
|
||||
const room = rtcStore.call?.room
|
||||
if (room) {
|
||||
try {
|
||||
await cancelCall(room)
|
||||
} catch (e) {
|
||||
console.warn('[Call] cancel 失败', { room }, e)
|
||||
}
|
||||
if (cancelling.value) {
|
||||
return
|
||||
}
|
||||
cancelling.value = true
|
||||
const room = rtcStore.call?.room
|
||||
try {
|
||||
if (room) {
|
||||
await cancelCall(room)
|
||||
}
|
||||
await lk.disconnect()
|
||||
rtcStore.reset()
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
await lk.disconnect()
|
||||
rtcStore.reset()
|
||||
}
|
||||
|
||||
/** 被叫拒绝来电 */
|
||||
async function handleReject() {
|
||||
const payload = rtcStore.incomingPayload
|
||||
if (payload?.room) {
|
||||
try {
|
||||
await rejectCall(payload.room)
|
||||
} catch (e) {
|
||||
console.warn('[Call] reject 失败', { room: payload.room }, e)
|
||||
}
|
||||
// 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op
|
||||
rtcStore.applyParticipantRejected({
|
||||
room: payload.room,
|
||||
conversationType: payload.conversationType,
|
||||
groupId: payload.groupId,
|
||||
operatorUserId: getCurrentUserId()
|
||||
})
|
||||
if (rejecting.value) {
|
||||
return
|
||||
}
|
||||
rejecting.value = true
|
||||
const payload = rtcStore.incomingPayload
|
||||
try {
|
||||
if (payload?.room) {
|
||||
await rejectCall(payload.room)
|
||||
// 本端先行从胶囊条移除自己,免等后端 RTC_CALL(REJECTED) 推回;私聊场景 store 内部 no-op
|
||||
rtcStore.applyParticipantRejected({
|
||||
room: payload.room,
|
||||
conversationType: payload.conversationType,
|
||||
groupId: payload.groupId,
|
||||
operatorUserId: getCurrentUserId()
|
||||
})
|
||||
}
|
||||
rtcStore.reset()
|
||||
} finally {
|
||||
rejecting.value = false
|
||||
}
|
||||
rtcStore.reset()
|
||||
}
|
||||
|
||||
/** 被叫接听来电 */
|
||||
async function handleAccept() {
|
||||
if (accepting.value) {
|
||||
return
|
||||
}
|
||||
const payload = rtcStore.incomingPayload
|
||||
if (!payload) return
|
||||
accepting.value = true
|
||||
try {
|
||||
const data = await acceptCall(payload.room)
|
||||
rtcStore.enterRunning(data)
|
||||
} catch (e: any) {
|
||||
console.error('[Call] accept 失败', { room: payload.room }, e)
|
||||
message.error(e?.msg || '接听失败')
|
||||
rtcStore.reset()
|
||||
} finally {
|
||||
accepting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 通话中挂断 */
|
||||
async function handleHangup() {
|
||||
const call = rtcStore.call
|
||||
if (call?.room) {
|
||||
try {
|
||||
await leaveCall(call.room)
|
||||
} catch (e) {
|
||||
console.warn('[Call] leave 失败', { room: call.room }, e)
|
||||
}
|
||||
// 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉
|
||||
rtcStore.applyParticipantDisconnected({
|
||||
room: call.room,
|
||||
userId: getCurrentUserId(),
|
||||
conversationType: call.conversationType,
|
||||
groupId: call.groupId
|
||||
})
|
||||
if (hangingUp.value) {
|
||||
return
|
||||
}
|
||||
hangingUp.value = true
|
||||
const call = rtcStore.call
|
||||
try {
|
||||
if (call?.room) {
|
||||
await leaveCall(call.room)
|
||||
// 本端先行从胶囊条移除自己,免等后端 RTC_PARTICIPANT_DISCONNECTED 推回;私聊场景 store 内部 no-op,整通话由 END 关掉
|
||||
rtcStore.applyParticipantDisconnected({
|
||||
room: call.room,
|
||||
userId: getCurrentUserId(),
|
||||
conversationType: call.conversationType,
|
||||
groupId: call.groupId
|
||||
})
|
||||
}
|
||||
await lk.disconnect()
|
||||
rtcStore.reset()
|
||||
} finally {
|
||||
hangingUp.value = false
|
||||
}
|
||||
await lk.disconnect()
|
||||
rtcStore.reset()
|
||||
}
|
||||
|
||||
/** LiveKit Room 异常断开;多见于网络中断 */
|
||||
|
|
@ -453,14 +480,9 @@ async function handleAddMemberSuccess(userIds: number[]) {
|
|||
if (!call?.room || userIds.length === 0) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await inviteCall({ room: call.room, inviteeIds: userIds })
|
||||
// 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里
|
||||
rtcStore.appendInvitees(userIds)
|
||||
message.success('已发送邀请')
|
||||
} catch (e: any) {
|
||||
console.error('[Call] invite 追加失败', { room: call.room, inviteeIds: userIds }, e)
|
||||
message.error(e?.msg || '添加成员失败')
|
||||
}
|
||||
await inviteCall({ room: call.room, inviteeIds: userIds })
|
||||
// 同步本地 inviteeIds,让新成员立即作为 pending 占位出现在网格里
|
||||
rtcStore.appendInvitees(userIds)
|
||||
message.success('已发送邀请')
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -53,12 +53,16 @@
|
|||
<div class="flex flex-shrink-0 gap-2 items-center">
|
||||
<button
|
||||
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#f04a4a] hover:opacity-90"
|
||||
:class="{ 'opacity-60 cursor-not-allowed': rejectDisabled }"
|
||||
:disabled="rejectDisabled"
|
||||
@click="$emit('reject')"
|
||||
>
|
||||
<Icon icon="ant-design:phone-outlined" :size="18" class="rotate-[135deg]" />
|
||||
</button>
|
||||
<button
|
||||
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#2ec27e] hover:opacity-90"
|
||||
:class="{ 'opacity-60 cursor-not-allowed': acceptDisabled }"
|
||||
:disabled="acceptDisabled"
|
||||
@click="$emit('accept')"
|
||||
>
|
||||
<Icon icon="ant-design:phone-outlined" :size="18" />
|
||||
|
|
@ -78,6 +82,8 @@ import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
|||
const props = defineProps<{
|
||||
payload: ImRtcCallNotification | null
|
||||
isGroup?: boolean
|
||||
accepting?: boolean
|
||||
rejecting?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{ accept: []; reject: [] }>()
|
||||
|
|
@ -88,6 +94,12 @@ const tipText = computed(() => {
|
|||
return `邀请你${getDictLabel(DICT_TYPE.IM_RTC_CALL_MEDIA_TYPE, props.payload.mediaType)}通话`
|
||||
})
|
||||
|
||||
/** 接听按钮禁用态 */
|
||||
const acceptDisabled = computed(() => !!props.accepting || !!props.rejecting)
|
||||
|
||||
/** 拒绝按钮禁用态 */
|
||||
const rejectDisabled = computed(() => !!props.rejecting || !!props.accepting)
|
||||
|
||||
/** 群通话成员;缓存为空时用 INVITE 载荷里的主叫兜底,避免空白 */
|
||||
const callMembers = useGroupCallMembers(
|
||||
computed(() => (props.isGroup ? props.payload?.groupId : undefined)),
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@
|
|||
</template>
|
||||
<div
|
||||
class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
|
||||
:class="{ 'opacity-60 pointer-events-none': hangingUp }"
|
||||
@click="$emit('hangup')"
|
||||
>
|
||||
<span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-[#f04a4a]">
|
||||
|
|
@ -212,6 +213,7 @@ const props = defineProps<{
|
|||
localStream?: MediaStream | null
|
||||
remoteVideoStream?: MediaStream | null
|
||||
remoteAudioStream?: MediaStream | null
|
||||
hangingUp?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
|
|
|
|||
|
|
@ -166,12 +166,7 @@ async function handleJoin() {
|
|||
return
|
||||
}
|
||||
popoverVisible.value = false
|
||||
try {
|
||||
const data = await joinCall(call.room)
|
||||
rtcStore.startInviting(data)
|
||||
} catch (e: any) {
|
||||
console.error('[GroupCallBanner] join 失败', { room: call.room }, e)
|
||||
message.error(e?.msg || '加入失败')
|
||||
}
|
||||
const data = await joinCall(call.room)
|
||||
rtcStore.startInviting(data)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -622,7 +622,6 @@ function onMutedChange(value: boolean | string | number) {
|
|||
groupStore.setSilent(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationGroupSide] setSilent 失败', { targetId }, error)
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
message.error('操作失败')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -650,13 +649,9 @@ async function onMuteAllChange(value: boolean | string | number) {
|
|||
return
|
||||
}
|
||||
const newValue = !!value
|
||||
try {
|
||||
await muteAll({ groupId: props.group.id, mutedAll: newValue })
|
||||
message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
|
||||
emit('reload')
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
}
|
||||
await muteAll({ groupId: props.group.id, mutedAll: newValue })
|
||||
message.success(newValue ? '已开启全群禁言' : '已关闭全群禁言')
|
||||
emit('reload')
|
||||
}
|
||||
|
||||
// ==================== 进群审批 ====================
|
||||
|
|
|
|||
|
|
@ -58,20 +58,28 @@
|
|||
</div>
|
||||
<div class="flex items-center mt-1 leading-5">
|
||||
<!-- 进群申请红字前缀:群主 / 管理员看到自己管理的群下还有未处理申请时显示 -->
|
||||
<span v-if="requestText" class="flex-shrink-0 text-12px text-[#c70b0b]">
|
||||
<span
|
||||
v-if="requestText"
|
||||
class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
|
||||
>
|
||||
{{ requestText }}
|
||||
</span>
|
||||
<!-- @红字提示:atMe 优先于 atAll -->
|
||||
<span v-if="atText" class="flex-shrink-0 text-12px text-[#c70b0b]">{{ atText }}</span>
|
||||
<span
|
||||
v-if="atText"
|
||||
class="conversation-item__prefix flex-shrink overflow-hidden text-12px text-[#c70b0b] truncate whitespace-nowrap"
|
||||
>
|
||||
{{ atText }}
|
||||
</span>
|
||||
<!-- 群聊最后一条发送者前缀:按 lastSenderId + 当前会话上下文实时算名字 -->
|
||||
<span
|
||||
v-if="showSendName"
|
||||
class="flex-shrink-0 text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap"
|
||||
class="conversation-item__sender flex-shrink overflow-hidden text-12px text-[var(--el-text-color-secondary)] truncate whitespace-nowrap"
|
||||
>
|
||||
{{ lastSenderDisplayName }}:
|
||||
</span>
|
||||
<span
|
||||
class="flex-1 overflow-hidden text-12px truncate text-[var(--el-text-color-secondary)]"
|
||||
class="flex-1 min-w-0 overflow-hidden text-12px truncate text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
{{ lastContentDisplay }}
|
||||
</span>
|
||||
|
|
@ -239,7 +247,6 @@ function handleMuted() {
|
|||
: groupStore.setSilent(targetId, next)
|
||||
sync.catch((e) => {
|
||||
console.error('[IM] 切换免打扰失败', e)
|
||||
message.error('切换免打扰失败')
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
})
|
||||
}
|
||||
|
|
@ -285,4 +292,12 @@ function handleContextMenu(e: MouseEvent) {
|
|||
.conversation-item__silent :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
.conversation-item__prefix {
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.conversation-item__sender {
|
||||
max-width: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ watch(visible, (open) => {
|
|||
}
|
||||
})
|
||||
|
||||
/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示(接口错误由全局拦截器统一 toast,不重复 catch) */
|
||||
/** 备注 popover 点击保存 */
|
||||
async function handleSaveDisplayName() {
|
||||
if (!props.friend) {
|
||||
return
|
||||
|
|
@ -227,7 +227,6 @@ function handleMutedChange(value: boolean | string | number) {
|
|||
}
|
||||
friendStore.setSilent(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
||||
message.error('切换免打扰失败')
|
||||
conversationStore.setSilent(type, targetId, !next)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,28 +280,26 @@ async function onUploadPicked(e: Event) {
|
|||
return
|
||||
}
|
||||
uploading.value = true
|
||||
let payload: { url: string; width: number; height: number }
|
||||
let size: { width: number; height: number }
|
||||
try {
|
||||
size = await probeImageSize(file)
|
||||
} catch (err) {
|
||||
console.warn('[IM] 解析个人表情失败', err)
|
||||
ElMessage.error('图片解析失败')
|
||||
uploading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// probe 本地图片宽高 + 上传到 OSS 并行起跑(probe 通常远快于上传,几乎完全被遮蔽)
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const [size, uploadRes] = await Promise.all([
|
||||
probeImageSize(file),
|
||||
updateFile(form) as Promise<{ data?: string }>
|
||||
])
|
||||
const uploadRes = (await updateFile(form)) as { data?: string }
|
||||
const url = uploadRes?.data
|
||||
if (!url) {
|
||||
ElMessage.error('上传失败')
|
||||
return
|
||||
}
|
||||
payload = { url, width: size.width, height: size.height }
|
||||
} catch (err) {
|
||||
console.warn('[IM] 上传个人表情失败', err)
|
||||
ElMessage.error('上传失败')
|
||||
uploading.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = { url, width: size.width, height: size.height }
|
||||
await faceStore.addFaceUserItem(payload)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
|
|
|
|||
|
|
@ -464,6 +464,7 @@ function toggleSide() {
|
|||
|
||||
/** 私聊通话入口:popover 触发;点 语音 / 视频 直接发起 */
|
||||
const callPopoverVisible = ref(false)
|
||||
const callInviting = ref(false) // 通话发起中
|
||||
async function startPrivateCall(mediaType: number) {
|
||||
callPopoverVisible.value = false
|
||||
const conversation = conversationStore.activeConversation
|
||||
|
|
@ -503,13 +504,21 @@ async function onCallMemberPicked(selectedIds: number[]) {
|
|||
})
|
||||
}
|
||||
|
||||
/** 实际调 create 接口;统一处理成功 / ENDED(如忙线立即结束)/ 异常三种返回 */
|
||||
/** 实际调 create 接口;统一处理成功 / ENDED(如忙线立即结束) */
|
||||
async function doInvite(reqVO: {
|
||||
conversationType: number
|
||||
mediaType: number
|
||||
groupId?: number
|
||||
inviteeIds: number[]
|
||||
}) {
|
||||
if (callInviting.value) {
|
||||
return
|
||||
}
|
||||
if (rtcStore.isActive) {
|
||||
message.warning('当前已有通话')
|
||||
return
|
||||
}
|
||||
callInviting.value = true
|
||||
try {
|
||||
const data = await createCall(reqVO)
|
||||
// 后端已 INSERT + 立即 end(如忙线):toast 提示,不进 INVITING 阶段;chat tip 由 RTC_CALL_END 推送写入消息流
|
||||
|
|
@ -519,8 +528,8 @@ async function doInvite(reqVO: {
|
|||
}
|
||||
// 正常进入 INVITING 阶段:走 store 逻辑发起通话,后续状态更新 / 消息流更新由 RTC 模块监听推送处理
|
||||
rtcStore.startInviting(data)
|
||||
} catch (e: any) {
|
||||
message.error(e?.msg || '发起通话失败')
|
||||
} finally {
|
||||
callInviting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,15 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 「消息内容」搜索入口暂时移除:后端 page 接口没有 content 字段,留着会误导管理员;后端补上后再恢复 -->
|
||||
<el-form-item label="消息内容" prop="content">
|
||||
<el-input
|
||||
v-model="queryParams.content"
|
||||
placeholder="请输入消息内容"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发送时间" prop="sendTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.sendTime"
|
||||
|
|
@ -176,6 +184,7 @@ const queryParams = reactive({
|
|||
groupId: undefined as number | undefined,
|
||||
senderId: undefined as number | undefined,
|
||||
type: undefined as number | undefined,
|
||||
content: undefined as string | undefined,
|
||||
sendTime: [] as string[]
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
|
|
|||
|
|
@ -37,7 +37,15 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 「消息内容」搜索入口暂时移除:后端 page 接口没有 content 字段,留着会误导管理员;后端补上后再恢复 -->
|
||||
<el-form-item label="消息内容" prop="content">
|
||||
<el-input
|
||||
v-model="queryParams.content"
|
||||
placeholder="请输入消息内容"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发送时间" prop="sendTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.sendTime"
|
||||
|
|
@ -151,6 +159,7 @@ const queryParams = reactive({
|
|||
senderId: undefined as number | undefined,
|
||||
receiverId: undefined as number | undefined,
|
||||
type: undefined as number | undefined,
|
||||
content: undefined as string | undefined,
|
||||
sendTime: [] as string[]
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
|
|
|||
Loading…
Reference in New Issue