| <template> |
| <div class="conversation-page"> |
| <v-container fluid class="pa-0"> |
| |
| <v-card flat> |
| <v-card-title class="d-flex align-center py-3 px-4"> |
| <span class="text-h4">{{ tm('history.title') }}</span> |
| <v-chip size="small" class="ml-2">{{ pagination.total || 0 }}</v-chip> |
| <v-row class="me-4 ms-4" dense> |
| <v-col cols="12" sm="6" md="4"> |
| <v-combobox v-model="platformFilter" :label="tm('filters.platform')" |
| :items="availablePlatforms" chips multiple clearable variant="solo-filled" flat |
| density="compact" hide-details> |
| <template v-slot:selection="{ item }"> |
| <v-chip size="small" label> |
| {{ item.title }} |
| </v-chip> |
| </template> |
| </v-combobox> |
| </v-col> |
| |
| <v-col cols="12" sm="6" md="4"> |
| <v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems" |
| chips multiple clearable variant="solo-filled" density="compact" hide-details flat> |
| <template v-slot:selection="{ item }"> |
| <v-chip size="small" variant="solo-filled" label> |
| {{ item.title }} |
| </v-chip> |
| </template> |
| </v-select> |
| </v-col> |
| |
| <v-col cols="12" sm="12" md="4"> |
| <v-text-field v-model="search" prepend-inner-icon="mdi-magnify" |
| :label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat |
| clearable></v-text-field> |
| </v-col> |
| </v-row> |
| <v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations" |
| :loading="loading" size="small" class="mr-2"> |
| {{ tm('history.refresh') }} |
| </v-btn> |
| <v-btn |
| v-if="selectedItems.length > 0" |
| color="success" |
| prepend-icon="mdi-download" |
| variant="tonal" |
| @click="exportConversations" |
| :disabled="loading" |
| size="small" |
| class="mr-2"> |
| {{ tm('batch.exportSelected', { count: selectedItems.length }) }} |
| </v-btn> |
| <v-btn |
| v-if="selectedItems.length > 0" |
| color="error" |
| prepend-icon="mdi-delete" |
| variant="tonal" |
| @click="confirmBatchDelete" |
| :disabled="loading" |
| size="small"> |
| {{ tm('batch.deleteSelected', { count: selectedItems.length }) }} |
| </v-btn> |
| </v-card-title> |
| |
| <v-divider></v-divider> |
| |
| <v-card-text class="pa-0"> |
| <v-data-table v-model="selectedItems" :headers="tableHeaders" :items="conversations" |
| :loading="loading" style="font-size: 12px;" density="comfortable" hide-default-footer |
| class="elevation-0" :items-per-page="pagination.page_size" |
| :items-per-page-options="pageSizeOptions" show-select return-object |
| :disabled="loading" @update:options="handleTableOptions"> |
| <template v-slot:item.title="{ item }"> |
| <div class="d-flex align-center"> |
| <span>{{ item.title || tm('status.noTitle') }}</span> |
| </div> |
| </template> |
| |
| <template v-slot:item.platform="{ item }"> |
| <v-chip size="small" label> |
| {{ item.sessionInfo.platform || tm('status.unknown') }} |
| </v-chip> |
| </template> |
| |
| <template v-slot:item.messageType="{ item }"> |
| <v-chip size="small" label> |
| {{ getMessageTypeDisplay(item.sessionInfo.messageType) }} |
| </v-chip> |
| </template> |
| |
| <template v-slot:item.cid="{ item }"> |
| <span class="text-truncate">{{ item.cid || tm('status.unknown') }}</span> |
| </template> |
| |
| <template v-slot:item.sessionId="{ item }"> |
| <span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span> |
| </template> |
| |
| <template v-slot:item.created_at="{ item }"> |
| {{ formatTimestamp(item.created_at) }} |
| </template> |
| |
| <template v-slot:item.updated_at="{ item }"> |
| {{ formatTimestamp(item.updated_at) }} |
| </template> |
| |
| <template v-slot:item.actions="{ item }"> |
| <div class="actions-wrapper"> |
| <v-btn icon variant="plain" size="x-small" class="action-button" |
| @click="viewConversation(item)" :disabled="loading"> |
| <v-icon>mdi-eye</v-icon> |
| </v-btn> |
| <v-btn icon variant="plain" size="x-small" class="action-button" |
| @click="editConversation(item)" :disabled="loading"> |
| <v-icon>mdi-pencil</v-icon> |
| </v-btn> |
| <v-btn icon color="error" variant="plain" size="x-small" class="action-button" |
| @click="confirmDeleteConversation(item)" :disabled="loading"> |
| <v-icon>mdi-delete</v-icon> |
| </v-btn> |
| </div> |
| </template> |
| |
| <template v-slot:no-data> |
| <div class="d-flex flex-column align-center py-6"> |
| <v-icon size="64" color="grey lighten-1">mdi-chat-remove</v-icon> |
| <span class="text-subtitle-1 text-disabled mt-3">{{ tm('status.noData') }}</span> |
| </div> |
| </template> |
| </v-data-table> |
| |
| |
| <div class="d-flex justify-center py-3"> |
| |
| <div class="d-flex justify-between align-center px-4 py-2 bg-grey-lighten-5"> |
| <div class="d-flex align-center"> |
| <span class="text-caption mr-2">{{ tm('pagination.itemsPerPage') }}:</span> |
| <v-select v-model="pagination.page_size" :items="pageSizeOptions" variant="outlined" |
| density="compact" hide-details style="max-width: 100px;" |
| :disabled="loading" @update:model-value="onPageSizeChange"></v-select> |
| </div> |
| <div class="text-caption ml-4"> |
| {{ tm('pagination.showingItems', { |
| start: Math.min((pagination.page - 1) * pagination.page_size + 1, pagination.total), |
| end: Math.min(pagination.page * pagination.page_size, pagination.total), |
| total: pagination.total |
| }) }} |
| </div> |
| </div> |
| <v-pagination v-model="pagination.page" :length="pagination.total_pages" :disabled="loading" |
| @update:model-value="fetchConversations" rounded="circle" :total-visible="7"></v-pagination> |
| </div> |
| </v-card-text> |
| </v-card> |
| </v-container> |
| |
| |
| <v-dialog v-model="dialogView" max-width="900px" scrollable> |
| <v-card class="conversation-detail-card"> |
| <v-card-title class="ml-2 mt-2 d-flex align-center"> |
| <span class="text-truncate">{{ selectedConversation?.title || tm('status.noTitle') }}</span> |
| <v-spacer></v-spacer> |
| <div class="d-flex align-center" v-if="selectedConversation?.sessionInfo"> |
| <v-chip text-color="primary" size="small" class="mr-2" rounded="md"> |
| {{ selectedConversation.sessionInfo.platform }} |
| </v-chip> |
| <v-chip text-color="secondary" size="small" rounded="md"> |
| {{ getMessageTypeDisplay(selectedConversation.sessionInfo.messageType) }} |
| </v-chip> |
| </div> |
| </v-card-title> |
| |
| <v-card-text> |
| <div class="mb-4 d-flex align-center"> |
| <v-btn color="secondary" variant="tonal" size="small" class="mr-2" |
| @click="isEditingHistory = !isEditingHistory"> |
| <v-icon class="mr-1">{{ isEditingHistory ? 'mdi-eye' : 'mdi-pencil' }}</v-icon> |
| {{ isEditingHistory ? tm('dialogs.view.previewMode') : tm('dialogs.view.editMode') }} |
| </v-btn> |
| <v-btn v-if="isEditingHistory" color="success" variant="tonal" size="small" |
| :loading="savingHistory" @click="saveHistoryChanges"> |
| <v-icon class="mr-1">mdi-content-save</v-icon> |
| {{ tm('dialogs.view.saveChanges') }} |
| </v-btn> |
| </div> |
| |
| |
| <div v-if="isEditingHistory" class="monaco-editor-container"> |
| <VueMonacoEditor v-model:value="editedHistory" theme="vs-dark" language="json" :options="{ |
| automaticLayout: true, |
| fontSize: 13, |
| tabSize: 2, |
| minimap: { enabled: false }, |
| scrollBeyondLastLine: false, |
| wordWrap: 'on' |
| }" @editorDidMount="onMonacoMounted" /> |
| </div> |
| |
| |
| <div v-else class="conversation-messages-container" style="background-color: var(--v-theme-surface);"> |
| |
| <div v-if="conversationHistory.length === 0" class="text-center py-5"> |
| <v-icon size="48" color="grey">mdi-chat-remove</v-icon> |
| <p class="text-disabled mt-2">{{ tm('status.emptyContent') }}</p> |
| </div> |
| |
| |
| <MessageList v-else :messages="formattedMessages" :isDark="isDark" /> |
| </div> |
| </v-card-text> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="closeHistoryDialog"> |
| {{ tm('dialogs.view.close') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="dialogEdit" max-width="500px"> |
| <v-card> |
| <v-card-title class="bg-primary text-white py-3"> |
| <v-icon color="white" class="me-2">mdi-pencil</v-icon> |
| <span>{{ tm('dialogs.edit.title') }}</span> |
| </v-card-title> |
| |
| <v-card-text class="py-4"> |
| <v-form ref="form" v-model="valid"> |
| <v-text-field v-model="editedItem.title" :label="tm('dialogs.edit.titleLabel')" |
| :placeholder="tm('dialogs.edit.titlePlaceholder')" variant="outlined" density="comfortable" |
| class="mb-3"></v-text-field> |
| </v-form> |
| </v-card-text> |
| |
| <v-divider></v-divider> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="dialogEdit = false" :disabled="loading"> |
| {{ tm('dialogs.edit.cancel') }} |
| </v-btn> |
| <v-btn color="primary" @click="saveConversation" :loading="loading"> |
| {{ tm('dialogs.edit.save') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="dialogDelete" max-width="500px"> |
| <v-card> |
| <v-card-title class="bg-error text-white py-3"> |
| <v-icon color="white" class="me-2">mdi-alert</v-icon> |
| <span>{{ tm('dialogs.delete.title') }}</span> |
| </v-card-title> |
| |
| <v-card-text class="py-4"> |
| <p>{{ tm('dialogs.delete.message', { title: selectedConversation?.title || tm('status.noTitle') }) |
| }}</p> |
| </v-card-text> |
| |
| <v-divider></v-divider> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="dialogDelete = false" :disabled="loading"> |
| {{ tm('dialogs.delete.cancel') }} |
| </v-btn> |
| <v-btn color="error" @click="deleteConversation" :loading="loading"> |
| {{ tm('dialogs.delete.confirm') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-dialog v-model="dialogBatchDelete" max-width="600px"> |
| <v-card> |
| <v-card-title class="bg-error text-white py-3"> |
| <v-icon color="white" class="me-2">mdi-delete</v-icon> |
| <span>{{ tm('dialogs.batchDelete.title') }}</span> |
| </v-card-title> |
| |
| <v-card-text class="py-4"> |
| <p class="mb-3">{{ tm('dialogs.batchDelete.message', { count: selectedItems.length }) }}</p> |
| |
| |
| <div v-if="selectedItems.length > 0" class="mb-3"> |
| <v-chip v-for="(item, index) in selectedItems.slice(0, 5)" :key="`${item.user_id}-${item.cid}`" |
| size="small" class="mr-1 mb-1" closable @click:close="removeFromSelection(item)" |
| :disabled="loading"> |
| {{ item.title || tm('status.noTitle') }} |
| </v-chip> |
| <v-chip v-if="selectedItems.length > 5" size="small" class="mr-1 mb-1"> |
| {{ tm('dialogs.batchDelete.andMore', { count: selectedItems.length - 5 }) }} |
| </v-chip> |
| </div> |
| |
| <v-alert type="warning" variant="tonal" class="mb-3"> |
| {{ tm('dialogs.batchDelete.warning') }} |
| </v-alert> |
| </v-card-text> |
| |
| <v-divider></v-divider> |
| |
| <v-card-actions class="pa-4"> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="dialogBatchDelete = false" :disabled="loading"> |
| {{ tm('dialogs.batchDelete.cancel') }} |
| </v-btn> |
| <v-btn color="error" @click="batchDeleteConversations" :loading="loading"> |
| {{ tm('dialogs.batchDelete.confirm') }} |
| </v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
| |
| |
| <v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top"> |
| {{ message }} |
| </v-snackbar> |
| </div> |
| </template> |
| |
| <script> |
| import axios from 'axios'; |
| import { debounce } from 'lodash'; |
| import { VueMonacoEditor } from '@guolao/vue-monaco-editor'; |
| import { useCommonStore } from '@/stores/common'; |
| import { useCustomizerStore } from '@/stores/customizer'; |
| import { useI18n, useModuleI18n } from '@/i18n/composables'; |
| import MessageList from '@/components/chat/MessageList.vue'; |
| import { |
| askForConfirmation as askForConfirmationDialog, |
| useConfirmDialog |
| } from '@/utils/confirmDialog'; |
| |
| export default { |
| name: 'ConversationPage', |
| components: { |
| VueMonacoEditor, |
| MessageList |
| }, |
| |
| setup() { |
| const { t, locale } = useI18n(); |
| const { tm } = useModuleI18n('features/conversation'); |
| const customizerStore = useCustomizerStore(); |
| const confirmDialog = useConfirmDialog(); |
| |
| return { |
| t, |
| tm, |
| locale, |
| customizerStore, |
| confirmDialog |
| }; |
| }, |
| |
| data() { |
| return { |
| |
| conversations: [], |
| search: '', |
| headers: [], |
| selectedItems: [], |
| |
| |
| platformFilter: [], |
| messageTypeFilter: [], |
| lastAppliedFilters: null, |
| |
| |
| pagination: { |
| page: 1, |
| page_size: 20, |
| total: 0, |
| total_pages: 0 |
| }, |
| pageSizeOptions: [10, 20, 50, 100], |
| |
| |
| dialogView: false, |
| dialogEdit: false, |
| dialogDelete: false, |
| dialogBatchDelete: false, |
| |
| |
| selectedConversation: null, |
| conversationHistory: [], |
| |
| |
| editedItem: { |
| user_id: '', |
| cid: '', |
| title: '' |
| }, |
| |
| |
| valid: true, |
| |
| |
| loading: false, |
| showMessage: false, |
| message: '', |
| messageType: 'success', |
| |
| |
| isEditingHistory: false, |
| editedHistory: '', |
| savingHistory: false, |
| monacoEditor: null, |
| |
| commonStore: useCommonStore() |
| } |
| }, |
| |
| watch: { |
| |
| platformFilter() { |
| this.debouncedApplyFilters(); |
| }, |
| messageTypeFilter() { |
| this.debouncedApplyFilters(); |
| }, |
| search() { |
| this.debouncedApplyFilters(); |
| } |
| }, |
| |
| created() { |
| this.debouncedApplyFilters = debounce(() => { |
| |
| this.pagination.page = 1; |
| this.fetchConversations(); |
| }, 300); |
| }, |
| |
| computed: { |
| |
| tableHeaders() { |
| return [ |
| { title: this.tm('table.headers.title'), key: 'title', sortable: true }, |
| { title: this.tm('table.headers.cid'), key: 'cid', sortable: true, width: '100px' }, |
| { |
| title: this.tm('table.headers.umo'), |
| align: 'center', |
| children: [ |
| { title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' }, |
| { title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' }, |
| { title: this.tm('table.headers.sessionId'), key: 'sessionId', sortable: true, width: '100px' }, |
| ], |
| }, |
| { title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' }, |
| { title: this.tm('table.headers.updatedAt'), key: 'updated_at', sortable: true, width: '180px' }, |
| { title: this.tm('table.headers.actions'), key: 'actions', sortable: false, align: 'center' } |
| ]; |
| }, |
| |
| |
| availablePlatforms() { |
| const platforms = [] |
| |
| const tutorialMap = this.commonStore.tutorial_map; |
| for (const platform in tutorialMap) { |
| if (tutorialMap.hasOwnProperty(platform)) { |
| platforms.push({ |
| title: platform, |
| value: platform |
| }) |
| } |
| } |
| return platforms; |
| }, |
| |
| |
| messageTypeItems() { |
| return [ |
| { title: this.tm('messageTypes.group'), value: 'GroupMessage' }, |
| { title: this.tm('messageTypes.friend'), value: 'FriendMessage' }, |
| ]; |
| }, |
| |
| |
| currentFilters() { |
| const platforms = this.platformFilter.map(item => |
| typeof item === 'object' ? item.value : item |
| ); |
| return { |
| platforms: platforms, |
| messageTypes: this.messageTypeFilter, |
| search: this.search |
| }; |
| }, |
| |
| |
| isDark() { |
| console.log('isDark', this.customizerStore.uiTheme); |
| return this.customizerStore.uiTheme === 'PurpleThemeDark'; |
| }, |
| |
| |
| formattedMessages() { |
| return this.conversationHistory.map(msg => { |
| console.log('处理消息:', msg.role, msg.content); |
| |
| |
| const messageParts = this.convertContentToMessageParts(msg.content); |
| |
| if (msg.role === 'user') { |
| return { |
| content: { |
| type: 'user', |
| message: messageParts |
| } |
| }; |
| } else { |
| return { |
| content: { |
| type: 'bot', |
| message: messageParts |
| } |
| }; |
| } |
| }); |
| } |
| }, |
| |
| mounted() { |
| this.fetchConversations(); |
| }, |
| |
| methods: { |
| |
| onMonacoMounted(editor) { |
| this.monacoEditor = editor; |
| |
| editor.onDidChangeModelContent(() => { |
| try { |
| JSON.parse(this.editedHistory); |
| |
| editor.getAction('editor.action.formatDocument').run(); |
| } catch (e) { |
| |
| } |
| }); |
| }, |
| |
| |
| handleTableOptions(options) { |
| |
| if (options.itemsPerPage !== this.pagination.page_size) { |
| this.pagination.page_size = options.itemsPerPage; |
| this.pagination.page = 1; |
| this.fetchConversations(); |
| } |
| }, |
| |
| |
| parseSessionId(userId) { |
| if (!userId) return { platform: 'default', messageType: 'default', sessionId: '' }; |
| |
| |
| const parts = userId.split(':'); |
| |
| if (parts.length >= 3) { |
| return { |
| platform: parts[0] || 'default', |
| messageType: parts[1] || 'default', |
| sessionId: parts.slice(2).join(':') |
| }; |
| } |
| |
| return { platform: 'default', messageType: 'default', sessionId: userId }; |
| }, |
| |
| |
| getMessageTypeDisplay(messageType) { |
| const typeMap = { |
| 'GroupMessage': this.tm('messageTypes.group'), |
| 'FriendMessage': this.tm('messageTypes.friend'), |
| 'default': this.tm('messageTypes.unknown') |
| }; |
| |
| return typeMap[messageType] || typeMap.default; |
| }, |
| |
| |
| fetchConversations: (() => { |
| let controller = new AbortController(); |
| |
| return async function () { |
| |
| controller?.abort() |
| controller = new AbortController(); |
| |
| this.loading = true; |
| try { |
| |
| const params = { |
| page: this.pagination.page, |
| page_size: this.pagination.page_size |
| }; |
| |
| |
| if (this.platformFilter.length > 0) { |
| const platforms = this.platformFilter.map(item => |
| typeof item === 'object' ? item.value : item |
| ); |
| params.platforms = platforms.join(','); |
| } |
| |
| if (this.messageTypeFilter.length > 0) { |
| params.message_types = this.messageTypeFilter.join(','); |
| } |
| |
| if (this.search) { |
| params.search = this.search.trim(); |
| } |
| |
| |
| params.exclude_ids = 'astrbot'; |
| params.exclude_platforms = 'webchat'; |
| |
| const response = await axios.get('/api/conversation/list', { |
| signal: controller.signal, |
| params |
| }); |
| |
| this.lastAppliedFilters = { ...this.currentFilters }; |
| |
| if (response.data.status === "ok") { |
| const data = response.data.data; |
| |
| if (!data || !data.conversations) { |
| console.error('API 返回数据格式不符合预期:', data); |
| this.showErrorMessage(this.tm('messages.fetchError')); |
| return; |
| } |
| |
| |
| this.conversations = (data.conversations || []).map(conv => { |
| |
| conv.sessionInfo = this.parseSessionId(conv.user_id); |
| return conv; |
| }); |
| |
| |
| if (data.pagination) { |
| this.pagination = { |
| page: data.pagination.page || 1, |
| page_size: data.pagination.page_size || 20, |
| total: data.pagination.total || 0, |
| total_pages: data.pagination.total_pages || 1 |
| }; |
| } else { |
| console.warn('API 响应中没有分页信息'); |
| } |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.fetchError')); |
| } |
| } catch (error) { |
| if (axios.isCancel(error)) return; |
| |
| console.error('获取对话列表出错:', error); |
| if (error.response) { |
| console.error('错误响应数据:', error.response.data); |
| console.error('错误状态码:', error.response.status); |
| } |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError')); |
| } finally { |
| this.loading = false; |
| } |
| } |
| })(), |
| |
| |
| async viewConversation(item) { |
| this.selectedConversation = item; |
| this.loading = true; |
| this.isEditingHistory = false; |
| |
| try { |
| console.log(`正在请求对话详情,user_id=${item.user_id}, cid=${item.cid}`); |
| const response = await axios.post('/api/conversation/detail', { |
| user_id: item.user_id, |
| cid: item.cid |
| }); |
| |
| if (response.data.status === "ok") { |
| try { |
| const historyData = response.data.data.history || '[]'; |
| this.conversationHistory = JSON.parse(historyData); |
| this.editedHistory = JSON.stringify(this.conversationHistory, null, 2); |
| } catch (e) { |
| this.conversationHistory = []; |
| this.editedHistory = '[]'; |
| console.error('解析对话历史失败:', e); |
| } |
| this.dialogView = true; |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.historyError')); |
| } |
| } catch (error) { |
| console.error('获取对话详情出错:', error); |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.historyError')); |
| } finally { |
| this.loading = false; |
| } |
| }, |
| |
| |
| async saveHistoryChanges() { |
| if (!this.selectedConversation) return; |
| |
| this.savingHistory = true; |
| |
| try { |
| |
| let historyJson; |
| try { |
| historyJson = JSON.parse(this.editedHistory); |
| } catch (e) { |
| this.showErrorMessage(this.tm('messages.invalidJson')); |
| return; |
| } |
| |
| const response = await axios.post('/api/conversation/update_history', { |
| user_id: this.selectedConversation.user_id, |
| cid: this.selectedConversation.cid, |
| history: historyJson |
| }); |
| |
| if (response.data.status === "ok") { |
| this.conversationHistory = historyJson; |
| this.showSuccessMessage(this.tm('messages.historySaveSuccess')); |
| this.isEditingHistory = false; |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.historySaveError')); |
| } |
| } catch (error) { |
| console.error('更新对话历史出错:', error); |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.historySaveError')); |
| } finally { |
| this.savingHistory = false; |
| } |
| }, |
| |
| |
| async closeHistoryDialog() { |
| if (this.isEditingHistory) { |
| if (await askForConfirmationDialog(this.tm('dialogs.view.confirmClose'), this.confirmDialog)) { |
| this.dialogView = false; |
| } |
| } else { |
| this.dialogView = false; |
| } |
| }, |
| |
| |
| editConversation(item) { |
| this.selectedConversation = item; |
| this.editedItem = Object.assign({}, item); |
| this.dialogEdit = true; |
| }, |
| |
| |
| async saveConversation() { |
| if (!this.$refs.form.validate()) return; |
| |
| this.loading = true; |
| try { |
| const response = await axios.post('/api/conversation/update', { |
| user_id: this.editedItem.user_id, |
| cid: this.editedItem.cid, |
| title: this.editedItem.title |
| }); |
| |
| if (response.data.status === "ok") { |
| |
| const index = this.conversations.findIndex(item => item.user_id === this.editedItem.user_id && item.cid === this.editedItem.cid |
| ); |
| |
| if (index !== -1) { |
| this.conversations[index].title = this.editedItem.title; |
| } |
| |
| this.dialogEdit = false; |
| this.showSuccessMessage(this.tm('messages.saveSuccess')); |
| |
| |
| this.fetchConversations(); |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.saveError')); |
| } |
| } catch (error) { |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.saveError')); |
| } finally { |
| this.loading = false; |
| } |
| }, |
| |
| |
| confirmDeleteConversation(item) { |
| this.selectedConversation = item; |
| this.dialogDelete = true; |
| }, |
| |
| |
| async deleteConversation() { |
| this.loading = true; |
| try { |
| const response = await axios.post('/api/conversation/delete', { |
| user_id: this.selectedConversation.user_id, |
| cid: this.selectedConversation.cid |
| }); |
| |
| if (response.data.status === "ok") { |
| const index = this.conversations.findIndex(item => item.user_id === this.selectedConversation.user_id && item.cid === this.selectedConversation.cid |
| ); |
| |
| if (index !== -1) { |
| this.conversations.splice(index, 1); |
| } |
| |
| this.dialogDelete = false; |
| this.showSuccessMessage(this.tm('messages.deleteSuccess')); |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.deleteError')); |
| } |
| } catch (error) { |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.deleteError')); |
| } finally { |
| this.loading = false; |
| this.selectedItems = this.selectedItems.filter(item => |
| !(item.user_id === this.selectedConversation.user_id && item.cid === this.selectedConversation.cid) |
| ); |
| this.selectedConversation = null; |
| } |
| }, |
| |
| |
| onPageSizeChange() { |
| this.pagination.page = 1; |
| this.fetchConversations(); |
| }, |
| |
| |
| confirmBatchDelete() { |
| if (this.selectedItems.length === 0) { |
| this.showErrorMessage(this.tm('messages.noItemSelected')); |
| return; |
| } |
| this.dialogBatchDelete = true; |
| }, |
| |
| |
| removeFromSelection(item) { |
| const index = this.selectedItems.findIndex(selected => |
| selected.user_id === item.user_id && selected.cid === item.cid |
| ); |
| if (index !== -1) { |
| this.selectedItems.splice(index, 1); |
| } |
| }, |
| |
| |
| async batchDeleteConversations() { |
| if (this.selectedItems.length === 0) { |
| this.showErrorMessage(this.tm('messages.noItemSelected')); |
| return; |
| } |
| |
| this.loading = true; |
| try { |
| |
| const conversations = this.selectedItems.map(item => ({ |
| user_id: item.user_id, |
| cid: item.cid |
| })); |
| |
| const response = await axios.post('/api/conversation/delete', { |
| conversations: conversations |
| }); |
| |
| if (response.data.status === "ok") { |
| const result = response.data.data; |
| this.dialogBatchDelete = false; |
| this.selectedItems = []; |
| |
| |
| if (result.failed_count > 0) { |
| this.showErrorMessage( |
| this.tm('messages.batchDeletePartial', { |
| deleted: result.deleted_count, |
| failed: result.failed_count |
| }) |
| ); |
| } else { |
| this.showSuccessMessage( |
| this.tm('messages.batchDeleteSuccess', { |
| count: result.deleted_count |
| }) |
| ); |
| } |
| |
| |
| this.fetchConversations(); |
| } else { |
| this.showErrorMessage(response.data.message || this.tm('messages.batchDeleteError')); |
| } |
| } catch (error) { |
| console.error('批量删除对话出错:', error); |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.batchDeleteError')); |
| } finally { |
| this.loading = false; |
| } |
| }, |
| |
| |
| async exportConversations() { |
| if (this.selectedItems.length === 0) { |
| this.showErrorMessage(this.tm('messages.noItemSelectedForExport')); |
| return; |
| } |
| |
| this.loading = true; |
| try { |
| |
| const conversations = this.selectedItems.map(item => ({ |
| user_id: item.user_id, |
| cid: item.cid |
| })); |
| |
| const response = await axios.post('/api/conversation/export', { |
| conversations: conversations |
| }, { |
| responseType: 'blob' |
| }); |
| |
| |
| const url = window.URL.createObjectURL(response.data); |
| const link = document.createElement('a'); |
| link.href = url; |
| |
| |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); |
| const filename = `conversations_export_${timestamp}.jsonl`; |
| |
| link.setAttribute('download', filename); |
| document.body.appendChild(link); |
| link.click(); |
| |
| |
| link.remove(); |
| window.URL.revokeObjectURL(url); |
| |
| this.showSuccessMessage(this.tm('messages.exportSuccess')); |
| } catch (error) { |
| console.error(this.tm('messages.exportError'), error); |
| this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.exportError')); |
| } finally { |
| this.loading = false; |
| } |
| }, |
| |
| |
| formatTimestamp(timestamp) { |
| if (!timestamp) return this.tm('status.unknown'); |
| |
| const date = new Date(timestamp * 1000); |
| const locale = this.locale || 'zh-CN'; |
| return new Intl.DateTimeFormat(locale, { |
| year: 'numeric', |
| month: '2-digit', |
| day: '2-digit', |
| hour: '2-digit', |
| minute: '2-digit', |
| second: '2-digit', |
| hour12: false |
| }).format(date); |
| }, |
| |
| |
| showSuccessMessage(message) { |
| this.message = message; |
| this.messageType = 'success'; |
| this.showMessage = true; |
| }, |
| |
| |
| showErrorMessage(message) { |
| this.message = message; |
| this.messageType = 'error'; |
| this.showMessage = true; |
| }, |
| |
| |
| convertContentToMessageParts(content) { |
| const parts = []; |
| |
| if (typeof content === 'string') { |
| |
| if (content.trim()) { |
| parts.push({ |
| type: 'plain', |
| text: content |
| }); |
| } |
| } else if (Array.isArray(content)) { |
| |
| content.forEach(item => { |
| if (item.type === 'text' && item.text) { |
| parts.push({ |
| type: 'plain', |
| text: item.text |
| }); |
| } else if (item.type === 'image_url' && item.image_url?.url) { |
| parts.push({ |
| type: 'image', |
| embedded_url: item.image_url.url |
| }); |
| } |
| }); |
| } else if (typeof content === 'object' && content !== null) { |
| |
| const textParts = []; |
| for (const [key, value] of Object.entries(content)) { |
| if (typeof value === 'string' && value.trim()) { |
| textParts.push(value); |
| } |
| } |
| if (textParts.length > 0) { |
| parts.push({ |
| type: 'plain', |
| text: textParts.join('\n') |
| }); |
| } |
| } |
| |
| |
| if (parts.length === 0) { |
| parts.push({ |
| type: 'plain', |
| text: '' |
| }); |
| } |
| |
| return parts; |
| }, |
| |
| |
| extractTextFromContent(content) { |
| if (typeof content === 'string') { |
| return content; |
| } else if (Array.isArray(content)) { |
| return content.filter(item => item.type === 'text') |
| .map(item => item.text) |
| .join('\n'); |
| } else if (typeof content === 'object') { |
| return Object.values(content).filter(val => typeof val === 'string').join(''); |
| } |
| return ''; |
| }, |
| |
| |
| extractImagesFromContent(content) { |
| if (Array.isArray(content)) { |
| return content.filter(item => item.type === 'image_url') |
| .map(item => item.image_url?.url) |
| .filter(url => url); |
| } |
| return []; |
| } |
| } |
| } |
| </script> |
| |
| <style> |
| .actions-wrapper { |
| display: flex; |
| justify-content: flex-end; |
| gap: 8px; |
| } |
| |
| .action-button { |
| border-radius: 8px; |
| font-weight: 500; |
| } |
| |
| .monaco-editor-container { |
| height: 500px; |
| border-radius: 8px; |
| overflow: hidden; |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| } |
| |
| |
| .conversation-messages-container { |
| max-height: 500px; |
| overflow-y: auto; |
| padding: 8px; |
| border-radius: 8px; |
| background-color: #f9f9f9; |
| } |
| |
| |
| .v-theme--dark .conversation-messages-container { |
| background-color: #1e1e1e; |
| } |
| |
| |
| .conversation-detail-card { |
| max-height: 90vh; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .text-truncate { |
| display: inline-block; |
| |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| </style> |
| |