mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(members): support batch deletion
This commit is contained in:
1 parent
5a5fae880a
commit
fdd2a943f2
13 files changed
+302
-36
No files matched your search
@@ -126,6 +126,7 @@ export {
|
||||
updateMemberAliases,
|
||||
mergeMembers,
|
||||
deleteMember,
|
||||
deleteMembers,
|
||||
ensureAliasesColumn,
|
||||
ensureAvatarColumn,
|
||||
updateSessionOwnerId,
|
||||
|
||||
@@ -201,7 +201,14 @@ export type {
|
||||
} from './message-query-functions'
|
||||
|
||||
// Member write operations (merge, delete, update aliases, DDL migration)
|
||||
export { updateMemberAliases, mergeMembers, deleteMember, ensureAliasesColumn, ensureAvatarColumn } from './member-ops'
|
||||
export {
|
||||
updateMemberAliases,
|
||||
mergeMembers,
|
||||
deleteMember,
|
||||
deleteMembers,
|
||||
ensureAliasesColumn,
|
||||
ensureAvatarColumn,
|
||||
} from './member-ops'
|
||||
|
||||
// Advanced analytics
|
||||
export {
|
||||
|
||||
@@ -107,17 +107,28 @@ export function mergeMembers(db: DatabaseAdapter, memberId1: number, memberId2:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete members and all their messages / name history in one transaction.
|
||||
*/
|
||||
export function deleteMembers(db: DatabaseAdapter, memberIds: readonly number[]): boolean {
|
||||
const uniqueMemberIds = Array.from(new Set(memberIds))
|
||||
if (uniqueMemberIds.length === 0) return false
|
||||
|
||||
const placeholders = uniqueMemberIds.map(() => '?').join(', ')
|
||||
db.transaction(() => {
|
||||
db.prepare(`DELETE FROM message WHERE sender_id IN (${placeholders})`).run(...uniqueMemberIds)
|
||||
db.prepare(`DELETE FROM member_name_history WHERE member_id IN (${placeholders})`).run(...uniqueMemberIds)
|
||||
db.prepare(`DELETE FROM member WHERE id IN (${placeholders})`).run(...uniqueMemberIds)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a member and all their messages / name history.
|
||||
*/
|
||||
export function deleteMember(db: DatabaseAdapter, memberId: number): boolean {
|
||||
try {
|
||||
db.transaction(() => {
|
||||
db.prepare('DELETE FROM message WHERE sender_id = ?').run(memberId)
|
||||
db.prepare('DELETE FROM member_name_history WHERE member_id = ?').run(memberId)
|
||||
db.prepare('DELETE FROM member WHERE id = ?').run(memberId)
|
||||
})
|
||||
return true
|
||||
return deleteMembers(db, [memberId])
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -95,6 +95,13 @@ function createSessionDb(): TestSqliteDb {
|
||||
content TEXT,
|
||||
platform_message_id TEXT
|
||||
);
|
||||
CREATE TABLE member_name_history (
|
||||
id INTEGER PRIMARY KEY,
|
||||
member_id INTEGER,
|
||||
name TEXT,
|
||||
start_time INTEGER,
|
||||
end_time INTEGER
|
||||
);
|
||||
CREATE TABLE segment (
|
||||
id INTEGER PRIMARY KEY,
|
||||
start_ts INTEGER,
|
||||
@@ -114,6 +121,9 @@ function createSessionDb(): TestSqliteDb {
|
||||
(1, 1, 100, 0, 'alpha first', 'm-1'),
|
||||
(2, 2, 200, 0, 'alpha from bob', 'm-2'),
|
||||
(3, 1, 300, 0, 'alpha later', 'm-3');
|
||||
INSERT INTO member_name_history (id, member_id, name, start_time, end_time) VALUES
|
||||
(1, 1, 'Alice', 100, 300),
|
||||
(2, 2, 'Bob', 200, 200);
|
||||
`)
|
||||
return db
|
||||
}
|
||||
@@ -290,6 +300,93 @@ describe('registerSharedRoutes smoke tests', () => {
|
||||
assert.equal(typeof body.duration, 'number')
|
||||
})
|
||||
|
||||
it('POST /_web/sessions/:id/members/batch-delete deletes selected member data in one request', async () => {
|
||||
const db = createSessionDb()
|
||||
db.exec(`
|
||||
INSERT INTO member (id, platform_id, account_name, group_nickname, avatar)
|
||||
VALUES (3, 'carol', 'Carol', NULL, NULL);
|
||||
INSERT INTO message (id, sender_id, ts, type, content, platform_message_id)
|
||||
VALUES (4, 3, 400, 0, 'carol stays', 'm-4');
|
||||
INSERT INTO member_name_history (id, member_id, name, start_time, end_time)
|
||||
VALUES (3, 3, 'Carol', 400, 400);
|
||||
`)
|
||||
const routeApp = Fastify()
|
||||
registerSharedRoutes(routeApp, createTestContext(new Map([['chat-1', db]])))
|
||||
await routeApp.ready()
|
||||
|
||||
const resp = await routeApp.inject({
|
||||
method: 'POST',
|
||||
url: '/_web/sessions/chat-1/members/batch-delete',
|
||||
payload: { memberIds: [1, 2] },
|
||||
})
|
||||
const remainingMembers = db.prepare('SELECT id FROM member ORDER BY id').all()
|
||||
const remainingMessages = db.prepare('SELECT sender_id AS senderId FROM message ORDER BY id').all()
|
||||
const remainingHistory = db.prepare('SELECT member_id AS memberId FROM member_name_history ORDER BY id').all()
|
||||
|
||||
await routeApp.close()
|
||||
db.close()
|
||||
|
||||
assert.equal(resp.statusCode, 200)
|
||||
assert.deepEqual(resp.json(), { success: true, deletedCount: 2 })
|
||||
assert.deepEqual(remainingMembers, [{ id: 3 }])
|
||||
assert.deepEqual(remainingMessages, [{ senderId: 3 }])
|
||||
assert.deepEqual(remainingHistory, [{ memberId: 3 }])
|
||||
})
|
||||
|
||||
it('POST /_web/sessions/:id/members/batch-delete rejects an empty selection', async () => {
|
||||
const db = createSessionDb()
|
||||
const routeApp = Fastify()
|
||||
registerSharedRoutes(routeApp, createTestContext(new Map([['chat-1', db]])))
|
||||
await routeApp.ready()
|
||||
|
||||
const resp = await routeApp.inject({
|
||||
method: 'POST',
|
||||
url: '/_web/sessions/chat-1/members/batch-delete',
|
||||
payload: { memberIds: [] },
|
||||
})
|
||||
const memberCount = db.prepare('SELECT COUNT(*) AS count FROM member').get() as { count: number }
|
||||
|
||||
await routeApp.close()
|
||||
db.close()
|
||||
|
||||
assert.equal(resp.statusCode, 400)
|
||||
assert.deepEqual(resp.json(), { success: false, error: 'memberIds must contain at least one positive integer' })
|
||||
assert.equal(memberCount.count, 2)
|
||||
})
|
||||
|
||||
it('POST /_web/sessions/:id/members/batch-delete rolls back every deletion when one member fails', async () => {
|
||||
const db = createSessionDb()
|
||||
db.exec(`
|
||||
CREATE TRIGGER prevent_bob_delete
|
||||
BEFORE DELETE ON member
|
||||
WHEN OLD.id = 2
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'Bob cannot be deleted');
|
||||
END;
|
||||
`)
|
||||
const routeApp = Fastify()
|
||||
registerSharedRoutes(routeApp, createTestContext(new Map([['chat-1', db]])))
|
||||
await routeApp.ready()
|
||||
|
||||
const resp = await routeApp.inject({
|
||||
method: 'POST',
|
||||
url: '/_web/sessions/chat-1/members/batch-delete',
|
||||
payload: { memberIds: [1, 2] },
|
||||
})
|
||||
const memberCount = db.prepare('SELECT COUNT(*) AS count FROM member').get() as { count: number }
|
||||
const messageCount = db.prepare('SELECT COUNT(*) AS count FROM message').get() as { count: number }
|
||||
const historyCount = db.prepare('SELECT COUNT(*) AS count FROM member_name_history').get() as { count: number }
|
||||
|
||||
await routeApp.close()
|
||||
db.close()
|
||||
|
||||
assert.equal(resp.statusCode, 500)
|
||||
assert.deepEqual(resp.json(), { success: false, error: 'Failed to delete selected members' })
|
||||
assert.equal(memberCount.count, 2)
|
||||
assert.equal(messageCount.count, 3)
|
||||
assert.equal(historyCount.count, 2)
|
||||
})
|
||||
|
||||
it('POST /_web/ai/logs/show reveals the current AI log file through the host shell', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'chatlab-ai-log-route-'))
|
||||
const currentLogPath = path.join(dir, 'logs', 'ai', 'ai_current.log')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { memberService } from '@openchatlab/node-runtime'
|
||||
import { appLogger, memberService } from '@openchatlab/node-runtime'
|
||||
import { apiErrorFromUnknown } from '../../errors'
|
||||
|
||||
export function registerMemberRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
@@ -39,6 +40,41 @@ export function registerMemberRoutes(server: FastifyInstance, ctx: HttpRouteCont
|
||||
}
|
||||
)
|
||||
|
||||
server.post<{ Params: { id: string }; Body: { memberIds?: unknown } }>(
|
||||
'/_web/sessions/:id/members/batch-delete',
|
||||
async (request, reply) => {
|
||||
const memberIds = request.body?.memberIds
|
||||
if (
|
||||
!Array.isArray(memberIds) ||
|
||||
memberIds.length === 0 ||
|
||||
memberIds.some((memberId) => !Number.isSafeInteger(memberId) || Number(memberId) <= 0)
|
||||
) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
error: 'memberIds must contain at least one positive integer',
|
||||
})
|
||||
}
|
||||
|
||||
const uniqueMemberIds = Array.from(new Set(memberIds as number[]))
|
||||
try {
|
||||
const success = memberService.deleteMembers(adapter, request.params.id, uniqueMemberIds)
|
||||
if (!success) {
|
||||
throw new Error('Member deletion transaction returned no result')
|
||||
}
|
||||
} catch (error) {
|
||||
if (apiErrorFromUnknown(error)) throw error
|
||||
appLogger.error('members', 'Batch member deletion failed', error)
|
||||
return reply.code(500).send({ success: false, error: 'Failed to delete selected members' })
|
||||
}
|
||||
|
||||
appLogger.info('members', 'Batch member deletion completed', {
|
||||
sessionId: request.params.id,
|
||||
memberCount: uniqueMemberIds.length,
|
||||
})
|
||||
return { success: true, deletedCount: uniqueMemberIds.length }
|
||||
}
|
||||
)
|
||||
|
||||
server.post<{ Params: { id: string }; Body: { memberId1: number; memberId2: number } }>(
|
||||
'/_web/sessions/:id/members/merge',
|
||||
async (request) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
updateMemberAliases as coreUpdateMemberAliases,
|
||||
mergeMembers as coreMergeMembers,
|
||||
deleteMember as coreDeleteMember,
|
||||
deleteMembers as coreDeleteMembers,
|
||||
} from '@openchatlab/core'
|
||||
import type { MemberWithAliases, MembersPaginationParams, MembersPaginatedResult } from '@openchatlab/core'
|
||||
import type { SessionRuntimeAdapter } from './adapters'
|
||||
@@ -95,6 +96,18 @@ export function deleteMember(adapter: SessionRuntimeAdapter, sessionId: string,
|
||||
return coreDeleteMember(db, memberId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple members and all associated data atomically.
|
||||
*/
|
||||
export function deleteMembers(
|
||||
adapter: SessionRuntimeAdapter,
|
||||
sessionId: string,
|
||||
memberIds: readonly number[]
|
||||
): boolean {
|
||||
const db = adapter.ensureWritable(sessionId)
|
||||
return coreDeleteMembers(db, memberIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member name change history.
|
||||
* Core prefers member_name_history and falls back to message-derived names for legacy sessions.
|
||||
|
||||
@@ -40,15 +40,17 @@ const members = ref<MemberWithStats[]>([])
|
||||
const isLoading = ref(false)
|
||||
const loadFailed = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const showMergeControls = computed(() => shouldShowMemberMergeControls(props.chatType, members.value.length))
|
||||
const showSelectionControls = computed(() => shouldShowMemberMergeControls(props.chatType, members.value.length))
|
||||
const memberTableGridClass = computed(() => {
|
||||
if (props.chatType === 'group') return ''
|
||||
return showMergeControls.value ? 'member-table-grid--private' : 'member-table-grid--private-without-selection'
|
||||
return showSelectionControls.value ? 'member-table-grid--private' : 'member-table-grid--private-without-selection'
|
||||
})
|
||||
|
||||
// 删除确认状态
|
||||
const deletingMember = ref<MemberWithStats | null>(null)
|
||||
const isDeleting = ref(false)
|
||||
const showBatchDeleteModal = ref(false)
|
||||
const isBatchDeleting = ref(false)
|
||||
|
||||
// 合并确认状态
|
||||
const showMergeModal = ref(false)
|
||||
@@ -91,31 +93,34 @@ function viewMemberChatRecords(member: MemberWithStats) {
|
||||
|
||||
const filteredSortedMembers = computed(() => filterAndSortMembers(members.value, searchQuery.value, sortState.value))
|
||||
const {
|
||||
selectedIds: selectedMergeIds,
|
||||
setSelection: setSelectedMergeIds,
|
||||
clearSelection: clearMergeSelection,
|
||||
selectedIds: selectedMemberIds,
|
||||
setSelection: setSelectedMemberIds,
|
||||
clearSelection: clearMemberSelection,
|
||||
handleRowClick: handleSelectionRowClick,
|
||||
handleRowMouseDown: handleSelectionRowMouseDown,
|
||||
} = useTableRowSelection({
|
||||
rows: filteredSortedMembers,
|
||||
getRowId: (member) => member.id,
|
||||
})
|
||||
const selectedMergeMembers = computed(() => members.value.filter((member) => selectedMergeIds.value.has(member.id)))
|
||||
const canMergeSelected = computed(() => selectedMergeMembers.value.length === 2)
|
||||
const selectedMembers = computed(() => members.value.filter((member) => selectedMemberIds.value.has(member.id)))
|
||||
const canMergeSelected = computed(() => selectedMembers.value.length === 2)
|
||||
const selectedMessageCount = computed(() =>
|
||||
selectedMembers.value.reduce((total, member) => total + member.messageCount, 0)
|
||||
)
|
||||
|
||||
function handleMemberRowClick(index: number, memberId: number, event: MouseEvent) {
|
||||
if (!showMergeControls.value) return
|
||||
if (!showSelectionControls.value) return
|
||||
handleSelectionRowClick(index, memberId, event)
|
||||
}
|
||||
|
||||
function handleMemberRowMouseDown(event: MouseEvent) {
|
||||
if (!showMergeControls.value) return
|
||||
if (!showSelectionControls.value) return
|
||||
handleSelectionRowMouseDown(event)
|
||||
}
|
||||
|
||||
const mergePlan = computed(() => {
|
||||
if (selectedMergeMembers.value.length !== 2) return null
|
||||
const [memberA, memberB] = selectedMergeMembers.value
|
||||
if (selectedMembers.value.length !== 2) return null
|
||||
const [memberA, memberB] = selectedMembers.value
|
||||
if (
|
||||
memberB.messageCount > memberA.messageCount ||
|
||||
(memberB.messageCount === memberA.messageCount && memberB.id < memberA.id)
|
||||
@@ -243,9 +248,9 @@ async function confirmDelete() {
|
||||
const memberId = deletingMember.value.id
|
||||
const success = await dataService.deleteMember(props.sessionId, memberId)
|
||||
if (success) {
|
||||
const nextSelection = new Set(selectedMergeIds.value)
|
||||
const nextSelection = new Set(selectedMemberIds.value)
|
||||
nextSelection.delete(memberId)
|
||||
setSelectedMergeIds(nextSelection)
|
||||
setSelectedMemberIds(nextSelection)
|
||||
await loadMembers()
|
||||
emit('data-changed')
|
||||
}
|
||||
@@ -257,6 +262,32 @@ async function confirmDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
function openBatchDeleteModal() {
|
||||
if (selectedMemberIds.value.size === 0) return
|
||||
showBatchDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function confirmBatchDelete() {
|
||||
const sessionId = props.sessionId
|
||||
const memberIds = Array.from(selectedMemberIds.value)
|
||||
if (memberIds.length === 0) return
|
||||
|
||||
isBatchDeleting.value = true
|
||||
try {
|
||||
const success = await dataService.deleteMembers(sessionId, memberIds)
|
||||
if (success && sessionId === props.sessionId) {
|
||||
showBatchDeleteModal.value = false
|
||||
clearMemberSelection()
|
||||
await loadMembers()
|
||||
emit('data-changed')
|
||||
}
|
||||
} catch (error) {
|
||||
reportMemberManagementError('Failed to delete selected members:', error)
|
||||
} finally {
|
||||
isBatchDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openMergeModal() {
|
||||
if (!canMergeSelected.value) return
|
||||
showMergeModal.value = true
|
||||
@@ -273,7 +304,7 @@ async function confirmMerge() {
|
||||
)
|
||||
if (success) {
|
||||
showMergeModal.value = false
|
||||
clearMergeSelection()
|
||||
clearMemberSelection()
|
||||
await loadMembers()
|
||||
emit('data-changed')
|
||||
}
|
||||
@@ -285,8 +316,8 @@ async function confirmMerge() {
|
||||
}
|
||||
|
||||
watch([searchQuery, sortState], resetVirtualScroll)
|
||||
watch(showMergeControls, (show) => {
|
||||
if (!show) clearMergeSelection()
|
||||
watch(showSelectionControls, (show) => {
|
||||
if (!show) clearMemberSelection()
|
||||
})
|
||||
|
||||
// 监听 sessionId 变化
|
||||
@@ -299,7 +330,7 @@ watch(
|
||||
savingAliasIds.value = new Set()
|
||||
searchQuery.value = ''
|
||||
resetSort()
|
||||
clearMergeSelection()
|
||||
clearMemberSelection()
|
||||
void resetVirtualScroll()
|
||||
void loadMembers()
|
||||
},
|
||||
@@ -349,14 +380,14 @@ onUnmounted(() => {
|
||||
<UButton icon="i-heroicons-x-mark" variant="link" color="neutral" size="xs" @click="searchQuery = ''" />
|
||||
</template>
|
||||
</UInput>
|
||||
<div v-if="showMergeControls" class="flex items-center gap-2">
|
||||
<div v-if="showSelectionControls" class="flex items-center gap-2">
|
||||
<UIcon
|
||||
v-if="isLoading && members.length > 0"
|
||||
name="i-heroicons-arrow-path"
|
||||
class="h-4 w-4 animate-spin text-primary-500"
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('members.list.mergeSelectedCount', { count: selectedMergeIds.size }) }}
|
||||
{{ t('members.list.selectedCount', { count: selectedMemberIds.size }) }}
|
||||
</span>
|
||||
<UButton
|
||||
icon="i-heroicons-link"
|
||||
@@ -367,6 +398,16 @@ onUnmounted(() => {
|
||||
>
|
||||
{{ t('members.list.mergeSelected') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
size="sm"
|
||||
color="error"
|
||||
variant="outline"
|
||||
:disabled="selectedMemberIds.size === 0"
|
||||
@click="openBatchDeleteModal"
|
||||
>
|
||||
{{ t('members.list.deleteSelected') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -408,7 +449,7 @@ onUnmounted(() => {
|
||||
class="member-table-grid shrink-0 border-b border-gray-200 bg-gray-50 px-3 py-2 text-xs font-medium text-gray-500 dark:border-white/5 dark:bg-page-dark/80 dark:text-gray-400"
|
||||
:class="memberTableGridClass"
|
||||
>
|
||||
<span v-if="showMergeControls" aria-hidden="true" />
|
||||
<span v-if="showSelectionControls" aria-hidden="true" />
|
||||
<span>{{ t('members.list.table.accountName') }}</span>
|
||||
<button
|
||||
v-if="props.chatType === 'group'"
|
||||
@@ -488,16 +529,18 @@ onUnmounted(() => {
|
||||
class="member-table-grid absolute left-0 top-0 w-full min-h-16 border-b border-gray-100/80 px-3 py-2 transition-colors hover:bg-gray-50 dark:border-white/5 dark:hover:bg-gray-800/30"
|
||||
:class="[
|
||||
memberTableGridClass,
|
||||
showMergeControls ? 'cursor-pointer' : '',
|
||||
showMergeControls && selectedMergeIds.has(member.id) ? 'bg-primary-50/70 dark:bg-primary-950/20' : '',
|
||||
showSelectionControls ? 'cursor-pointer' : '',
|
||||
showSelectionControls && selectedMemberIds.has(member.id)
|
||||
? 'bg-primary-50/70 dark:bg-primary-950/20'
|
||||
: '',
|
||||
]"
|
||||
:style="{ transform: `translateY(${virtualItem.start}px)` }"
|
||||
@mousedown="handleMemberRowMouseDown"
|
||||
@click="handleMemberRowClick(virtualItem.index, member.id, $event)"
|
||||
>
|
||||
<div v-if="showMergeControls" class="flex justify-center">
|
||||
<div v-if="showSelectionControls" class="flex justify-center">
|
||||
<UCheckbox
|
||||
:model-value="selectedMergeIds.has(member.id)"
|
||||
:model-value="selectedMemberIds.has(member.id)"
|
||||
@click.stop="handleMemberRowClick(virtualItem.index, member.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -608,6 +651,38 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- 批量删除确认弹窗 -->
|
||||
<UModal :open="showBatchDeleteModal" :ui="{ content: 'max-w-sm' }" @update:open="showBatchDeleteModal = $event">
|
||||
<template #content>
|
||||
<div class="p-6 text-center">
|
||||
<div
|
||||
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30"
|
||||
>
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="h-7 w-7 text-red-500" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('members.list.batchDeleteModal.title') }}
|
||||
</h3>
|
||||
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('members.list.batchDeleteModal.content', {
|
||||
count: selectedMembers.length,
|
||||
messageCount: selectedMessageCount.toLocaleString(),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div class="flex justify-center gap-3">
|
||||
<UButton variant="outline" :disabled="isBatchDeleting" @click="showBatchDeleteModal = false">
|
||||
{{ t('members.list.modal.cancel') }}
|
||||
</UButton>
|
||||
<UButton color="error" :loading="isBatchDeleting" @click="confirmBatchDelete">
|
||||
{{ t('members.list.modal.confirm') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- 合并确认弹窗 -->
|
||||
<UModal :open="showMergeModal" :ui="{ content: 'max-w-md' }" @update:open="showMergeModal = $event">
|
||||
<template #content>
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"aliasPlaceholder": "Press Enter to add",
|
||||
"delete": "Delete",
|
||||
"deleteSelected": "Delete Selected",
|
||||
"mergeSelected": "Merge Selected",
|
||||
"mergeSelectedCount": "{count} members selected",
|
||||
"selectedCount": "{count} members selected",
|
||||
"pagination": "Showing {start} - {end} of {total} members",
|
||||
"tip": "Tip: Adding aliases helps identify chat participants and improves search and AI analysis accuracy.",
|
||||
"mergeModal": {
|
||||
@@ -46,6 +47,10 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm Merge"
|
||||
},
|
||||
"batchDeleteModal": {
|
||||
"title": "Delete Selected Members?",
|
||||
"content": "This will delete {count} selected members and their {messageCount} messages. This action cannot be undone."
|
||||
},
|
||||
"modal": {
|
||||
"title": "Delete Member?",
|
||||
"content": "This will delete member {name} and their {count} messages. This action cannot be undone.",
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"aliasPlaceholder": "入力して Enter で追加",
|
||||
"delete": "削除",
|
||||
"deleteSelected": "選択項目を削除",
|
||||
"mergeSelected": "選択を結合",
|
||||
"mergeSelectedCount": "{count} 名を選択中",
|
||||
"selectedCount": "{count} 名を選択中",
|
||||
"pagination": "{start} - {end} 件表示、全 {total} 名のメンバー",
|
||||
"tip": "ヒント:別名を追加すると、チャット履歴の相手をより正確に識別できます。別名は検索や AI 分析で使用されます。",
|
||||
"mergeModal": {
|
||||
@@ -46,6 +47,10 @@
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "結合を確認"
|
||||
},
|
||||
"batchDeleteModal": {
|
||||
"title": "選択したメンバーを削除しますか?",
|
||||
"content": "選択した {count} 名のメンバーと、その {messageCount} 件のメッセージを削除します。この操作は元に戻せません。"
|
||||
},
|
||||
"modal": {
|
||||
"title": "メンバーを削除しますか?",
|
||||
"content": "メンバー {name} とその {count} 件のメッセージを削除します。この操作は元に戻せません。",
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"aliasPlaceholder": "输入后回车添加",
|
||||
"delete": "删除",
|
||||
"deleteSelected": "删除选中",
|
||||
"mergeSelected": "合并选中",
|
||||
"mergeSelectedCount": "已选 {count} 位成员",
|
||||
"selectedCount": "已选 {count} 位成员",
|
||||
"pagination": "显示 {start} - {end} 条,共 {total} 位成员",
|
||||
"tip": "提示:添加别名可以更好地识别聊天记录中的对话对象,别名将用于搜索和 AI 分析中。",
|
||||
"mergeModal": {
|
||||
@@ -46,6 +47,10 @@
|
||||
"cancel": "取消",
|
||||
"confirm": "确认合并"
|
||||
},
|
||||
"batchDeleteModal": {
|
||||
"title": "确认批量删除成员?",
|
||||
"content": "即将删除选中的 {count} 位成员及其 {messageCount} 条消息,此操作不可恢复。"
|
||||
},
|
||||
"modal": {
|
||||
"title": "确认删除成员?",
|
||||
"content": "即将删除成员 {name} 及其 {count} 条消息,此操作不可恢复。",
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
},
|
||||
"aliasPlaceholder": "輸入後按 Enter 新增",
|
||||
"delete": "刪除",
|
||||
"deleteSelected": "刪除所選",
|
||||
"mergeSelected": "合併所選",
|
||||
"mergeSelectedCount": "已選 {count} 位成員",
|
||||
"selectedCount": "已選 {count} 位成員",
|
||||
"pagination": "顯示第 {start} - {end} 筆,共 {total} 位成員",
|
||||
"tip": "提示:新增別名後,搜尋與 AI 分析時會更容易辨識聊天對象。",
|
||||
"mergeModal": {
|
||||
@@ -46,6 +47,10 @@
|
||||
"cancel": "取消",
|
||||
"confirm": "確認合併"
|
||||
},
|
||||
"batchDeleteModal": {
|
||||
"title": "確認批次刪除成員?",
|
||||
"content": "即將刪除所選的 {count} 位成員及其 {messageCount} 則訊息,此操作無法復原。"
|
||||
},
|
||||
"modal": {
|
||||
"title": "確認刪除成員?",
|
||||
"content": "即將刪除成員 {name} 及其 {count} 則訊息,此操作不可復原。",
|
||||
|
||||
@@ -351,6 +351,11 @@ export class FetchDataAdapter implements DataAdapter {
|
||||
return result.success
|
||||
}
|
||||
|
||||
async deleteMembers(sessionId: string, memberIds: number[]): Promise<boolean> {
|
||||
const result = await post<{ success: boolean }>(`/sessions/${sessionId}/members/batch-delete`, { memberIds })
|
||||
return result.success
|
||||
}
|
||||
|
||||
// ==================== 社交分析 ====================
|
||||
|
||||
getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): Promise<CatchphraseAnalysis> {
|
||||
|
||||
@@ -220,6 +220,7 @@ export interface DataAdapter {
|
||||
updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise<boolean>
|
||||
mergeMembers(sessionId: string, memberId1: number, memberId2: number): Promise<boolean>
|
||||
deleteMember(sessionId: string, memberId: number): Promise<boolean>
|
||||
deleteMembers(sessionId: string, memberIds: number[]): Promise<boolean>
|
||||
|
||||
// ==================== 社交分析 ====================
|
||||
|
||||
|
||||
Reference in new issue
Block a user