可以快速删除折叠消息
This commit is contained in:
@@ -51,10 +51,11 @@ const currentMapZoom = ref(2)
|
|||||||
const mapReportsLoading = ref(false)
|
const mapReportsLoading = ref(false)
|
||||||
const mapReportTotal = ref(0)
|
const mapReportTotal = ref(0)
|
||||||
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
|
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
|
||||||
type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: TextMessage }
|
type DeletableTextMessage = TextMessage & { mergedCount?: number; mergedMessages?: TextMessage[] }
|
||||||
|
type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: DeletableTextMessage }
|
||||||
type NodeActionPayload = NodeActionRequest & { reason: string }
|
type NodeActionPayload = NodeActionRequest & { reason: string }
|
||||||
type PendingDeleteAction =
|
type PendingDeleteAction =
|
||||||
| { kind: 'delete-message'; message: TextMessage }
|
| { kind: 'delete-message'; message: DeletableTextMessage }
|
||||||
| { kind: 'delete-node'; nodeId: string }
|
| { kind: 'delete-node'; nodeId: string }
|
||||||
| ({ kind: 'delete-and-block-node' } & NodeActionRequest)
|
| ({ kind: 'delete-and-block-node' } & NodeActionRequest)
|
||||||
let refreshTimer: number | undefined
|
let refreshTimer: number | undefined
|
||||||
@@ -122,14 +123,21 @@ const deleteModalMessage = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
if (action.kind === 'delete-message') {
|
if (action.kind === 'delete-message') {
|
||||||
return '确定要删除这条聊天消息吗?此操作不可撤销。'
|
const count = deleteMessageCount(action.message)
|
||||||
|
return count > 1
|
||||||
|
? `确定要删除这组已合并的 ${count} 条聊天消息吗?此操作不可撤销。`
|
||||||
|
: '确定要删除这条聊天消息吗?此操作不可撤销。'
|
||||||
}
|
}
|
||||||
if (action.kind === 'delete-node') {
|
if (action.kind === 'delete-node') {
|
||||||
return '确定要删除这个节点吗?此操作不可撤销。'
|
return '确定要删除这个节点吗?此操作不可撤销。'
|
||||||
}
|
}
|
||||||
return action.message
|
if (!action.message) {
|
||||||
? '确定要删除这条聊天消息并屏蔽该节点吗?请输入屏蔽原因。'
|
return '确定要删除并屏蔽这个节点吗?请输入屏蔽原因。'
|
||||||
: '确定要删除并屏蔽这个节点吗?请输入屏蔽原因。'
|
}
|
||||||
|
const count = deleteMessageCount(action.message)
|
||||||
|
return count > 1
|
||||||
|
? `确定要删除这组已合并的 ${count} 条聊天消息并屏蔽该节点吗?请输入屏蔽原因。`
|
||||||
|
: '确定要删除这条聊天消息并屏蔽该节点吗?请输入屏蔽原因。'
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteModalConfirmText = computed(() => {
|
const deleteModalConfirmText = computed(() => {
|
||||||
@@ -158,6 +166,15 @@ function mergeMessages(existing: TextMessage[], incoming: TextMessage[]): TextMe
|
|||||||
return Array.from(byId.values()).sort(compareMessages)
|
return Array.from(byId.values()).sort(compareMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function messagesForDelete(message: DeletableTextMessage): TextMessage[] {
|
||||||
|
const items = message.mergedMessages?.length ? message.mergedMessages : [message]
|
||||||
|
return Array.from(new Map(items.map((item) => [item.id, item])).values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMessageCount(message: DeletableTextMessage): number {
|
||||||
|
return messagesForDelete(message).length
|
||||||
|
}
|
||||||
|
|
||||||
function isSameJSON(left: unknown, right: unknown): boolean {
|
function isSameJSON(left: unknown, right: unknown): boolean {
|
||||||
return JSON.stringify(left) === JSON.stringify(right)
|
return JSON.stringify(left) === JSON.stringify(right)
|
||||||
}
|
}
|
||||||
@@ -295,7 +312,7 @@ async function logoutAdmin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestDeleteMessage(message: TextMessage) {
|
function requestDeleteMessage(message: DeletableTextMessage) {
|
||||||
pendingDeleteAction.value = { kind: 'delete-message', message }
|
pendingDeleteAction.value = { kind: 'delete-message', message }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,10 +357,9 @@ async function confirmDeleteModal(payload: { reason?: string }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessage(message: TextMessage) {
|
async function deleteMessage(message: DeletableTextMessage) {
|
||||||
try {
|
try {
|
||||||
await deleteTextMessage(message.id)
|
await deleteMessagesFromLocalState(message)
|
||||||
messages.value = messages.value.filter((item) => item.id !== message.id)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : String(err)
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
}
|
}
|
||||||
@@ -371,6 +387,32 @@ function isMessageNotFoundError(err: unknown): boolean {
|
|||||||
return err instanceof Error && err.message === 'message not found'
|
return err instanceof Error && err.message === 'message not found'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessagesFromLocalState(message: DeletableTextMessage) {
|
||||||
|
const items = messagesForDelete(message)
|
||||||
|
const removableIds = new Set<number>()
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
await Promise.all(items.map(async (item) => {
|
||||||
|
try {
|
||||||
|
await deleteTextMessage(item.id)
|
||||||
|
removableIds.add(item.id)
|
||||||
|
} catch (err) {
|
||||||
|
if (isMessageNotFoundError(err)) {
|
||||||
|
removableIds.add(item.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.push(err instanceof Error ? err.message : String(err))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (removableIds.size > 0) {
|
||||||
|
messages.value = messages.value.filter((item) => !removableIds.has(item.id))
|
||||||
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`部分消息删除失败(${errors.length}/${items.length}):${errors[0]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteNodeById(nodeId: string) {
|
async function deleteNodeById(nodeId: string) {
|
||||||
try {
|
try {
|
||||||
await deleteNode(nodeId)
|
await deleteNode(nodeId)
|
||||||
@@ -383,14 +425,7 @@ async function deleteNodeById(nodeId: string) {
|
|||||||
async function deleteAndBlockNode(payload: NodeActionPayload) {
|
async function deleteAndBlockNode(payload: NodeActionPayload) {
|
||||||
try {
|
try {
|
||||||
if (payload.message) {
|
if (payload.message) {
|
||||||
try {
|
await deleteMessagesFromLocalState(payload.message)
|
||||||
await deleteTextMessage(payload.message.id)
|
|
||||||
} catch (err) {
|
|
||||||
if (!isMessageNotFoundError(err)) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
messages.value = messages.value.filter((item) => item.id !== payload.message?.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ const props = defineProps<{
|
|||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
type GroupedTextMessage = TextMessage & { mergedCount: number; mergedMessages: TextMessage[] }
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'select-node': [nodeId: string]
|
'select-node': [nodeId: string]
|
||||||
'load-older': []
|
'load-older': []
|
||||||
'delete-message': [message: TextMessage]
|
'delete-message': [message: GroupedTextMessage]
|
||||||
'delete-and-block-node': [payload: { nodeId: string; nodeNum: number | null; message: TextMessage }]
|
'delete-and-block-node': [payload: { nodeId: string; nodeNum: number | null; message: GroupedTextMessage }]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const panelRef = ref<HTMLElement | null>(null)
|
const panelRef = ref<HTMLElement | null>(null)
|
||||||
const menuMessage = ref<TextMessage | null>(null)
|
const menuMessage = ref<GroupedTextMessage | null>(null)
|
||||||
const menuX = ref(0)
|
const menuX = ref(0)
|
||||||
const menuY = ref(0)
|
const menuY = ref(0)
|
||||||
const topThreshold = 8
|
const topThreshold = 8
|
||||||
const bottomThreshold = 40
|
const bottomThreshold = 40
|
||||||
|
|
||||||
type GroupedTextMessage = TextMessage & { mergedCount: number }
|
|
||||||
|
|
||||||
const groupedMessages = computed<GroupedTextMessage[]>(() => {
|
const groupedMessages = computed<GroupedTextMessage[]>(() => {
|
||||||
const groups = new Map<string, GroupedTextMessage>()
|
const groups = new Map<string, GroupedTextMessage>()
|
||||||
for (const message of props.messages) {
|
for (const message of props.messages) {
|
||||||
@@ -34,8 +34,9 @@ const groupedMessages = computed<GroupedTextMessage[]>(() => {
|
|||||||
const group = groups.get(key)
|
const group = groups.get(key)
|
||||||
if (group) {
|
if (group) {
|
||||||
group.mergedCount += 1
|
group.mergedCount += 1
|
||||||
|
group.mergedMessages.push(message)
|
||||||
} else {
|
} else {
|
||||||
groups.set(key, { ...message, mergedCount: 1 })
|
groups.set(key, { ...message, mergedCount: 1, mergedMessages: [message] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(groups.values())
|
return Array.from(groups.values())
|
||||||
@@ -74,7 +75,7 @@ function nodeDetailHref(nodeId: string): string {
|
|||||||
return `/detailed/${encodeURIComponent(nodeId)}`
|
return `/detailed/${encodeURIComponent(nodeId)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMessageMenu(message: TextMessage, event: MouseEvent) {
|
function openMessageMenu(message: GroupedTextMessage, event: MouseEvent) {
|
||||||
emit('select-node', message.from_id)
|
emit('select-node', message.from_id)
|
||||||
menuMessage.value = message
|
menuMessage.value = message
|
||||||
menuX.value = event.clientX
|
menuX.value = event.clientX
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ const chatHasMore = ref(true)
|
|||||||
const error = ref('')
|
const error = ref('')
|
||||||
const chatPageSize = 20
|
const chatPageSize = 20
|
||||||
const chatHistoryRef = ref<HTMLElement | null>(null)
|
const chatHistoryRef = ref<HTMLElement | null>(null)
|
||||||
|
type GroupedTextMessage = TextMessage & { mergedCount: number; mergedMessages: TextMessage[] }
|
||||||
type PendingDeleteAction =
|
type PendingDeleteAction =
|
||||||
| { kind: 'delete-message'; message: TextMessage }
|
| { kind: 'delete-message'; message: GroupedTextMessage }
|
||||||
| { kind: 'delete-and-block-node'; message: TextMessage; nodeId: string; nodeNum: number | null }
|
| { kind: 'delete-and-block-node'; message: GroupedTextMessage; nodeId: string; nodeNum: number | null }
|
||||||
|
|
||||||
type GroupedTextMessage = TextMessage & { mergedCount: number }
|
const menuMessage = ref<GroupedTextMessage | null>(null)
|
||||||
|
|
||||||
const menuMessage = ref<TextMessage | null>(null)
|
|
||||||
const menuX = ref(0)
|
const menuX = ref(0)
|
||||||
const menuY = ref(0)
|
const menuY = ref(0)
|
||||||
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
|
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
|
||||||
@@ -55,10 +54,19 @@ const deleteModalTitle = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const deleteModalMessage = computed(() => {
|
const deleteModalMessage = computed(() => {
|
||||||
if (pendingDeleteAction.value?.kind === 'delete-and-block-node') {
|
const action = pendingDeleteAction.value
|
||||||
return '确定要删除这条聊天消息并屏蔽该节点吗?请输入屏蔽原因。'
|
if (!action) {
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
return '确定要删除这条聊天消息吗?此操作不可撤销。'
|
const count = deleteMessageCount(action.message)
|
||||||
|
if (action.kind === 'delete-and-block-node') {
|
||||||
|
return count > 1
|
||||||
|
? `确定要删除这组已合并的 ${count} 条聊天消息并屏蔽该节点吗?请输入屏蔽原因。`
|
||||||
|
: '确定要删除这条聊天消息并屏蔽该节点吗?请输入屏蔽原因。'
|
||||||
|
}
|
||||||
|
return count > 1
|
||||||
|
? `确定要删除这组已合并的 ${count} 条聊天消息吗?此操作不可撤销。`
|
||||||
|
: '确定要删除这条聊天消息吗?此操作不可撤销。'
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteModalConfirmText = computed(() => {
|
const deleteModalConfirmText = computed(() => {
|
||||||
@@ -74,8 +82,9 @@ const groupedMessages = computed<GroupedTextMessage[]>(() => {
|
|||||||
const group = groups.get(key)
|
const group = groups.get(key)
|
||||||
if (group) {
|
if (group) {
|
||||||
group.mergedCount += 1
|
group.mergedCount += 1
|
||||||
|
group.mergedMessages.push(message)
|
||||||
} else {
|
} else {
|
||||||
groups.set(key, { ...message, mergedCount: 1 })
|
groups.set(key, { ...message, mergedCount: 1, mergedMessages: [message] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(groups.values())
|
return Array.from(groups.values())
|
||||||
@@ -150,6 +159,14 @@ function mergeMessages(existing: TextMessage[], incoming: TextMessage[]): TextMe
|
|||||||
return Array.from(byId.values()).sort(compareMessages)
|
return Array.from(byId.values()).sort(compareMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function messagesForDelete(message: GroupedTextMessage): TextMessage[] {
|
||||||
|
return Array.from(new Map(message.mergedMessages.map((item) => [item.id, item])).values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMessageCount(message: GroupedTextMessage): number {
|
||||||
|
return messagesForDelete(message).length
|
||||||
|
}
|
||||||
|
|
||||||
async function optional<T>(request: Promise<T>): Promise<T | null> {
|
async function optional<T>(request: Promise<T>): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
return await request
|
return await request
|
||||||
@@ -197,7 +214,7 @@ function closeMessageMenu() {
|
|||||||
menuMessage.value = null
|
menuMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMessageMenu(message: TextMessage, event: MouseEvent) {
|
function openMessageMenu(message: GroupedTextMessage, event: MouseEvent) {
|
||||||
if (!props.isAdmin) {
|
if (!props.isAdmin) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -214,10 +231,9 @@ function deleteSelectedMessage() {
|
|||||||
closeMessageMenu()
|
closeMessageMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performDeleteMessage(message: TextMessage) {
|
async function performDeleteMessage(message: GroupedTextMessage) {
|
||||||
try {
|
try {
|
||||||
await deleteTextMessage(message.id)
|
await deleteMessagesFromLocalState(message)
|
||||||
messages.value = messages.value.filter((item) => item.id !== message.id)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : String(err)
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
}
|
}
|
||||||
@@ -235,6 +251,32 @@ function isMessageNotFoundError(err: unknown): boolean {
|
|||||||
return err instanceof Error && err.message === 'message not found'
|
return err instanceof Error && err.message === 'message not found'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessagesFromLocalState(message: GroupedTextMessage) {
|
||||||
|
const items = messagesForDelete(message)
|
||||||
|
const removableIds = new Set<number>()
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
await Promise.all(items.map(async (item) => {
|
||||||
|
try {
|
||||||
|
await deleteTextMessage(item.id)
|
||||||
|
removableIds.add(item.id)
|
||||||
|
} catch (err) {
|
||||||
|
if (isMessageNotFoundError(err)) {
|
||||||
|
removableIds.add(item.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.push(err instanceof Error ? err.message : String(err))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (removableIds.size > 0) {
|
||||||
|
messages.value = messages.value.filter((item) => !removableIds.has(item.id))
|
||||||
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`部分消息删除失败(${errors.length}/${items.length}):${errors[0]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteAndBlockSelectedMessageNode() {
|
function deleteAndBlockSelectedMessageNode() {
|
||||||
if (!menuMessage.value) {
|
if (!menuMessage.value) {
|
||||||
return
|
return
|
||||||
@@ -249,16 +291,9 @@ function deleteAndBlockSelectedMessageNode() {
|
|||||||
closeMessageMenu()
|
closeMessageMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performDeleteAndBlockMessageNode(payload: { message: TextMessage; nodeId: string; nodeNum: number | null; reason: string }) {
|
async function performDeleteAndBlockMessageNode(payload: { message: GroupedTextMessage; nodeId: string; nodeNum: number | null; reason: string }) {
|
||||||
try {
|
try {
|
||||||
try {
|
await deleteMessagesFromLocalState(payload.message)
|
||||||
await deleteTextMessage(payload.message.id)
|
|
||||||
} catch (err) {
|
|
||||||
if (!isMessageNotFoundError(err)) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
messages.value = messages.value.filter((item) => item.id !== payload.message.id)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createNodeBlockingRule({
|
await createNodeBlockingRule({
|
||||||
|
|||||||
Reference in New Issue
Block a user