feat: 添加显隐标签的设置
This commit is contained in:
@@ -31,6 +31,8 @@ const AUTO_COPY_ON_SELECT_KEY = 'autoCopyOnSelect'; // 终端选中自动复制
|
|||||||
const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键
|
const STATUS_MONITOR_INTERVAL_SECONDS_KEY = 'statusMonitorIntervalSeconds'; // 状态监控间隔设置键
|
||||||
const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
|
const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
|
||||||
const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键
|
const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键
|
||||||
|
const SHOW_CONNECTION_TAGS_KEY = 'showConnectionTags'; // 连接标签显示设置键
|
||||||
|
const SHOW_QUICK_COMMAND_TAGS_KEY = 'showQuickCommandTags'; // 快捷指令标签显示设置键
|
||||||
|
|
||||||
export const settingsService = {
|
export const settingsService = {
|
||||||
/**
|
/**
|
||||||
@@ -479,6 +481,60 @@ export const settingsService = {
|
|||||||
// Directly call the specific repository function with the full, validated config
|
// Directly call the specific repository function with the full, validated config
|
||||||
await setCaptchaConfigInRepo(configToSave);
|
await setCaptchaConfigInRepo(configToSave);
|
||||||
console.log('[SettingsService] CAPTCHA config successfully set.');
|
console.log('[SettingsService] CAPTCHA config successfully set.');
|
||||||
|
}, // <-- Add comma here
|
||||||
|
|
||||||
|
// --- Show Connection Tags ---
|
||||||
|
async getShowConnectionTags(): Promise<boolean> {
|
||||||
|
console.log(`[Service] Attempting to get setting for key: ${SHOW_CONNECTION_TAGS_KEY}`);
|
||||||
|
try {
|
||||||
|
const valueStr = await settingsRepository.getSetting(SHOW_CONNECTION_TAGS_KEY);
|
||||||
|
console.log(`[Service] Raw value from repository for ${SHOW_CONNECTION_TAGS_KEY}:`, valueStr);
|
||||||
|
// 默认显示,所以只有当值为 'false' 时才返回 false
|
||||||
|
return valueStr !== 'false';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error getting show connection tags setting (key: ${SHOW_CONNECTION_TAGS_KEY}):`, error);
|
||||||
|
return true; // 默认返回 true
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
async setShowConnectionTags(enabled: boolean): Promise<void> {
|
||||||
|
console.log(`[Service] setShowConnectionTags called with: ${enabled}`);
|
||||||
|
try {
|
||||||
|
const valueStr = String(enabled);
|
||||||
|
console.log(`[Service] Attempting to save setting. Key: ${SHOW_CONNECTION_TAGS_KEY}, Value: ${valueStr}`);
|
||||||
|
await settingsRepository.setSetting(SHOW_CONNECTION_TAGS_KEY, valueStr);
|
||||||
|
console.log(`[Service] Successfully saved setting for key: ${SHOW_CONNECTION_TAGS_KEY}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_CONNECTION_TAGS_KEY}:`, error);
|
||||||
|
throw new Error('Failed to save show connection tags setting.');
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
// --- Show Quick Command Tags ---
|
||||||
|
async getShowQuickCommandTags(): Promise<boolean> {
|
||||||
|
console.log(`[Service] Attempting to get setting for key: ${SHOW_QUICK_COMMAND_TAGS_KEY}`);
|
||||||
|
try {
|
||||||
|
const valueStr = await settingsRepository.getSetting(SHOW_QUICK_COMMAND_TAGS_KEY);
|
||||||
|
console.log(`[Service] Raw value from repository for ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, valueStr);
|
||||||
|
// 默认显示,所以只有当值为 'false' 时才返回 false
|
||||||
|
return valueStr !== 'false';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error getting show quick command tags setting (key: ${SHOW_QUICK_COMMAND_TAGS_KEY}):`, error);
|
||||||
|
return true; // 默认返回 true
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
async setShowQuickCommandTags(enabled: boolean): Promise<void> {
|
||||||
|
console.log(`[Service] setShowQuickCommandTags called with: ${enabled}`);
|
||||||
|
try {
|
||||||
|
const valueStr = String(enabled);
|
||||||
|
console.log(`[Service] Attempting to save setting. Key: ${SHOW_QUICK_COMMAND_TAGS_KEY}, Value: ${valueStr}`);
|
||||||
|
await settingsRepository.setSetting(SHOW_QUICK_COMMAND_TAGS_KEY, valueStr);
|
||||||
|
console.log(`[Service] Successfully saved setting for key: ${SHOW_QUICK_COMMAND_TAGS_KEY}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, error);
|
||||||
|
throw new Error('Failed to save show quick command tags setting.');
|
||||||
|
}
|
||||||
} // <-- No comma after the last method
|
} // <-- No comma after the last method
|
||||||
|
|
||||||
}; // <-- End of settingsService object definition
|
}; // <-- End of settingsService object definition
|
||||||
|
|||||||
@@ -417,6 +417,86 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
res.status(500).json({ message: '设置 CAPTCHA 配置失败', error: error.message });
|
res.status(500).json({ message: '设置 CAPTCHA 配置失败', error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, // <-- Add comma here
|
||||||
|
|
||||||
|
// --- Show Connection Tags ---
|
||||||
|
async getShowConnectionTags(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('[控制器] 收到获取“显示连接标签”设置的请求。');
|
||||||
|
const isEnabled = await settingsService.getShowConnectionTags();
|
||||||
|
console.log(`[控制器] 向客户端发送“显示连接标签”设置: ${isEnabled}`);
|
||||||
|
res.json({ enabled: isEnabled });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 获取“显示连接标签”设置时出错:', error);
|
||||||
|
res.status(500).json({ message: '获取“显示连接标签”设置失败', error: error.message });
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
async setShowConnectionTags(req: Request, res: Response): Promise<void> {
|
||||||
|
console.log('[控制器] 收到设置“显示连接标签”设置的请求。');
|
||||||
|
try {
|
||||||
|
const { enabled } = req.body;
|
||||||
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
console.warn('[控制器] 收到无效的 enabled 格式:', enabled);
|
||||||
|
res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[控制器] 调用 settingsService.setShowConnectionTags...');
|
||||||
|
await settingsService.setShowConnectionTags(enabled);
|
||||||
|
console.log('[控制器] settingsService.setShowConnectionTags 成功完成。');
|
||||||
|
|
||||||
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
||||||
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
||||||
|
|
||||||
|
console.log('[控制器] 发送成功响应。');
|
||||||
|
res.status(200).json({ message: '“显示连接标签”设置已成功更新' });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 设置“显示连接标签”时出错:', error);
|
||||||
|
res.status(500).json({ message: '设置“显示连接标签”失败', error: error.message });
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
// --- Show Quick Command Tags ---
|
||||||
|
async getShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('[控制器] 收到获取“显示快捷指令标签”设置的请求。');
|
||||||
|
const isEnabled = await settingsService.getShowQuickCommandTags();
|
||||||
|
console.log(`[控制器] 向客户端发送“显示快捷指令标签”设置: ${isEnabled}`);
|
||||||
|
res.json({ enabled: isEnabled });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 获取“显示快捷指令标签”设置时出错:', error);
|
||||||
|
res.status(500).json({ message: '获取“显示快捷指令标签”设置失败', error: error.message });
|
||||||
|
}
|
||||||
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
|
async setShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
||||||
|
console.log('[控制器] 收到设置“显示快捷指令标签”设置的请求。');
|
||||||
|
try {
|
||||||
|
const { enabled } = req.body;
|
||||||
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
console.warn('[控制器] 收到无效的 enabled 格式:', enabled);
|
||||||
|
res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[控制器] 调用 settingsService.setShowQuickCommandTags...');
|
||||||
|
await settingsService.setShowQuickCommandTags(enabled);
|
||||||
|
console.log('[控制器] settingsService.setShowQuickCommandTags 成功完成。');
|
||||||
|
|
||||||
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
||||||
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
||||||
|
|
||||||
|
console.log('[控制器] 发送成功响应。');
|
||||||
|
res.status(200).json({ message: '“显示快捷指令标签”设置已成功更新' });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 设置“显示快捷指令标签”时出错:', error);
|
||||||
|
res.status(500).json({ message: '设置“显示快捷指令标签”失败', error: error.message });
|
||||||
|
}
|
||||||
|
} // <-- No comma after the last method
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ router.get('/sidebar', settingsController.getSidebarConfig);
|
|||||||
// PUT /api/v1/settings/sidebar - 更新侧栏配置
|
// PUT /api/v1/settings/sidebar - 更新侧栏配置
|
||||||
router.put('/sidebar', settingsController.setSidebarConfig);
|
router.put('/sidebar', settingsController.setSidebarConfig);
|
||||||
|
|
||||||
|
// +++ 新增:显示连接标签路由 +++
|
||||||
|
// GET /api/v1/settings/show-connection-tags - 获取设置
|
||||||
|
router.get('/show-connection-tags', settingsController.getShowConnectionTags);
|
||||||
|
// PUT /api/v1/settings/show-connection-tags - 更新设置
|
||||||
|
router.put('/show-connection-tags', settingsController.setShowConnectionTags);
|
||||||
|
|
||||||
|
// +++ 新增:显示快捷指令标签路由 +++
|
||||||
|
// GET /api/v1/settings/show-quick-command-tags - 获取设置
|
||||||
|
router.get('/show-quick-command-tags', settingsController.getShowQuickCommandTags);
|
||||||
|
// PUT /api/v1/settings/show-quick-command-tags - 更新设置
|
||||||
|
router.put('/show-quick-command-tags', settingsController.setShowQuickCommandTags);
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTagsStore, TagInfo } from '../stores/tags.store'; // 确保 TagInfo
|
|||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
||||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // +++ 修正导入大小写 +++
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // +++ 修正导入大小写 +++
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 新增:导入设置 store
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -26,9 +27,11 @@ const tagsStore = useTagsStore();
|
|||||||
const sessionStore = useSessionStore(); // 获取 session store 实例
|
const sessionStore = useSessionStore(); // 获取 session store 实例
|
||||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||||
const uiNotificationsStore = useUiNotificationsStore(); // +++ 修正实例化大小写 +++
|
const uiNotificationsStore = useUiNotificationsStore(); // +++ 修正实例化大小写 +++
|
||||||
|
const settingsStore = useSettingsStore(); // 新增:实例化设置 store
|
||||||
|
|
||||||
const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore);
|
const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore);
|
||||||
const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore);
|
const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore);
|
||||||
|
const { showConnectionTagsBoolean } = storeToRefs(settingsStore); // 新增:获取设置项
|
||||||
|
|
||||||
// 搜索词
|
// 搜索词
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
@@ -73,17 +76,25 @@ const highlightedIndex = ref(-1); // -1 表示没有高亮项
|
|||||||
const listAreaRef = ref<HTMLElement | null>(null); // 列表容器的 ref
|
const listAreaRef = ref<HTMLElement | null>(null); // 列表容器的 ref
|
||||||
|
|
||||||
// 计算属性:扁平化的、当前可见的连接列表(用于键盘导航)
|
// 计算属性:扁平化的、当前可见的连接列表(用于键盘导航)
|
||||||
|
// 注意:这个 flatVisibleConnections 依赖于 filteredAndGroupedConnections 和 expandedGroups
|
||||||
|
// 当 showConnectionTagsBoolean 为 false 时,它不会被直接使用,但键盘导航逻辑依赖它
|
||||||
const flatVisibleConnections = computed(() => {
|
const flatVisibleConnections = computed(() => {
|
||||||
const flatList: ConnectionInfo[] = [];
|
const flatList: ConnectionInfo[] = [];
|
||||||
filteredAndGroupedConnections.value.forEach(group => {
|
// 如果显示标签,则只包含展开分组的连接
|
||||||
// 只添加展开分组中的连接
|
if (showConnectionTagsBoolean.value) {
|
||||||
if (expandedGroups.value[group.groupName]) {
|
filteredAndGroupedConnections.value.forEach(group => {
|
||||||
flatList.push(...group.connections);
|
if (expandedGroups.value[group.groupName]) {
|
||||||
}
|
flatList.push(...group.connections);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果不显示标签,则包含所有过滤后的连接
|
||||||
|
flatList.push(...flatFilteredConnections.value); // 使用下面定义的 flatFilteredConnections
|
||||||
|
}
|
||||||
return flatList;
|
return flatList;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 计算属性:当前高亮连接的 ID
|
// 计算属性:当前高亮连接的 ID
|
||||||
const highlightedConnectionId = computed(() => {
|
const highlightedConnectionId = computed(() => {
|
||||||
if (highlightedIndex.value >= 0 && highlightedIndex.value < flatVisibleConnections.value.length) {
|
if (highlightedIndex.value >= 0 && highlightedIndex.value < flatVisibleConnections.value.length) {
|
||||||
@@ -110,8 +121,7 @@ const setTagInputRef = (el: any, id: string | number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算属性:过滤并按标签分组连接
|
// 计算属性:过滤并按标签分组连接 (仅在 showConnectionTagsBoolean 为 true 时使用)
|
||||||
// 需要修改 filteredAndGroupedConnections,使其包含 tagId
|
|
||||||
const filteredAndGroupedConnections = computed(() => {
|
const filteredAndGroupedConnections = computed(() => {
|
||||||
const groups: Record<string, { connections: ConnectionInfo[], tagId: number | null }> = {}; // 修改:添加 tagId
|
const groups: Record<string, { connections: ConnectionInfo[], tagId: number | null }> = {}; // 修改:添加 tagId
|
||||||
const untagged: ConnectionInfo[] = [];
|
const untagged: ConnectionInfo[] = [];
|
||||||
@@ -128,7 +138,7 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
if (conn.host.toLowerCase().includes(lowerSearchTerm)) {
|
if (conn.host.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check associated tag names
|
// Check associated tag names (Always check tags for filtering, regardless of display setting)
|
||||||
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||||
for (const tagId of conn.tag_ids) {
|
for (const tagId of conn.tag_ids) {
|
||||||
const tag = tagMap.get(tagId); // Use the existing tagMap
|
const tag = tagMap.get(tagId); // Use the existing tagMap
|
||||||
@@ -151,21 +161,27 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
const groupName = tag.name;
|
const groupName = tag.name;
|
||||||
if (!groups[groupName]) {
|
if (!groups[groupName]) {
|
||||||
groups[groupName] = { connections: [], tagId: tag.id }; // 修改:存储 tagId
|
groups[groupName] = { connections: [], tagId: tag.id }; // 修改:存储 tagId
|
||||||
|
// Initialize expanded state only if not already set
|
||||||
if (expandedGroups.value[groupName] === undefined) {
|
if (expandedGroups.value[groupName] === undefined) {
|
||||||
expandedGroups.value[groupName] = true;
|
expandedGroups.value[groupName] = true; // Default to expanded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Avoid duplicates if a connection has multiple tags matching the search
|
||||||
if (!groups[groupName].connections.some(c => c.id === conn.id)) {
|
if (!groups[groupName].connections.some(c => c.id === conn.id)) {
|
||||||
groups[groupName].connections.push(conn);
|
groups[groupName].connections.push(conn);
|
||||||
}
|
}
|
||||||
tagged = true;
|
tagged = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!tagged) {
|
// If none of the tags were found in the tagMap (e.g., stale data), treat as untagged
|
||||||
|
if (!tagged && !untagged.some(c => c.id === conn.id)) {
|
||||||
untagged.push(conn);
|
untagged.push(conn);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
untagged.push(conn);
|
// Ensure untagged connections are not duplicated
|
||||||
|
if (!untagged.some(c => c.id === conn.id)) {
|
||||||
|
untagged.push(conn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,8 +201,9 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
|
|
||||||
if (untagged.length > 0) {
|
if (untagged.length > 0) {
|
||||||
const untaggedGroupName = t('workspaceConnectionList.untagged');
|
const untaggedGroupName = t('workspaceConnectionList.untagged');
|
||||||
|
// Initialize expanded state only if not already set
|
||||||
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
||||||
expandedGroups.value[untaggedGroupName] = true;
|
expandedGroups.value[untaggedGroupName] = true; // Default to expanded
|
||||||
}
|
}
|
||||||
// 未标记的分组没有 tagId
|
// 未标记的分组没有 tagId
|
||||||
result.push({ groupName: untaggedGroupName, connections: untagged, tagId: null });
|
result.push({ groupName: untaggedGroupName, connections: untagged, tagId: null });
|
||||||
@@ -195,12 +212,47 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 新增:计算属性,仅过滤,不分组 (用于 showConnectionTagsBoolean 为 false 时)
|
||||||
|
const flatFilteredConnections = computed(() => {
|
||||||
|
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||||
|
const tagMap = new Map(tags.value.map(tag => [tag.id, tag.name])); // 创建 tagMap 用于搜索
|
||||||
|
|
||||||
|
const filtered = connections.value.filter(conn => {
|
||||||
|
// Check connection name
|
||||||
|
if (conn.name && conn.name.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check connection host
|
||||||
|
if (conn.host.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check associated tag names (Always check tags for filtering)
|
||||||
|
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||||
|
for (const tagId of conn.tag_ids) {
|
||||||
|
const tagName = tagMap.get(tagId);
|
||||||
|
if (tagName && tagName.toLowerCase().includes(lowerSearchTerm)) {
|
||||||
|
return true; // Match found in tag name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No match found
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort the flat list
|
||||||
|
return filtered.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// +++ 监听分组状态变化并保存到 localStorage +++
|
// +++ 监听分组状态变化并保存到 localStorage +++
|
||||||
watch(expandedGroups, (newState) => {
|
watch(expandedGroups, (newState) => {
|
||||||
try {
|
// Only save if tags are shown
|
||||||
localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(newState));
|
if (showConnectionTagsBoolean.value) {
|
||||||
} catch (e) {
|
try {
|
||||||
console.error('Failed to save expanded groups state to localStorage:', e);
|
localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(newState));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save expanded groups state to localStorage:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
@@ -213,6 +265,12 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
watch(expandedGroups, () => {
|
watch(expandedGroups, () => {
|
||||||
highlightedIndex.value = -1;
|
highlightedIndex.value = -1;
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 监听显示模式变化,重置高亮索引
|
||||||
|
watch(showConnectionTagsBoolean, () => {
|
||||||
|
highlightedIndex.value = -1;
|
||||||
|
});
|
||||||
|
|
||||||
// +++ 监听编辑状态,自动聚焦输入框 +++
|
// +++ 监听编辑状态,自动聚焦输入框 +++
|
||||||
watch(editingTagId, async (newId) => {
|
watch(editingTagId, async (newId) => {
|
||||||
if (newId !== null) {
|
if (newId !== null) {
|
||||||
@@ -364,6 +422,8 @@ onMounted(() => {
|
|||||||
unregisterFocusAction = focusSwitcherStore.registerFocusAction('connectionListSearch', focusSearchInput);
|
unregisterFocusAction = focusSwitcherStore.registerFocusAction('connectionListSearch', focusSearchInput);
|
||||||
connectionsStore.fetchConnections(); // 移到 onMounted
|
connectionsStore.fetchConnections(); // 移到 onMounted
|
||||||
tagsStore.fetchTags(); // 移到 onMounted
|
tagsStore.fetchTags(); // 移到 onMounted
|
||||||
|
// Load initial expanded state after fetching tags/connections
|
||||||
|
expandedGroups.value = loadInitialExpandedGroups();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -390,7 +450,7 @@ defineExpose({ focusSearchInput });
|
|||||||
// --- 键盘导航和确认 ---
|
// --- 键盘导航和确认 ---
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
const list = flatVisibleConnections.value;
|
const list = flatVisibleConnections.value; // Always navigate the potentially flat list
|
||||||
if (!list.length) return;
|
if (!list.length) return;
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
@@ -418,7 +478,8 @@ const scrollToHighlighted = async () => {
|
|||||||
await nextTick(); // 等待 DOM 更新
|
await nextTick(); // 等待 DOM 更新
|
||||||
if (!listAreaRef.value || highlightedConnectionId.value === null) return;
|
if (!listAreaRef.value || highlightedConnectionId.value === null) return;
|
||||||
|
|
||||||
const highlightedElement = listAreaRef.value.querySelector(`.connection-item[data-conn-id="${highlightedConnectionId.value}"]`);
|
// Query selector needs to work for both grouped and flat lists
|
||||||
|
const highlightedElement = listAreaRef.value.querySelector(`li[data-conn-id="${highlightedConnectionId.value}"]`);
|
||||||
if (highlightedElement) {
|
if (highlightedElement) {
|
||||||
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
}
|
}
|
||||||
@@ -558,7 +619,8 @@ const cancelEditingTag = () => {
|
|||||||
<!-- Connection List Area -->
|
<!-- Connection List Area -->
|
||||||
<div class="flex-grow overflow-y-auto p-2" ref="listAreaRef">
|
<div class="flex-grow overflow-y-auto p-2" ref="listAreaRef">
|
||||||
<!-- No Results / No Connections State -->
|
<!-- No Results / No Connections State -->
|
||||||
<div v-if="filteredAndGroupedConnections.length === 0 && connections.length > 0" class="p-6 text-center text-text-secondary">
|
<!-- 修改 v-if 条件,考虑两种模式,并且仅在有搜索词时显示 "No Results" -->
|
||||||
|
<div v-if="((showConnectionTagsBoolean && filteredAndGroupedConnections.length === 0) || (!showConnectionTagsBoolean && flatFilteredConnections.length === 0)) && connections.length > 0 && searchTerm" class="p-6 text-center text-text-secondary">
|
||||||
<i class="fas fa-search text-xl mb-2"></i>
|
<i class="fas fa-search text-xl mb-2"></i>
|
||||||
<p>{{ t('workspaceConnectionList.noResults') }} "{{ searchTerm }}"</p>
|
<p>{{ t('workspaceConnectionList.noResults') }} "{{ searchTerm }}"</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,11 +635,13 @@ const cancelEditingTag = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Groups and Connections -->
|
<!-- Groups and Connections (Conditional Rendering) -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-1 last:mb-0">
|
<!-- Grouped View -->
|
||||||
<!-- Group Header -->
|
<div v-if="showConnectionTagsBoolean">
|
||||||
<div
|
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-1 last:mb-0">
|
||||||
|
<!-- Group Header -->
|
||||||
|
<div
|
||||||
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||||
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||||
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||||
@@ -632,7 +696,26 @@ const cancelEditingTag = () => {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Flat View -->
|
||||||
|
<ul v-else class="list-none p-0 m-0">
|
||||||
|
<li
|
||||||
|
v-for="conn in flatFilteredConnections"
|
||||||
|
:key="conn.id"
|
||||||
|
class="group my-0.5 py-2 pr-3 pl-4 cursor-pointer flex items-center rounded-md whitespace-nowrap overflow-hidden text-ellipsis text-foreground hover:bg-primary/10 transition-colors duration-150"
|
||||||
|
:class="{ 'bg-primary/20 text-white font-medium': conn.id === highlightedConnectionId }"
|
||||||
|
:data-conn-id="conn.id"
|
||||||
|
@click.left="handleConnect(conn.id)"
|
||||||
|
@click.right.prevent
|
||||||
|
@contextmenu.prevent="showContextMenu($event, conn)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', conn.type === 'RDP' ? 'fa-desktop' : 'fa-server', 'mr-2.5 w-4 text-center text-text-secondary group-hover:text-primary', { 'text-white': conn.id === highlightedConnectionId }]"></i>
|
||||||
|
<span class="overflow-hidden text-ellipsis whitespace-nowrap flex-grow text-sm" :title="conn.name || conn.host">
|
||||||
|
{{ conn.name || conn.host }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -625,7 +625,13 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"sidebarPersistentSaveFailed": "Failed to save sidebar setting."
|
"sidebarPersistentSaveFailed": "Failed to save sidebar setting."
|
||||||
}
|
},
|
||||||
|
"showConnectionTagsTitle": "Show Connection Tags",
|
||||||
|
"showConnectionTagsLabel": "Show tags in connection list",
|
||||||
|
"showConnectionTagsDescription": "Disable to hide tags in the connection list and exclude them from search.",
|
||||||
|
"showQuickCommandTagsTitle": "Show Quick Command Tags",
|
||||||
|
"showQuickCommandTagsLabel": "Show tags in quick command list",
|
||||||
|
"showQuickCommandTagsDescription": "Disable to hide tags in the quick command list and exclude them from search."
|
||||||
},
|
},
|
||||||
"ipBlacklist": {
|
"ipBlacklist": {
|
||||||
"title": "IP Blacklist Management",
|
"title": "IP Blacklist Management",
|
||||||
|
|||||||
@@ -857,7 +857,13 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"sidebarPersistentSaved": "サイドバーの設定を保存しました。"
|
"sidebarPersistentSaved": "サイドバーの設定を保存しました。"
|
||||||
},
|
},
|
||||||
"title": "ワークスペースとターミナル"
|
"title": "ワークスペースとターミナル",
|
||||||
|
"showConnectionTagsTitle": "接続タグを表示",
|
||||||
|
"showConnectionTagsLabel": "接続リストにタグを表示",
|
||||||
|
"showConnectionTagsDescription": "無効にすると、接続リストのタグが非表示になり、検索から除外されます。",
|
||||||
|
"showQuickCommandTagsTitle": "クイックコマンドタグを表示",
|
||||||
|
"showQuickCommandTagsLabel": "クイックコマンドリストにタグを表示",
|
||||||
|
"showQuickCommandTagsDescription": "無効にすると、クイックコマンドリストのタグが非表示になり、検索から除外されます。"
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"version": "バージョン",
|
"version": "バージョン",
|
||||||
|
|||||||
@@ -625,7 +625,13 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"sidebarPersistentSaveFailed": "保存侧边栏设置失败。"
|
"sidebarPersistentSaveFailed": "保存侧边栏设置失败。"
|
||||||
}
|
},
|
||||||
|
"showConnectionTagsTitle": "显示连接标签",
|
||||||
|
"showConnectionTagsLabel": "在连接列表中显示标签",
|
||||||
|
"showConnectionTagsDescription": "关闭后将隐藏连接列表中的标签,并从搜索中排除标签。",
|
||||||
|
"showQuickCommandTagsTitle": "显示快捷指令标签",
|
||||||
|
"showQuickCommandTagsLabel": "在快捷指令列表中显示标签",
|
||||||
|
"showQuickCommandTagsDescription": "关闭后将隐藏快捷指令列表中的标签,并从搜索中排除标签。"
|
||||||
},
|
},
|
||||||
"ipBlacklist": {
|
"ipBlacklist": {
|
||||||
"title": "IP 黑名单管理",
|
"title": "IP 黑名单管理",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ interface SettingsState {
|
|||||||
ipBlacklistEnabled?: string;
|
ipBlacklistEnabled?: string;
|
||||||
dashboardSortBy?: SortField;
|
dashboardSortBy?: SortField;
|
||||||
dashboardSortOrder?: SortOrder;
|
dashboardSortOrder?: SortOrder;
|
||||||
|
showConnectionTags?: string; // 'true' or 'false'
|
||||||
|
showQuickCommandTags?: string; // 'true' or 'false'
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +84,23 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[SettingsStore] 加载通用设置...');
|
console.log('[SettingsStore] 加载通用设置...');
|
||||||
const response = await apiClient.get<Record<string, string>>('/settings'); // 使用 apiClient
|
// Fetch all settings, including the new ones
|
||||||
settings.value = response.data; // Store fetched general settings
|
const [
|
||||||
|
generalSettingsResponse,
|
||||||
|
showConnectionTagsResponse,
|
||||||
|
showQuickCommandTagsResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
apiClient.get<Record<string, string>>('/settings'),
|
||||||
|
apiClient.get<{ enabled: boolean }>('/settings/show-connection-tags'),
|
||||||
|
apiClient.get<{ enabled: boolean }>('/settings/show-quick-command-tags')
|
||||||
|
]);
|
||||||
|
|
||||||
|
settings.value = generalSettingsResponse.data; // Store fetched general settings
|
||||||
|
|
||||||
|
// Store the specific boolean settings
|
||||||
|
settings.value.showConnectionTags = String(showConnectionTagsResponse.data.enabled);
|
||||||
|
settings.value.showQuickCommandTags = String(showQuickCommandTagsResponse.data.enabled);
|
||||||
|
|
||||||
// --- 更详细的日志 ---
|
// --- 更详细的日志 ---
|
||||||
console.log('[SettingsStore] Fetched settings from backend:', JSON.stringify(settings.value));
|
console.log('[SettingsStore] Fetched settings from backend:', JSON.stringify(settings.value));
|
||||||
|
|
||||||
@@ -238,6 +255,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
settings.value.dashboardSortOrder = 'desc';
|
settings.value.dashboardSortOrder = 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Tag visibility defaults
|
||||||
|
if (settings.value.showConnectionTags === undefined) {
|
||||||
|
settings.value.showConnectionTags = 'true'; // 默认显示
|
||||||
|
}
|
||||||
|
if (settings.value.showQuickCommandTags === undefined) {
|
||||||
|
settings.value.showQuickCommandTags = 'true'; // 默认显示
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- 语言设置 ---
|
// --- 语言设置 ---
|
||||||
const langFromSettings = settings.value.language;
|
const langFromSettings = settings.value.language;
|
||||||
console.log(`[SettingsStore] Language from fetched settings: ${langFromSettings}`); // <-- 添加日志
|
console.log(`[SettingsStore] Language from fetched settings: ${langFromSettings}`); // <-- 添加日志
|
||||||
@@ -299,10 +325,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a single general setting value both locally and on the backend.
|
* Updates a single general setting value both locally and on the backend.
|
||||||
|
* Uses specific endpoints for boolean settings where available.
|
||||||
* @param key The setting key to update.
|
* @param key The setting key to update.
|
||||||
* @param value The new value for the setting.
|
* @param value The new value for the setting (string for general, boolean for specific).
|
||||||
*/
|
*/
|
||||||
async function updateSetting(key: keyof SettingsState, value: string) {
|
async function updateSetting(key: keyof SettingsState, value: string | boolean) {
|
||||||
// 移除外观相关的键检查
|
// 移除外观相关的键检查
|
||||||
const allowedKeys: Array<keyof SettingsState> = [
|
const allowedKeys: Array<keyof SettingsState> = [
|
||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
@@ -319,26 +346,48 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
|
'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
|
||||||
'ipBlacklistEnabled',
|
'ipBlacklistEnabled',
|
||||||
'dashboardSortBy',
|
'dashboardSortBy',
|
||||||
'dashboardSortOrder'
|
'dashboardSortOrder',
|
||||||
|
'showConnectionTags', // NEW
|
||||||
|
'showQuickCommandTags' // NEW
|
||||||
];
|
];
|
||||||
if (!allowedKeys.includes(key)) {
|
if (!allowedKeys.includes(key)) {
|
||||||
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
||||||
throw new Error(`不允许更新设置项 '${key}'`);
|
throw new Error(`不允许更新设置项 '${key}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use specific endpoints for boolean settings
|
||||||
|
const booleanEndpoints: Partial<Record<keyof SettingsState, string>> = {
|
||||||
|
showConnectionTags: '/settings/show-connection-tags',
|
||||||
|
showQuickCommandTags: '/settings/show-quick-command-tags',
|
||||||
|
autoCopyOnSelect: '/settings/auto-copy-on-select',
|
||||||
|
// Add other boolean settings with specific endpoints here if needed
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[SettingsStore] Attempting to update setting - Key: ${key}, Value: ${value}`); // +++ Add log +++
|
let apiPromise: Promise<any>;
|
||||||
// 注意:后端 controller 现在会过滤,但前端也做一层检查更好
|
const endpoint = booleanEndpoints[key];
|
||||||
const payload = { [key]: value };
|
|
||||||
console.log('[SettingsStore] Sending PUT request to /settings with payload:', payload); // +++ Add log +++
|
if (endpoint && typeof value === 'boolean') {
|
||||||
await apiClient.put('/settings', payload); // 使用 apiClient
|
console.log(`[SettingsStore] Attempting to update boolean setting via specific endpoint - Key: ${key}, Value: ${value}, Endpoint: ${endpoint}`);
|
||||||
console.log(`[SettingsStore] Successfully updated setting via API - Key: ${key}`); // +++ Add log +++
|
apiPromise = apiClient.put(endpoint, { enabled: value });
|
||||||
// Update store state *after* successful API call
|
} else if (typeof value === 'string') {
|
||||||
settings.value = { ...settings.value, [key]: value };
|
console.log(`[SettingsStore] Attempting to update general setting - Key: ${key}, Value: ${value}`);
|
||||||
|
const payload = { [key]: value };
|
||||||
|
console.log('[SettingsStore] Sending PUT request to /settings with payload:', payload);
|
||||||
|
apiPromise = apiClient.put('/settings', payload);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid value type for setting '${key}': expected boolean for specific endpoint or string for general.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiPromise;
|
||||||
|
console.log(`[SettingsStore] Successfully updated setting via API - Key: ${key}`);
|
||||||
|
|
||||||
|
// Update store state *after* successful API call
|
||||||
|
settings.value = { ...settings.value, [key]: String(value) }; // Store as string internally
|
||||||
|
|
||||||
// If updating language, check if it's valid and update i18n
|
// If updating language, check if it's valid and update i18n
|
||||||
if (key === 'language' && availableLocales.includes(value)) {
|
if (key === 'language' && typeof value === 'string' && availableLocales.includes(value)) {
|
||||||
console.log(`[SettingsStore] updateSetting: Language updated to ${value}. Calling setLocale...`); // <-- 添加日志
|
console.log(`[SettingsStore] updateSetting: Language updated to ${value}. Calling setLocale...`);
|
||||||
setLocale(value);
|
setLocale(value);
|
||||||
} else if (key === 'language') {
|
} else if (key === 'language') {
|
||||||
console.warn(`[SettingsStore] updateSetting: Attempted to set invalid language '${value}'. Ignoring i18n update.`);
|
console.warn(`[SettingsStore] updateSetting: Attempted to set invalid language '${value}'. Ignoring i18n update.`);
|
||||||
@@ -381,7 +430,9 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
|
'rdpModalHeight', // NEW: 添加 RDP 模态框高度键
|
||||||
'ipBlacklistEnabled',
|
'ipBlacklistEnabled',
|
||||||
'dashboardSortBy',
|
'dashboardSortBy',
|
||||||
'dashboardSortOrder'
|
'dashboardSortOrder',
|
||||||
|
'showConnectionTags', // NEW
|
||||||
|
'showQuickCommandTags' // NEW
|
||||||
];
|
];
|
||||||
const filteredUpdates: Partial<SettingsState> = {};
|
const filteredUpdates: Partial<SettingsState> = {};
|
||||||
let languageUpdate: string | undefined = undefined;
|
let languageUpdate: string | undefined = undefined;
|
||||||
@@ -435,7 +486,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const newWidths = { ...parsedSidebarPaneWidths.value, [paneName]: width };
|
const newWidths = { ...parsedSidebarPaneWidths.value, [paneName]: width };
|
||||||
parsedSidebarPaneWidths.value = newWidths; // Update local reactive state first
|
parsedSidebarPaneWidths.value = newWidths; // Update local reactive state first
|
||||||
try {
|
try {
|
||||||
await updateSetting('sidebarPaneWidths', JSON.stringify(newWidths));
|
// Use updateMultipleSettings for consistency, even for one setting
|
||||||
|
await updateMultipleSettings({ sidebarPaneWidths: JSON.stringify(newWidths) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[SettingsStore] Failed to save sidebarPaneWidths after updating ${paneName}:`, error);
|
console.error(`[SettingsStore] Failed to save sidebarPaneWidths after updating ${paneName}:`, error);
|
||||||
// Optionally revert local state or show error to user
|
// Optionally revert local state or show error to user
|
||||||
@@ -642,6 +694,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const recaptchaSiteKey = computed(() => captchaSettings.value?.recaptchaSiteKey ?? '');
|
const recaptchaSiteKey = computed(() => captchaSettings.value?.recaptchaSiteKey ?? '');
|
||||||
// DO NOT expose secret keys via getters
|
// DO NOT expose secret keys via getters
|
||||||
|
|
||||||
|
// NEW: Getters for tag visibility
|
||||||
|
const showConnectionTagsBoolean = computed(() => {
|
||||||
|
return settings.value.showConnectionTags !== 'false'; // Default to true
|
||||||
|
});
|
||||||
|
const showQuickCommandTagsBoolean = computed(() => {
|
||||||
|
return settings.value.showQuickCommandTags !== 'false'; // Default to true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings, // 只包含通用设置
|
settings, // 只包含通用设置
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -677,5 +738,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
dashboardSortBy,
|
dashboardSortBy,
|
||||||
dashboardSortOrder,
|
dashboardSortOrder,
|
||||||
saveDashboardSortPreference,
|
saveDashboardSortPreference,
|
||||||
|
// NEW: Expose tag visibility getters
|
||||||
|
showConnectionTagsBoolean,
|
||||||
|
showQuickCommandTagsBoolean,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,88 +26,123 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- List Area -->
|
<!-- List Area -->
|
||||||
<div class="flex-grow overflow-y-auto p-2">
|
<div class="flex-grow overflow-y-auto p-2">
|
||||||
<!-- Loading State (Show if loading and no groups are ready yet) -->
|
<!-- Loading State -->
|
||||||
<div v-if="isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
<div v-if="isLoading && quickCommandsStore.quickCommandsList.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||||
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
||||||
<p>{{ t('common.loading', '加载中...') }}</p>
|
<p>{{ t('common.loading', '加载中...') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Empty State (Show if not loading and no groups exist) -->
|
<!-- Empty State (No commands at all) -->
|
||||||
<div v-else-if="!isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
<div v-else-if="!isLoading && quickCommandsStore.quickCommandsList.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||||
<i class="fas fa-bolt text-xl mb-2"></i>
|
<i class="fas fa-bolt text-xl mb-2"></i>
|
||||||
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
||||||
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||||
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
|
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Grouped Command List -->
|
<!-- No Results State (Commands exist, but filter yields no results) -->
|
||||||
|
<div v-else-if="!isLoading && ((showQuickCommandTagsBoolean && filteredAndGroupedCommands.length === 0) || (!showQuickCommandTagsBoolean && flatFilteredCommands.length === 0)) && searchTerm" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||||
|
<i class="fas fa-search text-xl mb-2"></i>
|
||||||
|
<p>{{ t('quickCommands.noResults', '没有找到匹配的指令') }} "{{ searchTerm }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Command List (Grouped or Flat) -->
|
||||||
<div v-else class="list-none p-0 m-0" ref="commandListContainerRef"> <!-- Changed ref name -->
|
<div v-else class="list-none p-0 m-0" ref="commandListContainerRef"> <!-- Changed ref name -->
|
||||||
<div v-for="groupData in filteredAndGroupedCommands" :key="groupData.groupName" class="mb-1 last:mb-0">
|
<!-- Grouped View -->
|
||||||
<!-- Group Header -->
|
<div v-if="showQuickCommandTagsBoolean">
|
||||||
<!-- Group Header - Modified for inline editing -->
|
<div v-for="groupData in filteredAndGroupedCommands" :key="groupData.groupName" class="mb-1 last:mb-0">
|
||||||
<div
|
<!-- Group Header - Modified for inline editing -->
|
||||||
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
<div
|
||||||
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||||
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||||
>
|
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||||
<i
|
>
|
||||||
:class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"
|
<i
|
||||||
@click.stop="toggleGroup(groupData.groupName)"
|
:class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"
|
||||||
class="cursor-pointer flex-shrink-0"
|
@click.stop="toggleGroup(groupData.groupName)"
|
||||||
></i>
|
class="cursor-pointer flex-shrink-0"
|
||||||
<!-- Editing State -->
|
></i>
|
||||||
<input
|
<!-- Editing State -->
|
||||||
v-if="editingTagId === (groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
<input
|
||||||
:key="groupData.tagId === null ? 'untagged-input' : `tag-input-${groupData.tagId}`"
|
v-if="editingTagId === (groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
:ref="(el) => setTagInputRef(el, groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
:key="groupData.tagId === null ? 'untagged-input' : `tag-input-${groupData.tagId}`"
|
||||||
type="text"
|
:ref="(el) => setTagInputRef(el, groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||||
v-model="editedTagName"
|
type="text"
|
||||||
class="text-sm bg-input border border-primary rounded px-1 py-0 w-full"
|
v-model="editedTagName"
|
||||||
@blur="finishEditingTag"
|
class="text-sm bg-input border border-primary rounded px-1 py-0 w-full"
|
||||||
@keydown.enter.prevent="finishEditingTag"
|
@blur="finishEditingTag"
|
||||||
@keydown.esc.prevent="cancelEditingTag"
|
@keydown.enter.prevent="finishEditingTag"
|
||||||
@click.stop
|
@keydown.esc.prevent="cancelEditingTag"
|
||||||
/>
|
@click.stop
|
||||||
<!-- Display State -->
|
/>
|
||||||
<span
|
<!-- Display State -->
|
||||||
v-else
|
<span
|
||||||
class="text-sm inline-block overflow-hidden text-ellipsis whitespace-nowrap"
|
v-else
|
||||||
:class="{ 'cursor-pointer hover:underline': true }"
|
class="text-sm inline-block overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
:title="t('quickCommands.tags.clickToEditTag', '点击编辑标签')"
|
:class="{ 'cursor-pointer hover:underline': true }"
|
||||||
@click.stop="startEditingTag(groupData.tagId, groupData.groupName)"
|
:title="t('quickCommands.tags.clickToEditTag', '点击编辑标签')"
|
||||||
>
|
@click.stop="startEditingTag(groupData.tagId, groupData.groupName)"
|
||||||
{{ groupData.groupName }}
|
>
|
||||||
</span>
|
{{ groupData.groupName }}
|
||||||
<!-- Optional: Add count? -->
|
</span>
|
||||||
<!-- <span v-if="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId)" class="ml-auto text-xs text-text-secondary pl-2">({{ groupData.commands.length }})</span> -->
|
<!-- Optional: Add count? -->
|
||||||
</div>
|
<!-- <span v-if="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId)" class="ml-auto text-xs text-text-secondary pl-2">({{ groupData.commands.length }})</span> -->
|
||||||
<!-- Command Items List (only show if expanded) -->
|
</div>
|
||||||
<ul v-show="quickCommandsStore.expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
<!-- Command Items List (only show if expanded) -->
|
||||||
<li
|
<ul v-show="quickCommandsStore.expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
||||||
v-for="(cmd) in groupData.commands"
|
<li
|
||||||
:key="cmd.id"
|
v-for="(cmd) in groupData.commands"
|
||||||
:data-command-id="cmd.id"
|
:key="cmd.id"
|
||||||
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
:data-command-id="cmd.id"
|
||||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||||
@click="executeCommand(cmd)"
|
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||||
>
|
@click="executeCommand(cmd)"
|
||||||
<!-- Command Info (Structure remains the same) -->
|
>
|
||||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
<!-- Command Info (Structure remains the same) -->
|
||||||
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||||
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
||||||
</div>
|
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
||||||
<!-- Actions (Structure remains the same) -->
|
</div>
|
||||||
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
<!-- Actions (Structure remains the same) -->
|
||||||
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||||
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
||||||
<i class="fas fa-edit text-sm"></i>
|
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
||||||
</button>
|
<i class="fas fa-edit text-sm"></i>
|
||||||
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
</button>
|
||||||
<i class="fas fa-times text-sm"></i>
|
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
||||||
</button>
|
<i class="fas fa-times text-sm"></i>
|
||||||
</div>
|
</button>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Flat View -->
|
||||||
|
<ul v-else class="list-none p-0 m-0">
|
||||||
|
<li
|
||||||
|
v-for="(cmd) in flatFilteredCommands"
|
||||||
|
:key="cmd.id"
|
||||||
|
:data-command-id="cmd.id"
|
||||||
|
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||||
|
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||||
|
@click="executeCommand(cmd)"
|
||||||
|
>
|
||||||
|
<!-- Command Info -->
|
||||||
|
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||||
|
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
||||||
|
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||||
|
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
||||||
|
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
||||||
|
<i class="fas fa-edit text-sm"></i>
|
||||||
|
</button>
|
||||||
|
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
||||||
|
<i class="fas fa-times text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,12 +165,14 @@ import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件
|
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 新增:导入设置 store
|
||||||
|
|
||||||
const quickCommandsStore = useQuickCommandsStore();
|
const quickCommandsStore = useQuickCommandsStore();
|
||||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++
|
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++
|
||||||
const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知
|
const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||||
|
const settingsStore = useSettingsStore(); // 新增:实例化设置 store
|
||||||
|
|
||||||
const hoveredItemId = ref<number | null>(null);
|
const hoveredItemId = ref<number | null>(null);
|
||||||
const isFormVisible = ref(false);
|
const isFormVisible = ref(false);
|
||||||
@@ -159,9 +196,17 @@ const isLoading = computed(() => quickCommandsStore.isLoading);
|
|||||||
// selectedIndex now refers to the index within the flatVisibleCommands list
|
// selectedIndex now refers to the index within the flatVisibleCommands list
|
||||||
// Also get expandedGroups reactively for the template
|
// Also get expandedGroups reactively for the template
|
||||||
const { selectedIndex: storeSelectedIndex, flatVisibleCommands, expandedGroups } = storeToRefs(quickCommandsStore);
|
const { selectedIndex: storeSelectedIndex, flatVisibleCommands, expandedGroups } = storeToRefs(quickCommandsStore);
|
||||||
|
const { showQuickCommandTagsBoolean } = storeToRefs(settingsStore); // 新增:获取设置项
|
||||||
|
|
||||||
|
// 新增:计算属性,仅过滤和排序,不分组
|
||||||
|
const flatFilteredCommands = computed(() => {
|
||||||
|
// 直接使用 store 中的 flatVisibleCommands,因为它已经处理了过滤和排序
|
||||||
|
return quickCommandsStore.flatVisibleCommands;
|
||||||
|
});
|
||||||
|
|
||||||
// --- Helper function for selection check ---
|
// --- Helper function for selection check ---
|
||||||
const isCommandSelected = (commandId: number): boolean => {
|
const isCommandSelected = (commandId: number): boolean => {
|
||||||
|
// 使用 store 的 flatVisibleCommands 和 storeSelectedIndex
|
||||||
if (storeSelectedIndex.value < 0 || !flatVisibleCommands.value[storeSelectedIndex.value]) {
|
if (storeSelectedIndex.value < 0 || !flatVisibleCommands.value[storeSelectedIndex.value]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -181,8 +226,18 @@ onMounted(async () => { // Make onMounted async
|
|||||||
await quickCommandsStore.fetchQuickCommands();
|
await quickCommandsStore.fetchQuickCommands();
|
||||||
// Also fetch the quick command tags using the correct store instance
|
// Also fetch the quick command tags using the correct store instance
|
||||||
await quickCommandTagsStore.fetchTags();
|
await quickCommandTagsStore.fetchTags();
|
||||||
|
// +++ 注册自定义聚焦动作 +++
|
||||||
|
unregisterFocus = focusSwitcherStore.registerFocusAction('quickCommandsSearch', focusSearchInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// +++ 调用保存的注销函数 +++
|
||||||
|
if (unregisterFocus) {
|
||||||
|
unregisterFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// +++ Watcher to focus input when editing starts +++
|
// +++ Watcher to focus input when editing starts +++
|
||||||
watch(editingTagId, async (newId) => {
|
watch(editingTagId, async (newId) => {
|
||||||
if (newId !== null) {
|
if (newId !== null) {
|
||||||
@@ -197,18 +252,11 @@ watch(editingTagId, async (newId) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 新增:监听显示模式变化,重置选择
|
||||||
|
watch(showQuickCommandTagsBoolean, () => {
|
||||||
|
quickCommandsStore.resetSelection();
|
||||||
|
});
|
||||||
|
|
||||||
// +++ 注册/注销自定义聚焦动作 +++
|
|
||||||
onMounted(() => {
|
|
||||||
// +++ 保存返回的注销函数 +++
|
|
||||||
unregisterFocus = focusSwitcherStore.registerFocusAction('quickCommandsSearch', focusSearchInput);
|
|
||||||
});
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
// +++ 调用保存的注销函数 +++
|
|
||||||
if (unregisterFocus) {
|
|
||||||
unregisterFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 事件处理 ---
|
// --- 事件处理 ---
|
||||||
|
|
||||||
@@ -221,12 +269,13 @@ const updateSearchTerm = (event: Event) => {
|
|||||||
// +++ 重构滚动逻辑 +++
|
// +++ 重构滚动逻辑 +++
|
||||||
const scrollToSelected = async (index: number) => {
|
const scrollToSelected = async (index: number) => {
|
||||||
await nextTick(); // 等待 DOM 更新
|
await nextTick(); // 等待 DOM 更新
|
||||||
|
// 使用 store 的 flatVisibleCommands
|
||||||
if (index < 0 || !commandListContainerRef.value || !flatVisibleCommands.value[index]) return;
|
if (index < 0 || !commandListContainerRef.value || !flatVisibleCommands.value[index]) return;
|
||||||
|
|
||||||
const selectedCommandId = flatVisibleCommands.value[index].id;
|
const selectedCommandId = flatVisibleCommands.value[index].id;
|
||||||
const listContainer = commandListContainerRef.value;
|
const listContainer = commandListContainerRef.value;
|
||||||
|
|
||||||
// Find the element using the data attribute
|
// Find the element using the data attribute (works for both views)
|
||||||
const selectedElement = listContainer.querySelector(`li[data-command-id="${selectedCommandId}"]`) as HTMLLIElement;
|
const selectedElement = listContainer.querySelector(`li[data-command-id="${selectedCommandId}"]`) as HTMLLIElement;
|
||||||
|
|
||||||
if (selectedElement) {
|
if (selectedElement) {
|
||||||
@@ -244,9 +293,9 @@ watch(storeSelectedIndex, (newIndex) => {
|
|||||||
scrollToSelected(newIndex);
|
scrollToSelected(newIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard navigation now operates on the flat visible list
|
// Keyboard navigation now operates on the flat visible list from the store
|
||||||
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
||||||
// Use flatVisibleCommands for navigation logic
|
// 使用 store 的 flatVisibleCommands
|
||||||
const commands = flatVisibleCommands.value;
|
const commands = flatVisibleCommands.value;
|
||||||
if (!commands.length) return;
|
if (!commands.length) return;
|
||||||
|
|
||||||
@@ -263,6 +312,7 @@ const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
|||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
// 使用 store 的 storeSelectedIndex
|
||||||
if (storeSelectedIndex.value >= 0 && storeSelectedIndex.value < commands.length) {
|
if (storeSelectedIndex.value >= 0 && storeSelectedIndex.value < commands.length) {
|
||||||
executeCommand(commands[storeSelectedIndex.value]);
|
executeCommand(commands[storeSelectedIndex.value]);
|
||||||
}
|
}
|
||||||
@@ -294,6 +344,7 @@ const toggleGroup = (groupName: string) => {
|
|||||||
// After toggling, selection might become invalid if the selected item is now hidden
|
// After toggling, selection might become invalid if the selected item is now hidden
|
||||||
// Reset selection or check if the selected item is still visible
|
// Reset selection or check if the selected item is still visible
|
||||||
nextTick(() => { // Wait for DOM update potentially caused by v-show
|
nextTick(() => { // Wait for DOM update potentially caused by v-show
|
||||||
|
// 使用 store 的 flatVisibleCommands 和 storeSelectedIndex
|
||||||
const selectedCmdId = storeSelectedIndex.value >= 0 && flatVisibleCommands.value[storeSelectedIndex.value]
|
const selectedCmdId = storeSelectedIndex.value >= 0 && flatVisibleCommands.value[storeSelectedIndex.value]
|
||||||
? flatVisibleCommands.value[storeSelectedIndex.value].id
|
? flatVisibleCommands.value[storeSelectedIndex.value].id
|
||||||
: null;
|
: null;
|
||||||
@@ -476,4 +527,3 @@ const cancelEditingTag = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -444,6 +444,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<hr class="border-border/50"> <!-- NEW: Separator -->
|
||||||
|
<!-- Show Connection Tags -->
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.workspace.showConnectionTagsTitle', '显示连接标签') }}</h3>
|
||||||
|
<form @submit.prevent="handleUpdateShowConnectionTags" class="space-y-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="showConnectionTags" v-model="showConnectionTagsLocal"
|
||||||
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
|
<label for="showConnectionTags" class="text-sm text-foreground cursor-pointer select-none">{{ $t('settings.workspace.showConnectionTagsLabel', '在连接列表中显示标签') }}</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-text-secondary mt-1">{{ $t('settings.workspace.showConnectionTagsDescription', '关闭后将隐藏连接列表中的标签,并从搜索中排除标签。') }}</p>
|
||||||
|
<div class="flex items-center justify-between pt-2">
|
||||||
|
<button type="submit" :disabled="showConnectionTagsLoading"
|
||||||
|
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 disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out text-sm font-medium">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="showConnectionTagsMessage" :class="['text-sm', showConnectionTagsSuccess ? 'text-success' : 'text-error']">{{ showConnectionTagsMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<hr class="border-border/50"> <!-- NEW: Separator -->
|
||||||
|
<!-- Show Quick Command Tags -->
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.workspace.showQuickCommandTagsTitle', '显示快捷指令标签') }}</h3>
|
||||||
|
<form @submit.prevent="handleUpdateShowQuickCommandTags" class="space-y-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="showQuickCommandTags" v-model="showQuickCommandTagsLocal"
|
||||||
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
|
<label for="showQuickCommandTags" class="text-sm text-foreground cursor-pointer select-none">{{ $t('settings.workspace.showQuickCommandTagsLabel', '在快捷指令列表中显示标签') }}</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-text-secondary mt-1">{{ $t('settings.workspace.showQuickCommandTagsDescription', '关闭后将隐藏快捷指令列表中的标签,并从搜索中排除标签。') }}</p>
|
||||||
|
<div class="flex items-center justify-between pt-2">
|
||||||
|
<button type="submit" :disabled="showQuickCommandTagsLoading"
|
||||||
|
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 disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out text-sm font-medium">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="showQuickCommandTagsMessage" :class="['text-sm', showQuickCommandTagsSuccess ? 'text-success' : 'text-error']">{{ showQuickCommandTagsMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -624,6 +664,8 @@ const {
|
|||||||
captchaSettings, // <-- Import CAPTCHA settings state
|
captchaSettings, // <-- Import CAPTCHA settings state
|
||||||
commandInputSyncTarget, // NEW: Import command input sync target getter
|
commandInputSyncTarget, // NEW: Import command input sync target getter
|
||||||
ipBlacklistEnabledBoolean, // <-- Import IP Blacklist enabled getter
|
ipBlacklistEnabledBoolean, // <-- Import IP Blacklist enabled getter
|
||||||
|
showConnectionTagsBoolean, // NEW: Import connection tag visibility getter
|
||||||
|
showQuickCommandTagsBoolean, // NEW: Import quick command tag visibility getter
|
||||||
} = storeToRefs(settingsStore);
|
} = storeToRefs(settingsStore);
|
||||||
|
|
||||||
// Removed Passkey state import from authStore
|
// Removed Passkey state import from authStore
|
||||||
@@ -648,6 +690,8 @@ const popupEditorEnabled = ref(true); // 本地状态,用于 v-model
|
|||||||
const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态
|
const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态
|
||||||
const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target
|
const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target
|
||||||
const ipBlacklistEnabled = ref(true); // <-- Local state for IP Blacklist switch
|
const ipBlacklistEnabled = ref(true); // <-- Local state for IP Blacklist switch
|
||||||
|
const showConnectionTagsLocal = ref(true); // NEW: Local state for connection tags switch
|
||||||
|
const showQuickCommandTagsLocal = ref(true); // NEW: Local state for quick command tags switch
|
||||||
|
|
||||||
// --- Local UI feedback state ---
|
// --- Local UI feedback state ---
|
||||||
const ipWhitelistLoading = ref(false);
|
const ipWhitelistLoading = ref(false);
|
||||||
@@ -692,6 +736,12 @@ const selectedTimezone = ref('UTC'); // 本地状态,用于时区 v-model
|
|||||||
const timezoneLoading = ref(false);
|
const timezoneLoading = ref(false);
|
||||||
const timezoneMessage = ref('');
|
const timezoneMessage = ref('');
|
||||||
const timezoneSuccess = ref(false);
|
const timezoneSuccess = ref(false);
|
||||||
|
const showConnectionTagsLoading = ref(false); // NEW
|
||||||
|
const showConnectionTagsMessage = ref(''); // NEW
|
||||||
|
const showConnectionTagsSuccess = ref(false); // NEW
|
||||||
|
const showQuickCommandTagsLoading = ref(false); // NEW
|
||||||
|
const showQuickCommandTagsMessage = ref(''); // NEW
|
||||||
|
const showQuickCommandTagsSuccess = ref(false); // NEW
|
||||||
// CAPTCHA Form State
|
// CAPTCHA Form State
|
||||||
const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the form object
|
const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the form object
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -740,6 +790,8 @@ watch(settings, (newSettings, oldSettings) => {
|
|||||||
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
|
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
|
||||||
selectedTimezone.value = newSettings.timezone || 'UTC'; // 同步时区设置
|
selectedTimezone.value = newSettings.timezone || 'UTC'; // 同步时区设置
|
||||||
ipBlacklistEnabled.value = ipBlacklistEnabledBoolean.value; // <-- Sync IP Blacklist enabled state
|
ipBlacklistEnabled.value = ipBlacklistEnabledBoolean.value; // <-- Sync IP Blacklist enabled state
|
||||||
|
showConnectionTagsLocal.value = showConnectionTagsBoolean.value; // NEW: Sync connection tags state
|
||||||
|
showQuickCommandTagsLocal.value = showQuickCommandTagsBoolean.value; // NEW: Sync quick command tags state
|
||||||
|
|
||||||
}, { deep: true, immediate: true }); // immediate: true to run on initial load
|
}, { deep: true, immediate: true }); // immediate: true to run on initial load
|
||||||
|
|
||||||
@@ -928,6 +980,46 @@ const handleUpdateTimezone = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Show Connection Tags setting method ---
|
||||||
|
const handleUpdateShowConnectionTags = async () => {
|
||||||
|
showConnectionTagsLoading.value = true;
|
||||||
|
showConnectionTagsMessage.value = '';
|
||||||
|
showConnectionTagsSuccess.value = false;
|
||||||
|
try {
|
||||||
|
await settingsStore.updateSetting('showConnectionTags', showConnectionTagsLocal.value);
|
||||||
|
showConnectionTagsMessage.value = t('settings.workspace.success.showConnectionTagsSaved', '连接标签显示设置已保存'); // 需要添加翻译
|
||||||
|
showConnectionTagsSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新显示连接标签设置失败:', error);
|
||||||
|
showConnectionTagsMessage.value = error.message || t('settings.workspace.error.showConnectionTagsSaveFailed', '保存连接标签显示设置失败'); // 需要添加翻译
|
||||||
|
showConnectionTagsSuccess.value = false;
|
||||||
|
// No need to revert local state on failure with explicit save button
|
||||||
|
} finally {
|
||||||
|
showConnectionTagsLoading.value = false;
|
||||||
|
// Keep message visible until next save attempt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Show Quick Command Tags setting method ---
|
||||||
|
const handleUpdateShowQuickCommandTags = async () => {
|
||||||
|
showQuickCommandTagsLoading.value = true;
|
||||||
|
showQuickCommandTagsMessage.value = '';
|
||||||
|
showQuickCommandTagsSuccess.value = false;
|
||||||
|
try {
|
||||||
|
await settingsStore.updateSetting('showQuickCommandTags', showQuickCommandTagsLocal.value);
|
||||||
|
showQuickCommandTagsMessage.value = t('settings.workspace.success.showQuickCommandTagsSaved', '快捷指令标签显示设置已保存'); // 需要添加翻译
|
||||||
|
showQuickCommandTagsSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新显示快捷指令标签设置失败:', error);
|
||||||
|
showQuickCommandTagsMessage.value = error.message || t('settings.workspace.error.showQuickCommandTagsSaveFailed', '保存快捷指令标签显示设置失败'); // 需要添加翻译
|
||||||
|
showQuickCommandTagsSuccess.value = false;
|
||||||
|
// No need to revert local state on failure with explicit save button
|
||||||
|
} finally {
|
||||||
|
showQuickCommandTagsLoading.value = false;
|
||||||
|
// Keep message visible until next save attempt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- 外观设置 ---
|
// --- 外观设置 ---
|
||||||
const openStyleCustomizer = () => {
|
const openStyleCustomizer = () => {
|
||||||
appearanceStore.toggleStyleCustomizer(true);
|
appearanceStore.toggleStyleCustomizer(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user