This commit is contained in:
Baobhan Sith
2025-05-28 20:24:30 +08:00
parent f022033b22
commit 27cb02b825
18 changed files with 186 additions and 56 deletions
@@ -62,6 +62,7 @@ import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickComma
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store';
import TagInput from './TagInput.vue';
import { useConfirmDialog } from '../composables/useConfirmDialog';
import { useAlertDialog } from '../composables/useAlertDialog'; // +++ 导入 useAlertDialog +++
const props = defineProps<{
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
@@ -71,6 +72,7 @@ const emit = defineEmits(['close']);
const { t } = useI18n();
const { showConfirmDialog } = useConfirmDialog();
const { showAlertDialog } = useAlertDialog(); // +++ 获取 showAlertDialog 函数 +++
const quickCommandsStore = useQuickCommandsStore();
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate tag store +++
const isSubmitting = ref(false);
@@ -144,8 +146,7 @@ const handleDeleteTag = async (tagId: number) => {
formData.tagIds.splice(index, 1);
}
} else {
// Optional: Show error notification if deletion fails
alert(t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' }));
showAlertDialog({ title: t('common.error', '错误'), message: t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' }) });
}
}
};
@@ -6,11 +6,13 @@ import { useI18n } from 'vue-i18n';
import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store';
import { useTagsStore } from '../stores/tags.store';
import { useConfirmDialog } from '../composables/useConfirmDialog';
import { useAlertDialog } from '../composables/useAlertDialog';
const { t } = useI18n();
const { t } = useI18n();
const router = useRouter();
const tagsStore = useTagsStore();
const { showConfirmDialog } = useConfirmDialog();
const { showAlertDialog } = useAlertDialog();
@@ -132,11 +134,9 @@ const handleDelete = async (conn: ConnectionInfo) => {
if (confirmed) {
const success = await connectionsStore.deleteConnection(conn.id);
if (!success) {
// 如果删除失败,显示 store 中的错误信息 (或自定义错误)
// 可以考虑使用更友好的提示方式,例如 toast 通知库
alert(t('connections.errors.deleteFailed', { error: connectionsStore.error || '未知错误' }));
showAlertDialog({ title: t('common.error'), message: t('connections.errors.deleteFailed', { error: connectionsStore.error || '未知错误' }) });
}
// 成功时列表会自动更新,无需额外操作
}
};
@@ -149,9 +149,9 @@ const handleDelete = async (conn: ConnectionInfo) => {
// 显示测试结果
if (result.success) {
alert(t('connections.test.success'));
showAlertDialog({ title: t('common.success'), message: t('connections.test.success') });
} else {
alert(t('connections.test.failed', { error: result.message || '未知错误' }));
showAlertDialog({ title: t('common.error'), message: t('connections.test.failed', { error: result.message || '未知错误' }) });
}
};
@@ -455,7 +455,6 @@ const handleItemAction = (item: FileListItem) => {
if (!absolutePath) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] sftp:realpath:success for ${itemPath} missing absolutePath. Payload:`, payload);
alert(`Failed to resolve symbolic link "${item.filename}": Server did not return a valid path.`);
return;
}
if (!targetType) {
@@ -475,14 +474,12 @@ const handleItemAction = (item: FileListItem) => {
const resolvedPathInfo = payload.absolutePath ? ` (Resolved path: ${payload.absolutePath})` : '';
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to get realpath or target type for symlink '${itemPath}': ${serverErrorMsg}${resolvedPathInfo}`);
alert(`Failed to resolve symbolic link "${item.filename}": ${serverErrorMsg}.${resolvedPathInfo} Please ensure the target exists and you have permissions.`);
}
});
timeoutId = setTimeout(() => {
cleanupListeners();
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Timeout getting realpath for symlink '${itemPath}' (ID: ${requestId}).`);
alert(`Timeout resolving symbolic link "${item.filename}".`);
}, 10000); // 10 秒超时
wsSend({ type: 'sftp:realpath', requestId: requestId, payload: { path: itemPath } });
return; // Handled by async callbacks
@@ -620,13 +617,7 @@ const handleModalConfirm = (value?: string) => {
case 'newFile':
if (value) {
if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) {
// alert(t('fileManager.errors.fileExists', { name: value })); // Consider using modal for this error too
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] File ${value} already exists. Modal should prevent this.`);
// Re-open modal or show error in modal
// For now, we rely on modal's internal logic or a notification system
// To prevent closing, we can avoid calling handleModalClose here if an error occurs.
// However, the current modal design closes on confirm.
// A more robust solution would be for the modal to emit 'error' or handle validation internally.
return; // Prevent closing if error
}
manager.createFile(value);
@@ -635,7 +626,6 @@ const handleModalConfirm = (value?: string) => {
case 'newFolder':
if (value) {
if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) {
// alert(t('fileManager.errors.folderExists', { name: value }));
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Folder ${value} already exists. Modal should prevent this.`);
return; // Prevent closing if error
}
@@ -758,20 +748,17 @@ const triggerFileUpload = () => { fileInputRef.value?.click(); };
const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileListItem 数组
// 恢复使用 props.wsDeps.isConnected
if (!props.wsDeps.isConnected.value) {
alert(t('fileManager.errors.notConnected'));
return;
}
// connectionId 仍然从 props 获取
const currentConnectionId = props.dbConnectionId;
if (!currentConnectionId) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`);
alert(t('fileManager.errors.missingConnectionId'));
return;
}
// 修改:简化检查
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: SFTP manager is not available.`);
alert(t('fileManager.errors.sftpManagerNotFound'));
return;
}
@@ -808,18 +795,15 @@ const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileList
// +++ 文件夹下载触发器 +++
const triggerDownloadDirectory = (item: FileListItem) => {
if (!props.wsDeps.isConnected.value) {
alert(t('fileManager.errors.notConnected'));
return;
}
const currentConnectionId = props.dbConnectionId;
if (!currentConnectionId) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: Missing connection ID.`);
alert(t('fileManager.errors.missingConnectionId'));
return;
}
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: SFTP manager is not available.`);
alert(t('fileManager.errors.sftpManagerNotFound'));
return;
}
@@ -878,16 +862,10 @@ const triggerDownloadDirectory = (item: FileListItem) => {
} catch (e2) { /* ignore */}
}
if (response.status === 404) {
alert(t('fileManager.errors.downloadDirectoryNotImplemented', 'Directory download feature is not yet implemented on the server.'));
} else {
alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: ${errorMsg}`);
}
}
})
.catch(error => {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Network error during directory download:`, error);
alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: Network error.`);
});
};
@@ -7,6 +7,7 @@ import { storeToRefs } from 'pinia';
import draggable from 'vuedraggable';
import LayoutNodeEditor from './LayoutNodeEditor.vue';
import { useConfirmDialog } from '../composables/useConfirmDialog';
import { useAlertDialog } from '../composables/useAlertDialog';
@@ -27,6 +28,7 @@ const layoutStore = useLayoutStore();
const settingsStore = useSettingsStore(); // +++ Initialize settings store +++
const { layoutLockedBoolean } = storeToRefs(settingsStore); // +++ Get reactive state +++
const { showConfirmDialog } = useConfirmDialog();
const { showAlertDialog } = useAlertDialog();
// --- State ---
const localLayoutTree: Ref<LayoutNode | null> = ref(null);
@@ -180,9 +182,6 @@ const handleLayoutLockChange = async () => { // Removed event parameter
// but the button's appearance relies on layoutLockedBoolean which comes from the store.
} catch (error) {
console.error('[LayoutConfigurator] Failed to update layout lock setting:', error);
// Optionally show an error message
// No UI element state to revert directly here, the button state depends on layoutLockedBoolean
alert(t('layoutConfigurator.lockUpdateError', '更新布局锁定状态失败。'));
}
};
@@ -219,8 +218,6 @@ const saveLayout = async () => { // Make async
console.log('[LayoutConfigurator] Layout saved successfully, dialog closed.');
} catch (error) {
console.error('[LayoutConfigurator] Error saving layout:', error);
// Optionally notify the user about the error
alert(t('layoutConfigurator.saveError', '保存布局时出错,请稍后再试。')); // Keep default text for now
}
};
@@ -3,10 +3,12 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useProxiesStore, ProxyInfo } from '../stores/proxies.store';
import { useConfirmDialog } from '../composables/useConfirmDialog';
import { useAlertDialog } from '../composables/useAlertDialog';
const { t } = useI18n();
const proxiesStore = useProxiesStore();
const { showConfirmDialog } = useConfirmDialog();
const { showAlertDialog } = useAlertDialog();
const { proxies, isLoading, error } = storeToRefs(proxiesStore);
//
@@ -21,7 +23,7 @@ const handleDelete = async (proxy: ProxyInfo) => {
if (confirmed) {
const success = await proxiesStore.deleteProxy(proxy.id);
if (!success) {
alert(t('proxies.errors.deleteFailed', { error: proxiesStore.error || '未知错误' }));
showAlertDialog({ title: t('common.error'), message: t('proxies.errors.deleteFailed', { error: proxiesStore.error || t('common.unknownError') }) });
}
}
};
@@ -423,7 +423,6 @@ const handleMenuAction = async (action: 'add' | 'edit' | 'delete' | 'clone') =>
.catch(error => {
// UI
console.error("Cloning failed in component:", error);
// alert(t('connections.errors.cloneFailed', { error: connectionsStore.error || '' })); // store
});
}
}
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n';
interface Props {
visible: boolean;
title: string;
message: string;
okText?: string;
}
const props = defineProps<Props>();
const emit = defineEmits(['ok', 'update:visible']);
const { t } = useI18n();
const dialogVisible = ref(props.visible);
watch(() => props.visible, (newValue) => {
dialogVisible.value = newValue;
});
watch(dialogVisible, (newValue) => {
if (newValue !== props.visible) {
emit('update:visible', newValue);
}
});
const handleOk = () => {
emit('ok');
// ""storevisiblestore
// emit('update:visible', false);
};
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && dialogVisible.value) {
handleOk();
}
};
watch(dialogVisible, (isVisible) => {
if (isVisible) {
document.addEventListener('keydown', handleKeydown);
} else {
document.removeEventListener('keydown', handleKeydown);
}
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown);
});
</script>
<template>
<teleport to="body">
<div
v-if="dialogVisible"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-overlay p-4"
@mousedown.self="handleOk"
>
<div
class="bg-background text-foreground p-5 rounded-lg shadow-xl border border-border w-full max-w-md flex flex-col"
role="dialog"
aria-modal="true"
:aria-labelledby="props.title"
>
<h3 class="text-xl font-semibold mb-4 text-center flex-shrink-0" :id="props.title">
{{ props.title }}
</h3>
<div class="flex-grow mb-6 text-sm">
<p class="text-text-secondary text-center whitespace-pre-wrap">
{{ props.message }}
</p>
</div>
<div class="flex justify-end gap-3 flex-shrink-0">
<button
@click="handleOk"
type="button"
class="px-4 py-2 text-sm font-medium text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-background-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center bg-primary hover:bg-primary-hover focus:ring-primary"
>
{{ props.okText || t('common.ok', '确定') }}
</button>
</div>
</div>
</div>
</teleport>
</template>
@@ -64,7 +64,7 @@ onBeforeUnmount(() => {
<teleport to="body">
<div
v-if="dialogVisible"
class="fixed inset-0 z-[100] flex items-center justify-center bg-overlay p-4"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-overlay p-4"
@mousedown.self="handleCancel"
>
<div