fix: replace native confirm dialogs in dashboard

- Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback.

- Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper.

- Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron.
This commit is contained in:
邹永赫
2026-02-10 15:23:54 +09:00
parent ff73a55210
commit 2e5841aa07
14 changed files with 104 additions and 58 deletions

View File

@@ -141,9 +141,10 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { inject, ref } from 'vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import type { Session } from '@/composables/useSessions';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
import StyledMenu from '@/components/shared/StyledMenu.vue';
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
@@ -183,6 +184,8 @@ const emit = defineEmits<{
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
const sidebarCollapsed = ref(true);
const showProviderConfigDialog = ref(false);
@@ -199,10 +202,10 @@ function toggleSidebar() {
localStorage.setItem('sidebarCollapsed', JSON.stringify(sidebarCollapsed.value));
}
function handleDeleteConversation(session: Session) {
async function handleDeleteConversation(session: Session) {
const sessionTitle = session.display_name || tm('conversation.newConversation');
const message = tm('conversation.confirmDelete', { name: sessionTitle });
if (window.confirm(message)) {
if (await askForConfirmation(message, confirmDialog)) {
emit('deleteConversation', session.session_id);
}
}
@@ -359,4 +362,3 @@ function handleDeleteConversation(session: Session) {
justify-content: center;
}
</style>

View File

@@ -42,8 +42,9 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { inject, ref } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
export interface Project {
project_id: string;
@@ -72,6 +73,8 @@ const emit = defineEmits<{
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
const expanded = ref(props.initialExpanded);
// 从 localStorage 读取项目展开状态
@@ -85,9 +88,9 @@ function toggleExpanded() {
localStorage.setItem('projectsExpanded', JSON.stringify(expanded.value));
}
function handleDeleteProject(project: Project) {
async function handleDeleteProject(project: Project) {
const message = tm('project.confirmDelete', { title: project.title });
if (window.confirm(message)) {
if (await askForConfirmation(message, confirmDialog)) {
emit('deleteProject', project.project_id);
}
}

View File

@@ -45,8 +45,10 @@
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import type { Project } from '@/components/chat/ProjectList.vue';
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog';
interface Session {
session_id: string;
@@ -69,14 +71,16 @@ const emit = defineEmits<{
const { tm } = useModuleI18n('features/chat');
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined);
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleString();
}
function handleDeleteSession(session: Session) {
async function handleDeleteSession(session: Session) {
const sessionTitle = session.display_name || tm('conversation.newConversation');
const message = tm('conversation.confirmDelete', { name: sessionTitle });
if (window.confirm(message)) {
if (await askForConfirmation(message, confirmDialog)) {
emit('deleteSession', session.session_id);
}
}

View File

@@ -218,6 +218,7 @@ import axios from 'axios';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
import ItemCard from '@/components/shared/ItemCard.vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
export default {
name: 'McpServersSection',
@@ -382,18 +383,21 @@ export default {
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
}
},
deleteServer(server) {
async deleteServer(server) {
const serverName = server.name || server;
if (confirm(this.tm('dialogs.confirmDelete', { name: serverName }))) {
axios.post('/api/tools/mcp/delete', { name: serverName })
.then(response => {
this.getServers();
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
})
.catch(error => {
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
const message = this.tm('dialogs.confirmDelete', { name: serverName });
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) {
return;
}
axios.post('/api/tools/mcp/delete', { name: serverName })
.then(response => {
this.getServers();
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
})
.catch(error => {
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
},
editServer(server) {
const configCopy = { ...server };

View File

@@ -367,13 +367,16 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, inject, watch } from 'vue'
import axios from 'axios'
import { useI18n } from '@/i18n/composables'
import { askForConfirmation, resolveConfirmDialog } from '@/utils/confirmDialog'
import WaitingForRestart from './WaitingForRestart.vue'
const { t } = useI18n()
const confirmDialog = resolveConfirmDialog(inject('$confirm', undefined))
const isOpen = ref(false)
const activeTab = ref('export')
const wfr = ref(null)
@@ -844,7 +847,7 @@ const restoreFromList = async (filename) => {
// 删除备份
const deleteBackup = async (filename) => {
if (!confirm(t('features.settings.backup.list.confirmDelete'))) return
if (!(await askForConfirmation(t('features.settings.backup.list.confirmDelete'), confirmDialog))) return
try {
const response = await axios.post('/api/backup/delete', { filename })
@@ -992,4 +995,4 @@ defineExpose({ open })
.non-interactive-chip:hover {
box-shadow: none !important;
}
</style>
</style>

View File

@@ -307,6 +307,7 @@
<script>
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
export default {
name: 'PersonaForm',
@@ -596,8 +597,8 @@ export default {
async deletePersona() {
if (!this.editingPersona) return;
if (!confirm(this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }))) {
if (!(await askForConfirmationDialog(this.tm('messages.deleteConfirm', { id: this.editingPersona.persona_id }), resolveConfirmDialog(this.$confirm)))) {
return;
}

View File

@@ -1,6 +1,7 @@
import { ref, computed, inject, onMounted, nextTick, watch } from 'vue'
import axios from 'axios'
import { getProviderIcon } from '@/utils/providerUtils'
import { askForConfirmation as askForConfirmationDialog, type ConfirmDialogHandler } from '@/utils/confirmDialog'
export interface UseProviderSourcesOptions {
defaultTab?: string
@@ -37,25 +38,10 @@ export function resolveDefaultTab(value?: string) {
export function useProviderSources(options: UseProviderSourcesOptions) {
const { tm, showMessage } = options
type ConfirmDialogOptions = {
title?: string
message?: string
}
type ConfirmDialogHandler = (options: ConfirmDialogOptions) => Promise<boolean>
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined)
async function askForConfirmation(message: string) {
if (confirmDialog) {
try {
return await confirmDialog({ message })
} catch {
return false
}
}
return window.confirm(message)
return askForConfirmationDialog(message, confirmDialog)
}
// ===== State =====

View File

@@ -0,0 +1,29 @@
export type ConfirmDialogOptions = {
title?: string
message?: string
}
export type ConfirmDialogHandler = (options: ConfirmDialogOptions) => Promise<boolean>
export function resolveConfirmDialog(candidate: unknown): ConfirmDialogHandler | undefined {
if (typeof candidate === 'function') {
return candidate as ConfirmDialogHandler
}
return undefined
}
export async function askForConfirmation(
message: string,
confirmDialog?: ConfirmDialogHandler
): Promise<boolean> {
if (confirmDialog) {
try {
return await confirmDialog({ message })
} catch {
return false
}
}
return window.confirm(message)
}

View File

@@ -190,6 +190,7 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
export default {
name: 'ConfigPage',
@@ -473,8 +474,9 @@ export default {
this.createNewConfig();
}
},
confirmDeleteConfig(config) {
if (confirm(this.tm('configManagement.confirmDelete').replace('{name}', config.name))) {
async confirmDeleteConfig(config) {
const message = this.tm('configManagement.confirmDelete').replace('{name}', config.name);
if (await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm))) {
this.deleteConfig(config.id);
}
},
@@ -658,4 +660,4 @@ export default {
padding: 0;
border-radius: 0 0 16px 16px;
}
</style>
</style>

View File

@@ -333,6 +333,7 @@ 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, resolveConfirmDialog } from '@/utils/confirmDialog';
export default {
name: 'ConversationPage',
@@ -744,9 +745,9 @@ export default {
},
// 关闭对话历史对话框
closeHistoryDialog() {
async closeHistoryDialog() {
if (this.isEditingHistory) {
if (confirm(this.tm('dialogs.view.confirmClose'))) {
if (await askForConfirmationDialog(this.tm('dialogs.view.confirmClose'), resolveConfirmDialog(this.$confirm))) {
this.dialogView = false;
}
} else {
@@ -1133,4 +1134,4 @@ export default {
transform: translateY(0);
}
}
</style>
</style>

View File

@@ -197,6 +197,7 @@ import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
export default {
name: 'PlatformPage',
@@ -451,15 +452,18 @@ export default {
});
},
deletePlatform(platform) {
if (confirm(`${this.messages.deleteConfirm} ${platform.id}?`)) {
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
this.getConfig();
this.showSuccess(res.data.message || this.messages.deleteSuccess);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
});
async deletePlatform(platform) {
const message = `${this.messages.deleteConfirm} ${platform.id}?`;
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) {
return;
}
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
this.getConfig();
this.showSuccess(res.data.message || this.messages.deleteSuccess);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
});
},
platformStatusChange(platform) {

View File

@@ -522,6 +522,7 @@
<script>
import axios from 'axios'
import { useI18n, useModuleI18n } from '@/i18n/composables'
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog'
export default {
name: 'SessionManagementPage',
@@ -1503,7 +1504,8 @@ export default {
},
async deleteGroup(group) {
if (!confirm(`确定要删除分组 "${group.name}" 吗?`)) return
const message = `确定要删除分组 "${group.name}" 吗?`
if (!(await askForConfirmationDialog(message, resolveConfirmDialog(this.$confirm)))) return
try {
const response = await axios.post('/api/session/group/delete', { id: group.id })

View File

@@ -240,14 +240,17 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, inject, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
import { askForConfirmation, type ConfirmDialogHandler } from '@/utils/confirmDialog'
const { tm: t } = useModuleI18n('features/knowledge-base/document')
const route = useRoute()
const confirmDialog = inject<ConfirmDialogHandler | undefined>('$confirm', undefined)
const kbId = ref(route.params.kbId as string)
const docId = ref(route.params.docId as string)
@@ -356,7 +359,7 @@ const viewChunk = (chunk: any) => {
// 删除分块
const deleteChunk = async (chunk: any) => {
if (!confirm(t('chunks.deleteConfirm'))) return
if (!(await askForConfirmation(t('chunks.deleteConfirm'), confirmDialog))) return
try {
const response = await axios.post('/api/kb/chunk/delete', {
chunk_id: chunk.chunk_id,

View File

@@ -260,6 +260,7 @@ import PersonaCard from './PersonaCard.vue';
import PersonaForm from '@/components/shared/PersonaForm.vue';
import CreateFolderDialog from './CreateFolderDialog.vue';
import MoveToFolderDialog from './MoveToFolderDialog.vue';
import { askForConfirmation as askForConfirmationDialog, resolveConfirmDialog } from '@/utils/confirmDialog';
import type { Folder, FolderTreeNode } from '@/components/folder/types';
@@ -420,7 +421,8 @@ export default defineComponent({
},
async confirmDeletePersona(persona: Persona) {
if (!confirm(this.tm('messages.deleteConfirm', { id: persona.persona_id }))) {
const confirmDialog = resolveConfirmDialog((this as any).$confirm);
if (!(await askForConfirmationDialog(this.tm('messages.deleteConfirm', { id: persona.persona_id }), confirmDialog))) {
return;
}