feat: 状态监视器添加IP地址显示

This commit is contained in:
Baobhan Sith
2025-05-15 00:26:46 +08:00
parent 1e7e9d1c04
commit 0e396a92fa
60 changed files with 518 additions and 352 deletions
+2 -8
View File
@@ -10,17 +10,11 @@ import { useLayoutStore } from './stores/layout.store';
import { useFocusSwitcherStore } from './stores/focusSwitcher.store';
import { useSessionStore } from './stores/session.store';
import { storeToRefs } from 'pinia';
// 导入通知显示组件
import UINotificationDisplay from './components/UINotificationDisplay.vue';
// 导入文件编辑器弹窗组件
import FileEditorOverlay from './components/FileEditorOverlay.vue';
// 导入样式自定义器组件
import StyleCustomizer from './components/StyleCustomizer.vue';
// +++ 导入焦点切换配置器组件 +++
import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue';
// +++ 导入 RDP 模态框组件 +++
import RemoteDesktopModal from './components/RemoteDesktopModal.vue';
// +++ 导入 VNC 模态框组件 +++
import VncModal from './components/VncModal.vue';
const { t } = useI18n();
@@ -69,7 +63,7 @@ onMounted(() => {
// Use setTimeout to ensure styles are applied and elements have dimensions
setTimeout(updateUnderline, 100);
// +++ 添加全局 Alt 键监听器 +++
// +++ 全局 Alt 键监听器 +++
window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++
window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++
@@ -86,7 +80,7 @@ onMounted(() => {
layoutStore.loadHeaderVisibility();
});
// +++ 添加卸载钩子以移除监听器 +++
// +++ 卸载钩子以移除监听器 +++
onUnmounted(() => {
window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++
window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++
@@ -59,8 +59,8 @@
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import new tag store +++
import TagInput from './TagInput.vue'; // +++ Import TagInput component (assuming it exists) +++
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store';
import TagInput from './TagInput.vue';
const props = defineProps<{
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
@@ -109,12 +109,12 @@ onMounted(() => {
// --- Tag Creation Handling ---
// Assuming TagInput emits 'create-tag' with the tag name
const handleCreateTag = async (tagName: string) => {
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`);
if (!tagName || tagName.trim().length === 0) return;
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`);
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
if (newTag && !formData.tagIds.includes(newTag.id)) {
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`);
// Add the new tag's ID to the selected list
formData.tagIds.push(newTag.id);
}
@@ -122,12 +122,12 @@ const handleCreateTag = async (tagName: string) => {
// --- Tag Deletion Handling ---
const handleDeleteTag = async (tagId: number) => {
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`);
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
if (!tagToDelete) return;
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`);
const success = await quickCommandTagsStore.deleteTag(tagId);
if (success) {
// If deletion is successful, TagInput's availableTags will update,
@@ -135,7 +135,7 @@ const handleDeleteTag = async (tagId: number) => {
// We also need to remove it from the local formData.tagIds if it was selected.
const index = formData.tagIds.indexOf(tagId);
if (index > -1) {
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); // +++ 添加日志 +++
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`);
formData.tagIds.splice(index, 1);
}
} else {
@@ -46,12 +46,12 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 引入 UI 通知 store
import { useI18n } from 'vue-i18n'; // 引入 i18n
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
import { useI18n } from 'vue-i18n';
const commandHistoryStore = useCommandHistoryStore();
const uiNotificationsStore = useUiNotificationsStore(); // 实例化 UI 通知 store
const { t } = useI18n(); // 使用 i18n
const uiNotificationsStore = useUiNotificationsStore();
const { t } = useI18n();
const hoveredItemId = ref<number | null>(null);
const listContainer = ref<HTMLElement | null>(null);
@@ -2,17 +2,17 @@
import { ref, watch, nextTick, onMounted, onBeforeUnmount, defineExpose, computed, defineOptions } from 'vue'; // Import defineOptions
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useSessionStore } from '../stores/session.store'; // +++ 导入 Session Store +++
import { useSessionStore } from '../stores/session.store';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
import { useSettingsStore } from '../stores/settings.store';
import { useQuickCommandsStore } from '../stores/quickCommands.store';
import { useCommandHistoryStore } from '../stores/commandHistory.store';
import QuickCommandsModal from './QuickCommandsModal.vue'; // +++ Import the modal component +++
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue'; // +++ Import the new modal +++
import { useFileEditorStore } from '../stores/fileEditor.store'; // +++ Import File Editor Store +++
import QuickCommandsModal from './QuickCommandsModal.vue';
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue';
import { useFileEditorStore } from '../stores/fileEditor.store';
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
// Disable attribute inheritance as this component has multiple root nodes (div + modal)
defineOptions({ inheritAttrs: false });
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
@@ -1,15 +1,13 @@
<script setup lang="ts">
import { computed, type PropType, ref, watch, defineExpose, onMounted, onBeforeUnmount, nextTick } from 'vue'; // 添加 nextTick
import { useI18n } from 'vue-i18n';
// import { storeToRefs } from 'pinia'; // 移除 storeToRefs
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件 (路径确认无误)
// import { useFileEditorStore } from '../stores/fileEditor.store'; // 移除 Store 导入
import type { FileTab } from '../stores/fileEditor.store'; // 保留类型导入
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
import { useSessionStore } from '../stores/session.store'; // +++ 导入会话 Store +++
import { useSettingsStore } from '../stores/settings.store'; // +++ 导入设置 Store +++
import { storeToRefs } from 'pinia'; // +++ 导入 storeToRefs +++
import MonacoEditor from './MonacoEditor.vue';
import FileEditorTabs from './FileEditorTabs.vue';
import type { FileTab } from '../stores/fileEditor.store';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
import { useSessionStore } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store';
import { storeToRefs } from 'pinia';
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
const { t } = useI18n();
@@ -228,7 +226,7 @@ let unregisterFocusFn: (() => void) | null = null; // 保存注销函数
onMounted(() => {
// 注册动作并保存返回的注销函数
unregisterFocusFn = focusSwitcherStore.registerFocusAction('fileEditorActive', focusActiveEditor);
// +++ 添加键盘事件监听器 +++
// +++ 键盘事件监听器 +++
window.addEventListener('keydown', handleKeyDown);
});
@@ -45,7 +45,7 @@ const {
closeOtherTabs, // 修正:移除 Global 后缀
closeTabsToTheRight, // 修正:移除 Global 后缀
closeTabsToTheLeft, // 修正:移除 Global 后缀
changeEncoding: changeGlobalEncoding, // +++ 添加全局编码更改 action +++
changeEncoding: changeGlobalEncoding, // +++ 全局编码更改 action +++
} = fileEditorStore;
// 会话 Store Actions (用于非共享模式)
@@ -58,7 +58,7 @@ const {
closeOtherTabsInSession,
closeTabsToTheRightInSession,
closeTabsToTheLeftInSession,
changeEncodingInSession, // +++ 添加会话编码更改 action +++
changeEncodingInSession, // +++ 会话编码更改 action +++
} = sessionStore;
// --- 移除本地文件状态 ---
@@ -646,7 +646,7 @@ const triggerDownloadDirectory = (item: FileListItem) => {
// +++ 添加压缩/解压处理函数 +++
// +++ 压缩/解压处理函数 +++
const handleCompress = (items: FileListItem[], format: CompressFormat) => {
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot compress: SFTP manager not available.`);
@@ -800,7 +800,7 @@ watch(sortDirection, () => {
const saveLayoutSettings = () => {
// 确保 colWidths.value 是普通对象,而不是 Proxy
const widthsToSave = JSON.parse(JSON.stringify(colWidths.value));
// +++ 添加日志:记录保存的值 +++
// +++ 日志:记录保存的值 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering saveLayoutSettings: multiplier=${rowSizeMultiplier.value}, widths=${JSON.stringify(widthsToSave)}`);
settingsStore.updateFileManagerLayoutSettings(rowSizeMultiplier.value, widthsToSave);
};
@@ -819,7 +819,7 @@ watchEffect(() => {
const storeMultiplier = fileManagerRowSizeMultiplierNumber.value;
const storeWidths = fileManagerColWidthsObject.value;
// +++ 添加日志:记录从 store 获取的值 +++
// +++ 日志:记录从 store 获取的值 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] watchEffect triggered. Store values: multiplier=${storeMultiplier}, widths=${JSON.stringify(storeWidths)}`);
// 只有当 store 加载完成并提供了有效值时才更新
@@ -829,7 +829,7 @@ watchEffect(() => {
const currentWidthsString = JSON.stringify(colWidths.value);
const storeWidthsString = JSON.stringify(storeWidths);
// +++ 添加日志:记录当前值和 store 值,以及是否更新 +++
// +++ 日志:记录当前值和 store 值,以及是否更新 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Multiplier=${currentMultiplier}, Store Multiplier=${storeMultiplier}. Update needed: ${storeMultiplier !== currentMultiplier}`);
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Widths=${currentWidthsString}, Store Widths=${storeWidthsString}. Update needed: ${storeWidthsString !== currentWidthsString}`);
@@ -851,7 +851,7 @@ watchEffect(() => {
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Column widths updated from store: ${JSON.stringify(updatedWidths)}`);
}
} else {
// +++ 添加日志:记录等待 store 加载 +++
// +++ 日志:记录等待 store 加载 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Waiting for valid layout settings from store... Store Multiplier=${storeMultiplier}, Store Widths Keys=${Object.keys(storeWidths).length}`);
}
});
@@ -1103,7 +1103,7 @@ const stopResize = () => {
document.body.style.cursor = '';
document.body.style.userSelect = '';
// +++ 在调整结束后保存列宽 +++
// +++ 添加日志:记录触发保存 +++
// +++ 日志:记录触发保存 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] stopResize triggered saveLayoutSettings.`);
saveLayoutSettings();
}
@@ -1240,7 +1240,7 @@ const handleWheel = (event: WheelEvent) => {
// console.log(`Row size multiplier: ${rowSizeMultiplier.value}`); // 调试日志
// +++ 在行大小变化后保存设置 +++
if (rowSizeMultiplier.value !== oldMultiplier) {
// +++ 添加日志:记录触发保存 +++
// +++ 日志:记录触发保存 +++
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] handleWheel triggered saveLayoutSettings.`);
saveLayoutSettings();
}
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, type PropType } from 'vue';
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu'; // 导入菜单项类型
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu';
import { onUnmounted } from 'vue';
import { useDeviceDetection } from '../composables/useDeviceDetection';
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { ref, computed, watch, type Ref, nextTick } from 'vue'; // Import nextTick
import { ref, computed, watch, type Ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
import { useSettingsStore } from '../stores/settings.store'; // +++ Import settings store +++
import { storeToRefs } from 'pinia'; // +++ Import storeToRefs +++
import { useSettingsStore } from '../stores/settings.store';
import { storeToRefs } from 'pinia';
import draggable from 'vuedraggable';
import LayoutNodeEditor from './LayoutNodeEditor.vue';
// +++ Import a switch component if available, otherwise use checkbox +++
// Assuming a simple checkbox for now
// --- Props ---
const props = defineProps({
@@ -1,17 +1,17 @@
<script setup lang="ts">
import type { ConnectionInfo } from '../stores/connections.store'; // +++ ConnectionInfo +++
import type { ConnectionInfo } from '../stores/connections.store';
import { computed, defineAsyncComponent, type PropType, type Component, ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; // <-- Import useI18n
// font-awesome
import { useI18n } from 'vue-i18n';
import '@fortawesome/fontawesome-free/css/all.min.css';
import { Splitpanes, Pane } from 'splitpanes';
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
import { useSessionStore } from '../stores/session.store';
import { useFileEditorStore } from '../stores/fileEditor.store'; // <-- Import FileEditorStore
import { useSettingsStore } from '../stores/settings.store'; // +++ Import SettingsStore +++
import { useSidebarResize } from '../composables/useSidebarResize'; // +++ Import useSidebarResize +++
import { useFileEditorStore } from '../stores/fileEditor.store';
import { useSettingsStore } from '../stores/settings.store';
import { useSidebarResize } from '../composables/useSidebarResize';
import { storeToRefs } from 'pinia';
// import { defineEmits } from 'vue'; // --- ---
// --- Props ---
const props = defineProps({
@@ -267,7 +267,7 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
// --- Methods ---
// Splitpanes
const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string]: any }> }) => {
// +++ +++
// +++ +++
// +++ Log the entire layoutNode object if ID is undefined +++
if (props.layoutNode && typeof props.layoutNode.id === 'undefined') {
console.warn(`[LayoutRenderer DEBUG] handlePaneResize triggered but props.layoutNode.id is undefined. Full layoutNode prop:`, JSON.parse(JSON.stringify(props.layoutNode)));
@@ -291,7 +291,7 @@ const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string
size: paneInfo.size
}));
// +++ store action +++
// +++ store action +++
// console.log(`[LayoutRenderer DEBUG] Calling layoutStore.updateNodeSizes for node ID: ${props.layoutNode.id} with sizes:`, JSON.parse(JSON.stringify(childrenSizes)));
// store action
layoutStore.updateNodeSizes(props.layoutNode.id, childrenSizes);
@@ -6,7 +6,7 @@
import { ref, onMounted, onBeforeUnmount, watch, defineExpose } from 'vue';
import * as monaco from 'monaco-editor';
const localFontSize = ref(14); // 14
const localFontSize = ref(14); // 14
const props = defineProps({
modelValue: {
@@ -73,7 +73,7 @@
import { ref, onMounted, computed } from 'vue';
import { useNotificationsStore } from '../stores/notifications.store';
import { NotificationSetting, NotificationChannelType, NotificationEvent } from '../types/server.types';
import NotificationSettingForm from './NotificationSettingForm.vue'; // Import the form component
import NotificationSettingForm from './NotificationSettingForm.vue';
import { useI18n } from 'vue-i18n';
const store = useNotificationsStore();
@@ -2,7 +2,7 @@
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '../stores/settings.store';
import { useConnectionsStore } from '../stores/connections.store'; // +++ Import connections store +++
import { useConnectionsStore } from '../stores/connections.store';
// @ts-ignore - guacamole-common-js
import Guacamole from 'guacamole-common-js';
import apiClient from '../utils/apiClient';
@@ -2,10 +2,10 @@
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSshKeysStore } from '../stores/sshKeys.store';
import SshKeyManagementModal from './SshKeyManagementModal.vue'; // Import the modal
import SshKeyManagementModal from './SshKeyManagementModal.vue';
const props = defineProps<{
modelValue: number | null; // The selected ssh_key_id (v-model)
modelValue: number | null;
}>();
const emit = defineEmits(['update:modelValue']); // Removed 'use-direct-input' event
@@ -40,11 +40,11 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed, type PropType } from 'vue'; // PropType
import { ref, watch, onMounted, computed, type PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import { Line } from 'vue-chartjs';
import { useSessionStore } from '../stores/session.store'; // sessionStore
import { storeToRefs } from 'pinia'; // storeToRefs
import { useSessionStore } from '../stores/session.store';
import { storeToRefs } from 'pinia';
import {
Chart as ChartJS,
Title,
@@ -25,6 +25,19 @@
<!-- 状态网格 -->
<div v-else class="status-grid grid gap-3">
<!-- IP 地址 (如果启用) -->
<div v-if="statusMonitorShowIpBoolean && activeSessionId && sessionIpAddress" class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">IP:</label>
<div class="flex items-center">
<span
class="ip-address-value truncate text-left cursor-pointer hover:text-primary transition-colors"
:title="sessionIpAddress"
@click="copyIpToClipboard(sessionIpAddress)">
{{ sessionIpAddress }}
</span>
</div>
</div>
<!-- CPU 型号 -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
@@ -122,9 +135,17 @@ import { useI18n } from 'vue-i18n';
import StatusCharts from './StatusCharts.vue';
import { useSessionStore } from '../stores/session.store'; // sessionStore
import { storeToRefs } from 'pinia'; // storeToRefs
import { useSettingsStore } from '../stores/settings.store'; // store
import { useConnectionsStore } from '../stores/connections.store'; // store
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // + store
const { t } = useI18n();
const sessionStore = useSessionStore();
const settingsStore = useSettingsStore(); // store
const connectionsStore = useConnectionsStore(); // store
const uiNotificationsStore = useUiNotificationsStore(); // + store
const { sessions } = storeToRefs(sessionStore); // sessions
const { statusMonitorShowIpBoolean } = storeToRefs(settingsStore); // IP
const isSwitchingSession = ref(false);
interface ServerStatus {
@@ -278,4 +299,30 @@ const swapDisplay = computed(() => {
return `${formatMemorySize(used)} / ${formatMemorySize(total)} ${percent}`;
});
const sessionIpAddress = computed(() => {
const sessionState = currentSessionState.value;
if (sessionState && sessionState.connectionId) {
// connectionsStore connections
const connectionIdAsNumber = parseInt(sessionState.connectionId, 10);
if (isNaN(connectionIdAsNumber)) {
return null; // connectionId null
}
const connectionInfo = connectionsStore.connections.find(conn => conn.id === connectionIdAsNumber);
return connectionInfo?.host || null;
}
return null;
});
const copyIpToClipboard = async (ipAddress: string | null) => {
if (!ipAddress) return;
try {
await navigator.clipboard.writeText(ipAddress);
uiNotificationsStore.showSuccess(t('common.copied', '已复制!')); // + 使 store
} catch (err) {
console.error('Failed to copy IP address: ', err);
uiNotificationsStore.showError(t('statusMonitor.copyIpError', '复制 IP 失败')); // +
//
}
};
</script>
@@ -128,7 +128,7 @@ const handleKeyDown = async (event: KeyboardEvent) => {
selectTag(existingTag);
} else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing
// emit
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); // +++ +++
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`);
emit('create-tag', trimmedInput);
// availableTags prop TagInput
// tag ID modelValue
@@ -160,9 +160,9 @@ const removeTagLocally = (tagToRemove: GenericTag) => {
// ( 'x' ) - Emit event
const handleDeleteTagGlobally = (tagToDelete: GenericTag) => {
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // +++ +++
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`);
// Emit event for parent to handle deletion confirmation and API call
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); // +++ +++
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`);
emit('delete-tag', tagToDelete.id);
// Parent should handle confirmation, call store action, and update modelValue/availableTags
// We might still want to remove it locally immediately for better UX,
@@ -562,7 +562,7 @@ const clearSearch = () => {
searchAddon?.clearDecorations();
};
// +++ clear +++
// +++ clear +++
const clear = () => {
terminal?.clear();
};
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, defineEmits } from 'vue'; // +++ Import ref +++
import { ref, defineEmits } from 'vue';
const emit = defineEmits<{
(e: 'send-key', keySequence: string): void;
@@ -17,14 +17,14 @@
<script setup lang="ts">
import { useSettingsStore } from '../../stores/settings.store';
import { useAppearanceStore } from '../../stores/appearance.store'; // store
import { useAppearanceStore } from '../../stores/appearance.store';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useAppearanceSettings } from '../../composables/settings/useAppearanceSettings';
const settingsStore = useSettingsStore();
const { settings } = storeToRefs(settingsStore); // To ensure v-if="settings" works
const appearanceStore = useAppearanceStore(); // store
const { settings } = storeToRefs(settingsStore);
const appearanceStore = useAppearanceStore();
const { t } = useI18n();
const {
@@ -222,7 +222,27 @@
<p v-if="terminalEnableRightClickPasteMessage" :class="['text-sm', terminalEnableRightClickPasteSuccess ? 'text-success' : 'text-error']">{{ terminalEnableRightClickPasteMessage }}</p>
</div>
</form>
</div>
</div>
<hr class="border-border/50">
<!-- Status Monitor Show IP -->
<div class="settings-section-content">
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.statusMonitorShowIp.title', '状态监视器 IP 显示') }}</h3>
<form @submit.prevent="handleUpdateStatusMonitorShowIpSetting" class="space-y-4">
<div class="flex items-center">
<input type="checkbox" id="statusMonitorShowIp" v-model="statusMonitorShowIpEnabled"
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
<label for="statusMonitorShowIp" class="text-sm text-foreground cursor-pointer select-none">{{ $t('settings.statusMonitorShowIp.enableLabel', '在状态监视器中显示IP地址') }}</label>
</div>
<div class="flex items-center justify-between pt-2">
<button type="submit"
:disabled="statusMonitorShowIpLoading"
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-150 ease-in-out text-sm font-medium">
{{ $t('common.save') }}
</button>
<p v-if="statusMonitorShowIpMessage" :class="['text-sm', statusMonitorShowIpSuccess ? 'text-success' : 'text-error']">{{ statusMonitorShowIpMessage }}</p>
</div>
</form>
</div>
</div>
</div>
</template>
@@ -283,6 +303,11 @@ const {
showPopupFileManagerMessage,
showPopupFileManagerSuccess,
handleUpdateShowPopupFileManager,
statusMonitorShowIpEnabled,
statusMonitorShowIpLoading,
statusMonitorShowIpMessage,
statusMonitorShowIpSuccess,
handleUpdateStatusMonitorShowIpSetting,
} = useWorkspaceSettings();
</script>
@@ -1,7 +1,7 @@
import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue'; // 导入 ComponentPublicInstance
import type { FileListItem } from '../../types/sftp.types'; // 修正路径
import { type useI18n } from 'vue-i18n'; // 导入 useI18n 以获取 t 的类型
import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue'; // <-- 导入组件类型
import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue';
import type { FileListItem } from '../../types/sftp.types';
import { type useI18n } from 'vue-i18n';
import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue';
// 定义菜单项类型 (可以根据需要扩展)
export interface ContextMenuItem {
@@ -242,7 +242,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
];
} else { // Clicked on '..'
menu = [
// +++ 添加粘贴 (可以粘贴到上级目录) +++
// +++ 粘贴 (可以粘贴到上级目录) +++
{ label: t('fileManager.actions.paste'), action: onPaste, disabled: !(isConnected.value && isSftpReady.value) || !hasClipboardContent },
{ label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !(isConnected.value && isSftpReady.value) }
];
@@ -2,7 +2,7 @@ import { ref, watch } from 'vue';
import { useSettingsStore } from '../../stores/settings.store';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { availableLocales } from '../../i18n'; // 导入可用语言列表
import { availableLocales } from '../../i18n';
export function useSystemSettings() {
const settingsStore = useSettingsStore();
@@ -17,8 +17,9 @@ export function useWorkspaceSettings() {
showQuickCommandTagsBoolean,
terminalScrollbackLimitNumber,
fileManagerShowDeleteConfirmationBoolean,
terminalEnableRightClickPasteBoolean,
showPopupFileManagerBoolean, // +++ Import the new getter +++
terminalEnableRightClickPasteBoolean,
showPopupFileManagerBoolean,
statusMonitorShowIpBoolean,
} = storeToRefs(settingsStore);
// --- Popup Editor ---
@@ -286,6 +287,30 @@ export function useWorkspaceSettings() {
}
};
// --- Status Monitor Show IP ---
const statusMonitorShowIpEnabled = ref(false);
const statusMonitorShowIpLoading = ref(false);
const statusMonitorShowIpMessage = ref('');
const statusMonitorShowIpSuccess = ref(false);
const handleUpdateStatusMonitorShowIpSetting = async () => {
statusMonitorShowIpLoading.value = true;
statusMonitorShowIpMessage.value = '';
statusMonitorShowIpSuccess.value = false;
try {
const valueToSave = statusMonitorShowIpEnabled.value ? 'true' : 'false';
await settingsStore.updateSetting('showStatusMonitorIpAddress', valueToSave);
statusMonitorShowIpMessage.value = t('common.saved');
statusMonitorShowIpSuccess.value = true;
} catch (error: any) {
console.error('Failed to update status monitor IP display setting:', error);
statusMonitorShowIpMessage.value = error.message || t('settings.statusMonitorShowIp.error.saveFailed', 'Failed to save status monitor IP display setting.'); // 需要添加相应的i18n键
statusMonitorShowIpSuccess.value = false;
} finally {
statusMonitorShowIpLoading.value = false;
}
};
// Watchers to sync local state with store state
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
@@ -296,8 +321,9 @@ export function useWorkspaceSettings() {
watch(showQuickCommandTagsBoolean, (newValue) => { showQuickCommandTagsLocal.value = newValue; }, { immediate: true });
watch(terminalScrollbackLimitNumber, (newValue) => { terminalScrollbackLimitLocal.value = newValue; }, { immediate: true });
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true });
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true });
watch(showPopupFileManagerBoolean, (newValue) => { showPopupFileManagerLocal.value = newValue; }, { immediate: true }); // +++ Watch for popup file manager +++
watch(statusMonitorShowIpBoolean, (newValue) => { statusMonitorShowIpEnabled.value = newValue; }, { immediate: true });
return {
@@ -367,5 +393,11 @@ export function useWorkspaceSettings() {
showPopupFileManagerMessage,
showPopupFileManagerSuccess,
handleUpdateShowPopupFileManager,
statusMonitorShowIpEnabled,
statusMonitorShowIpLoading,
statusMonitorShowIpMessage,
statusMonitorShowIpSuccess,
handleUpdateStatusMonitorShowIpSetting,
};
}
@@ -1,8 +1,8 @@
import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue'; // 引入 reactive 和 computed
import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue';
import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types';
import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types';
// 导入 UI 通知 store
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 更正导入
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
/**
* @interface WebSocketDependencies
@@ -24,7 +24,7 @@ export function useSidebarResize({
const handleMouseDown = (event: MouseEvent) => {
console.log(`[useSidebarResize] handleMouseDown triggered for side: ${side}`, { sidebar: sidebarRef.value, handle: handleRef.value }); // +++ Add Log +++
if (!sidebarRef.value || !handleRef.value) {
console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.'); // +++ Add Log +++
console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.');
return;
}
+7 -1
View File
@@ -816,6 +816,10 @@
"terminalRightClickPasteSuccess": "Terminal right-click paste setting saved.",
"terminalRightClickPasteError": "Failed to save terminal right-click paste setting."
},
"statusMonitorShowIp": {
"title": "Status Monitor: Show IP Address",
"enableLabel": "Show IP address in status monitor"
},
"terminalScrollback": {
"title": "Terminal Scrollback Limit",
"limitLabel": "Maximum Lines",
@@ -940,6 +944,7 @@
"cancel": "Cancel",
"save": "Save",
"saving": "Saving...",
"saved": "Saved",
"testing": "Testing...",
"edit": "Edit",
"delete": "Delete",
@@ -962,7 +967,8 @@
"sortDescending": "Descending",
"restore": "Restore",
"minimize": "Minimize",
"send":"Send"
"send":"Send",
"copied": "Copied to clipboard"
},
"layoutConfigurator": {
"title": "Layout Configurator",
+7 -1
View File
@@ -96,6 +96,7 @@
"retry": "再試行",
"save": "保存",
"saving": "保存中...",
"saved": "保存済み",
"search": "検索",
"settings": "設定",
"sortAscending": "昇順",
@@ -104,7 +105,8 @@
"width": "幅",
"restore": "元に戻す",
"minimize": "最小化",
"send":"送信する"
"send":"送信する",
"copied": "クリップボードにコピーしました"
},
"connections": {
"actions": {
@@ -1075,6 +1077,10 @@
"terminalRightClickPasteSuccess": "ターミナルの右クリック貼り付け設定が保存されました。",
"terminalRightClickPasteError": "ターミナルの右クリック貼り付け設定の保存に失敗しました。"
},
"statusMonitorShowIp": {
"title": "ステータスモニターでIPアドレスを表示",
"enableLabel": "ステータスモニターにIPアドレスを表示する"
},
"terminalScrollback": {
"title": "ターミナルスクロールバック制限",
"limitLabel": "最大行数",
+7 -1
View File
@@ -810,6 +810,10 @@
"terminalRightClickPasteSuccess": "终端右键粘贴设置已保存。",
"terminalRightClickPasteError": "保存终端右键粘贴设置失败。"
},
"statusMonitorShowIp": {
"title": "状态监视器显示IP地址",
"enableLabel": "在状态监视器中显示IP地址"
},
"terminalScrollback": {
"title": "终端回滚行数",
"limitLabel": "最大行数",
@@ -941,6 +945,7 @@
"cancel": "取消",
"save": "保存",
"saving": "保存中...",
"saved": "已保存",
"testing": "测试中...",
"edit": "编辑",
"delete": "删除",
@@ -963,7 +968,8 @@
"sortDescending": "降序",
"restore": "还原",
"minimize": "最小化",
"send":"发送"
"send":"发送",
"copied": "已复制到剪贴板"
},
"layoutConfigurator": {
"title": "布局管理器",
+7 -9
View File
@@ -1,16 +1,14 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia'; // 引入 Pinia
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; // 引入持久化插件
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue';
import router from './router'; // 引入我们创建的 router
import i18n from './i18n'; // 引入 i18n 实例
import { useAuthStore } from './stores/auth.store'; // 引入 Auth Store
import { useSettingsStore } from './stores/settings.store'; // 引入 Settings Store
import { useAppearanceStore } from './stores/appearance.store'; // 引入 Appearance Store
import router from './router';
import i18n from './i18n';
import { useAuthStore } from './stores/auth.store';
import { useSettingsStore } from './stores/settings.store';
import { useAppearanceStore } from './stores/appearance.store';
import './style.css';
// 导入 Font Awesome CSS
import '@fortawesome/fontawesome-free/css/all.min.css';
// 导入 splitpanes CSS
import 'splitpanes/dist/splitpanes.css';
+3 -3
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
import router from '../router'; // 引入 router 用于重定向
import { setLocale } from '../i18n'; // 导入 setLocale
import apiClient from '../utils/apiClient';
import router from '../router';
import { setLocale } from '../i18n';
// 扩展的用户信息接口,包含 2FA 状态和语言偏好
interface UserInfo {
@@ -1,10 +1,10 @@
import { ref, computed, readonly, watch, nextTick } from 'vue'; // Import nextTick
import { ref, computed, readonly, watch, nextTick } from 'vue';
import { defineStore } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useSessionStore } from './session.store'; // 导入会话 Store
import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types'; // 移除 SftpReadFileRequestPayload, 因为 readFile 不再需要它
import * as iconv from '@vscode/iconv-lite-umd'; // +++ 导入 iconv-lite +++
import { Buffer } from 'buffer/'; // +++ 导入 Buffer (需要安装 buffer 依赖) +++
import { useSessionStore } from './session.store';
import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types';
import * as iconv from '@vscode/iconv-lite-umd';
import { Buffer } from 'buffer/';
// --- 类型定义 ---
// 文件信息,用于打开文件操作
@@ -1,11 +1,11 @@
import { defineStore } from 'pinia';
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
import { ref, computed, watch } from 'vue'; // Import watch
import apiClient from '../utils/apiClient';
import { ref, computed, watch } from 'vue';
import { useUiNotificationsStore } from './uiNotifications.store';
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; // +++ Import new tag store +++
import { useI18n } from 'vue-i18n'; // +++ Import i18n for "Untagged" +++
// Assuming QuickCommand type in types includes tagIds now, or define it here
// import type { QuickCommand } from '../types/quick-commands.types';
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store';
import { useI18n } from 'vue-i18n';
// 定义前端使用的快捷指令接口 (包含 tagIds)
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
@@ -1,11 +1,11 @@
// packages/frontend/src/stores/session.store.ts
import { defineStore } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
// 从新模块导入状态
import {
sessions,
activeSessionId,
@@ -13,30 +13,30 @@ import {
rdpConnectionInfo,
isVncModalOpen,
vncConnectionInfo,
// SSH Suspend Mode State
suspendedSshSessions,
isLoadingSuspendedSessions,
} from './session/state';
// 从新模块导入 Getters
import {
sessionTabs,
sessionTabsWithStatus,
activeSession,
} from './session/getters';
// 从新模块导入 Actions
import * as sessionActions from './session/actions/sessionActions';
import * as editorActions from './session/actions/editorActions';
import * as sftpManagerActions from './session/actions/sftpManagerActions';
import * as modalActions from './session/actions/modalActions';
import * as commandInputActions from './session/actions/commandInputActions';
import * as sshSuspendActions from './session/actions/sshSuspendActions'; // 导入 SSH 挂起 Actions
import * as sshSuspendActions from './session/actions/sshSuspendActions';
// 导入需要的类型 (例如 FileInfo 可能会在参数中使用)
import type { FileInfo } from './fileEditor.store';
// SftpManagerInstance 类型主要在 action 文件内部使用,但如果 store 直接暴露它,则需要导入
// import type { SftpManagerInstance } from './session/types';
export const useSessionStore = defineStore('session', () => {
@@ -1,20 +1,20 @@
// packages/frontend/src/stores/session/actions/sessionActions.ts
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useConnectionsStore, type ConnectionInfo } from '../../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts
import { useConnectionsStore, type ConnectionInfo } from '../../connections.store';
import { sessions, activeSessionId } from '../state';
import { generateSessionId } from '../utils';
import type { SessionState, SshTerminalInstance, StatusMonitorInstance, DockerManagerInstance, SftpManagerInstance, WsManagerInstance } from '../types';
// Composables for manager creation - 路径相对于此文件
import { createWebSocketConnectionManager } from '../../../composables/useWebSocketConnection';
import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal';
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor';
import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager';
import { registerSshSuspendHandlers } from './sshSuspendActions'; // 导入 SSH 挂起处理器注册函数
// getOrCreateSftpManager 将在 sftpManagerActions.ts 中定义,并在主 store 中协调
import { registerSshSuspendHandlers } from './sshSuspendActions';
// --- 辅助函数 (特定于此模块的 actions) ---
const findConnectionInfo = (connectionId: number | string, connectionsStore: ReturnType<typeof useConnectionsStore>): ConnectionInfo | undefined => {
@@ -1,4 +1,4 @@
// packages/frontend/src/stores/session/actions/sshSuspendActions.ts
import { v4 as uuidv4 } from 'uuid';
import { sessions, suspendedSshSessions, isLoadingSuspendedSessions, activeSessionId } from '../state';
import type {
@@ -8,8 +8,8 @@ import type {
SshSuspendResumeReqMessage,
SshSuspendTerminateReqMessage,
SshSuspendRemoveEntryReqMessage,
// SshSuspendEditNameReqMessage, // Removed, using HTTP API
// S2C Payloads
SshMarkedForSuspendAckPayload,
SshUnmarkedForSuspendAckPayload,
SshSuspendListResponsePayload,
@@ -17,19 +17,19 @@ import type {
SshOutputCachedChunkPayload,
SshSuspendTerminatedRespPayload,
SshSuspendEntryRemovedRespPayload,
// SshSuspendNameEditedRespPayload, // Removed, using HTTP API
SshSuspendAutoTerminatedNotifPayload,
} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession
import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息
import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知
import type { SuspendedSshSession } from '../../../types/ssh-suspend.types'; // 路径: packages/frontend/src/types/ssh-suspend.types.ts
import i18n from '../../../i18n'; // 直接导入 i18n 实例
import type { ComposerTranslation } from 'vue-i18n'; // 导入 ComposerTranslation 类型
import apiClient from '../../../utils/apiClient'; // +++ 导入 apiClient +++
} from '../../../types/websocket.types';
import type { WsManagerInstance, SessionState } from '../types';
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions';
import { useConnectionsStore } from '../../connections.store';
import { useUiNotificationsStore } from '../../uiNotifications.store';
import type { SuspendedSshSession } from '../../../types/ssh-suspend.types';
import i18n from '../../../i18n';
import type { ComposerTranslation } from 'vue-i18n';
import apiClient from '../../../utils/apiClient';
const t: ComposerTranslation = i18n.global.t; // 从全局 i18n 实例获取 t 函数并显式注解类型
const t: ComposerTranslation = i18n.global.t;
// 辅助函数:获取一个可用的 WebSocket 管理器
// 优先使用当前激活的会话,或者任意一个已连接的 SSH 会话
@@ -1,11 +1,9 @@
// packages/frontend/src/stores/session/types.ts
import type { Ref } from 'vue';
import type { FileTab as OriginalFileTab } from '../fileEditor.store'; // 路径: packages/frontend/src/stores/fileEditor.store.ts
import type { WsConnectionStatus } from '../../composables/useWebSocketConnection'; // 路径: packages/frontend/src/composables/useWebSocketConnection.ts
// 从源模块导入 DockerManagerInstance 类型并使用别名
import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager'; // 路径: packages/frontend/src/composables/useDockerManager.ts
import type { FileTab as OriginalFileTab } from '../fileEditor.store';
import type { WsConnectionStatus } from '../../composables/useWebSocketConnection';
import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager';
// 导入工厂函数仅用于通过 ReturnType 推导实例类型
// 这些导入仅用于类型推断,不在运行时使用
+52 -40
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
import { ref, computed } from 'vue'; // 移除 watch
import i18n, { setLocale, defaultLng, availableLocales } from '../i18n'; // Import i18n instance, setLocale, defaultLng, and availableLocales
import apiClient from '../utils/apiClient';
import { ref, computed } from 'vue';
import i18n, { setLocale, defaultLng, availableLocales } from '../i18n';
import type { PaneName } from './layout.store';
import { useAuthStore } from './auth.store';
import type { ConnectionInfo } from './connections.store';
@@ -62,6 +62,7 @@ interface SettingsState {
terminalScrollbackLimit?: string; // 终端回滚行数上限 (e.g., '5000', '0' for unlimited)
fileManagerShowDeleteConfirmation?: string; // 'true' or 'false' - 文件管理器删除确认提示
terminalEnableRightClickPaste?: string; // 'true' or 'false' - 终端右键粘贴
showStatusMonitorIpAddress?: string; // 'true' or 'false' - 状态监视器显示IP地址
[key: string]: string | undefined;
}
@@ -115,7 +116,7 @@ export const useSettingsStore = defineStore('settings', () => {
if (settings.value.showPopupFileEditor === undefined) {
settings.value.showPopupFileEditor = 'true';
}
// +++ 添加 showPopupFileManager 默认值 (改为 false) +++
// +++ showPopupFileManager 默认值 (改为 false) +++
if (settings.value.showPopupFileManager === undefined) {
settings.value.showPopupFileManager = 'false'; // 默认禁用弹窗文件管理器
}
@@ -304,6 +305,10 @@ export const useSettingsStore = defineStore('settings', () => {
settings.value.terminalEnableRightClickPaste = 'true'; // 默认启用右键粘贴
console.log(`[SettingsStore] terminalEnableRightClickPaste not found, set to default: ${settings.value.terminalEnableRightClickPaste}`);
}
if (settings.value.showStatusMonitorIpAddress === undefined) {
settings.value.showStatusMonitorIpAddress = 'false'; // 默认禁用状态监视器显示IP
console.log(`[SettingsStore] showStatusMonitorIpAddress not found, set to default: ${settings.value.showStatusMonitorIpAddress}`);
}
// --- 语言设置 ---
@@ -375,28 +380,29 @@ export const useSettingsStore = defineStore('settings', () => {
// 移除外观相关的键检查
const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone', // 添加时区键
'rdpModalWidth', // 添加 RDP 模态框宽度键
'rdpModalHeight', // 添加 RDP 模态框高度键
'vncModalWidth', // 添加 VNC 模态框宽度键
'vncModalHeight', // 添加 VNC 模态框高度键
'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++
'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++
'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++
'fileManagerColWidths', // +++ 文件管理器列宽键 +++
'commandInputSyncTarget', // +++ 命令输入同步目标键 +++
'timezone', // 时区键
'rdpModalWidth', // RDP 模态框宽度键
'rdpModalHeight', // RDP 模态框高度键
'vncModalWidth', // VNC 模态框宽度键
'vncModalHeight', // VNC 模态框高度键
'ipBlacklistEnabled',
'dashboardSortBy',
'dashboardSortOrder',
'showConnectionTags',
'showQuickCommandTags',
'layoutLocked',
'terminalScrollbackLimit',
'fileManagerShowDeleteConfirmation',
'terminalEnableRightClickPaste'
'showConnectionTags',
'showQuickCommandTags',
'layoutLocked',
'terminalScrollbackLimit',
'fileManagerShowDeleteConfirmation',
'terminalEnableRightClickPaste',
'showStatusMonitorIpAddress'
];
if (!allowedKeys.includes(key)) {
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
@@ -470,28 +476,29 @@ export const useSettingsStore = defineStore('settings', () => {
// 移除外观相关的键检查
const allowedKeys: Array<keyof SettingsState> = [
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
'timezone', // 添加时区键
'rdpModalWidth', // 添加 RDP 模态框宽度键
'rdpModalHeight', // 添加 RDP 模态框高度键
'vncModalWidth', // 添加 VNC 模态框宽度键
'vncModalHeight', // 添加 VNC 模态框高度键
'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++
'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++
'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++
'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++
'fileManagerColWidths', // +++ 文件管理器列宽键 +++
'commandInputSyncTarget', // +++ 命令输入同步目标键 +++
'timezone', // 时区键
'rdpModalWidth', // RDP 模态框宽度键
'rdpModalHeight', // RDP 模态框高度键
'vncModalWidth', // VNC 模态框宽度键
'vncModalHeight', // VNC 模态框高度键
'ipBlacklistEnabled',
'dashboardSortBy',
'dashboardSortOrder',
'showConnectionTags',
'showQuickCommandTags',
'layoutLocked',
'terminalScrollbackLimit',
'fileManagerShowDeleteConfirmation',
'terminalEnableRightClickPaste'
'showConnectionTags',
'showQuickCommandTags',
'layoutLocked',
'terminalScrollbackLimit',
'fileManagerShowDeleteConfirmation',
'terminalEnableRightClickPaste',
'showStatusMonitorIpAddress'
];
const filteredUpdates: Partial<SettingsState> = {};
let languageUpdate: string | undefined = undefined;
@@ -793,6 +800,10 @@ export const useSettingsStore = defineStore('settings', () => {
const terminalEnableRightClickPasteBoolean = computed(() => {
return settings.value.terminalEnableRightClickPaste !== 'false'; // Default to true
});
const statusMonitorShowIpBoolean = computed(() => {
return settings.value.showStatusMonitorIpAddress === 'true';
});
return {
settings, // 只包含通用设置
@@ -838,5 +849,6 @@ export const useSettingsStore = defineStore('settings', () => {
terminalScrollbackLimitNumber, // Expose terminal scrollback limit getter
fileManagerShowDeleteConfirmationBoolean, // Expose file manager delete confirmation getter
terminalEnableRightClickPasteBoolean, // Expose terminal right click paste getter
statusMonitorShowIpBoolean, // 暴露状态监视器显示IP getter
};
});
+2 -2
View File
@@ -1,6 +1,6 @@
import axios from 'axios';
import router from '../router'; // 引入 router 用于可能的重定向
import { useAuthStore } from '../stores/auth.store'; // 引入 auth store 用于检查认证状态和登出
import router from '../router';
import { useAuthStore } from '../stores/auth.store';
// 创建 axios 实例
const apiClient = axios.create({
+12 -12
View File
@@ -1,17 +1,17 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import AddConnectionForm from '../components/AddConnectionForm.vue'; // +++ +++
import AddConnectionForm from '../components/AddConnectionForm.vue';
import { useConnectionsStore } from '../stores/connections.store';
import { useAuditLogStore } from '../stores/audit.store';
import { useSessionStore } from '../stores/session.store';
import { useTagsStore } from '../stores/tags.store'; // +++ store +++
import type { TagInfo } from '../stores/tags.store'; // +++ +++
// Removed settings store import for sorting
import type { SortField, SortOrder } from '../stores/settings.store'; // Keep type import
import { useTagsStore } from '../stores/tags.store';
import type { TagInfo } from '../stores/tags.store';
import type { SortField, SortOrder } from '../stores/settings.store';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import type { ConnectionInfo } from '../stores/connections.store';
import { storeToRefs } from 'pinia'; // Keep for other stores if needed
import { storeToRefs } from 'pinia';
import { formatDistanceToNow } from 'date-fns';
import { zhCN, enUS, ja } from 'date-fns/locale';
import type { Locale } from 'date-fns';
@@ -21,18 +21,18 @@ const router = useRouter();
const connectionsStore = useConnectionsStore();
const auditLogStore = useAuditLogStore();
const sessionStore = useSessionStore();
const tagsStore = useTagsStore(); // +++ store +++
// Removed settings store instantiation
const tagsStore = useTagsStore();
const { connections, isLoading: isLoadingConnections } = storeToRefs(connectionsStore);
const { logs: auditLogs, isLoading: isLoadingLogs, totalLogs } = storeToRefs(auditLogStore);
const { tags, isLoading: isLoadingTags } = storeToRefs(tagsStore); // +++ +++
// Removed refs from settings store
const { tags, isLoading: isLoadingTags } = storeToRefs(tagsStore);
// Local state for sorting with localStorage persistence
const LS_SORT_BY_KEY = 'dashboard_connections_sort_by';
const LS_SORT_ORDER_KEY = 'dashboard_connections_sort_order';
const LS_FILTER_TAG_KEY = 'dashboard_connections_filter_tag'; // +++ localStorage key +++
const LS_FILTER_TAG_KEY = 'dashboard_connections_filter_tag';
// Initialize with localStorage values or defaults
const localSortBy = ref<SortField>(localStorage.getItem(LS_SORT_BY_KEY) as SortField || 'last_connected_at');
@@ -521,7 +521,7 @@ const finishEditingTag = async () => {
if (commandIdsToAssign.length > 0) {
console.log(`[QuickCmdView] Assigning ${commandIdsToAssign.length} commands to new tag ID: ${newTag.id}`);
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`); // +++ +++
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`);
// Call the store action to assign commands to the new tag
const assignSuccess = await quickCommandsStore.assignCommandsToTagAction(commandIdsToAssign, newTag.id);
if (assignSuccess) {
+2 -2
View File
@@ -86,10 +86,10 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'; // Simplified Vue imports
import { onMounted, ref } from 'vue';
import { useAuthStore } from '../stores/auth.store';
import { useSettingsStore } from '../stores/settings.store';
import { useAppearanceStore } from '../stores/appearance.store'; // store
import { useAppearanceStore } from '../stores/appearance.store';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useVersionCheck } from '../composables/settings/useVersionCheck';