diff --git a/dashboard/src/router/MainRoutes.ts b/dashboard/src/router/MainRoutes.ts index e21b383c6..d86edcf0b 100644 --- a/dashboard/src/router/MainRoutes.ts +++ b/dashboard/src/router/MainRoutes.ts @@ -19,7 +19,7 @@ const MainRoutes = { { name: 'Commands', path: '/commands', - component: () => import('@/views/CommandPage.vue') + component: () => import('@/views/command/index.vue') }, { name: 'ExtensionMarketplace', diff --git a/dashboard/src/views/command/components/CommandFilters.vue b/dashboard/src/views/command/components/CommandFilters.vue new file mode 100644 index 000000000..c4b212803 --- /dev/null +++ b/dashboard/src/views/command/components/CommandFilters.vue @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ tm('filters.showSystemPlugins') }} + + + mdi-alert-circle + + {{ tm('filters.systemPluginConflictHint') }} + + + + + + + + diff --git a/dashboard/src/views/command/components/CommandTable.vue b/dashboard/src/views/command/components/CommandTable.vue new file mode 100644 index 000000000..44a879c8b --- /dev/null +++ b/dashboard/src/views/command/components/CommandTable.vue @@ -0,0 +1,255 @@ + + + + + + + + + + {{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }} + + + + + + {{ item.effective_command }} + + + + + + + + {{ getTypeInfo(item.type).icon }} + {{ getTypeInfo(item.type).text }}{{ item.is_group && item.sub_commands?.length > 0 ? `(${item.sub_commands.length})` : '' }} + + + + + {{ item.plugin_display_name || item.plugin }} + + + + + {{ item.description || '-' }} + + + + + + {{ getPermissionLabel(item.permission) }} + + + + + + {{ getStatusInfo(item).text }} + + + + + + + + mdi-play + {{ tm('tooltips.enable') }} + + + mdi-pause + {{ tm('tooltips.disable') }} + + + + mdi-pencil + {{ tm('tooltips.rename') }} + + + + mdi-information + {{ tm('tooltips.viewDetails') }} + + + + + + + + mdi-console-line + {{ tm('empty.noCommands') }} + {{ tm('empty.noCommandsDesc') }} + + + + + + + + + + diff --git a/dashboard/src/views/command/components/DetailsDialog.vue b/dashboard/src/views/command/components/DetailsDialog.vue new file mode 100644 index 000000000..6d9188374 --- /dev/null +++ b/dashboard/src/views/command/components/DetailsDialog.vue @@ -0,0 +1,143 @@ + + + + + + {{ tm('dialogs.details.title') }} + + + + {{ tm('dialogs.details.type') }} + + + {{ getTypeInfo(command.type).icon }} + {{ getTypeInfo(command.type).text }} + + + + + {{ tm('dialogs.details.handler') }} + {{ command.handler_name }} + + + {{ tm('dialogs.details.module') }} + {{ command.module_path }} + + + {{ tm('dialogs.details.originalCommand') }} + {{ command.original_command }} + + + {{ tm('dialogs.details.effectiveCommand') }} + {{ command.effective_command }} + + + {{ tm('dialogs.details.parentGroup') }} + {{ command.parent_signature }} + + + {{ tm('dialogs.details.aliases') }} + + + {{ alias }} + + + + + {{ tm('dialogs.details.subCommands') }} + + + + {{ sub.current_fragment }} + + + + + + {{ tm('dialogs.details.permission') }} + + + {{ getPermissionLabel(command.permission) }} + + + + + {{ tm('dialogs.details.conflictStatus') }} + + {{ tm('status.conflict') }} + + + + + + + + {{ t('core.actions.close') }} + + + + + + + diff --git a/dashboard/src/views/command/components/RenameDialog.vue b/dashboard/src/views/command/components/RenameDialog.vue new file mode 100644 index 000000000..ffdc5a826 --- /dev/null +++ b/dashboard/src/views/command/components/RenameDialog.vue @@ -0,0 +1,53 @@ + + + + + + {{ tm('dialogs.rename.title') }} + + + + + + + {{ tm('dialogs.rename.cancel') }} + + + {{ tm('dialogs.rename.confirm') }} + + + + + diff --git a/dashboard/src/views/command/composables/useCommandActions.ts b/dashboard/src/views/command/composables/useCommandActions.ts new file mode 100644 index 000000000..a285c473f --- /dev/null +++ b/dashboard/src/views/command/composables/useCommandActions.ts @@ -0,0 +1,177 @@ +/** + * 指令操作方法 Composable + */ +import { reactive } from 'vue'; +import axios from 'axios'; +import type { CommandItem, RenameDialogState, DetailsDialogState, TypeInfo, StatusInfo } from '../types'; + +export function useCommandActions( + toast: (message: string, color?: string) => void, + fetchCommands: () => Promise +) { + // 重命名对话框状态 + const renameDialog = reactive({ + show: false, + command: null, + newName: '', + loading: false + }); + + // 详情对话框状态 + const detailsDialog = reactive({ + show: false, + command: null + }); + + /** + * 切换指令启用/禁用状态 + */ + const toggleCommand = async ( + cmd: CommandItem, + successMessage: string, + errorMessage: string + ) => { + try { + const res = await axios.post('/api/commands/toggle', { + handler_full_name: cmd.handler_full_name, + enabled: !cmd.enabled + }); + if (res.data.status === 'ok') { + toast(successMessage, 'success'); + await fetchCommands(); + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } + }; + + /** + * 打开重命名对话框 + */ + const openRenameDialog = (cmd: CommandItem) => { + renameDialog.command = cmd; + renameDialog.newName = cmd.current_fragment || ''; + renameDialog.show = true; + }; + + /** + * 确认重命名 + */ + const confirmRename = async (successMessage: string, errorMessage: string) => { + if (!renameDialog.command || !renameDialog.newName.trim()) return; + + renameDialog.loading = true; + try { + const res = await axios.post('/api/commands/rename', { + handler_full_name: renameDialog.command.handler_full_name, + new_name: renameDialog.newName.trim() + }); + if (res.data.status === 'ok') { + toast(successMessage, 'success'); + renameDialog.show = false; + await fetchCommands(); + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } finally { + renameDialog.loading = false; + } + }; + + /** + * 打开详情对话框 + */ + const openDetailsDialog = (cmd: CommandItem) => { + detailsDialog.command = cmd; + detailsDialog.show = true; + }; + + /** + * 获取类型显示信息 + */ + const getTypeInfo = (type: string, translations: { group: string; subCommand: string; command: string }): TypeInfo => { + switch (type) { + case 'group': + return { text: translations.group, color: 'info', icon: 'mdi-folder-outline' }; + case 'sub_command': + return { text: translations.subCommand, color: 'secondary', icon: 'mdi-subdirectory-arrow-right' }; + default: + return { text: translations.command, color: 'primary', icon: 'mdi-console-line' }; + } + }; + + /** + * 获取权限颜色 + */ + const getPermissionColor = (permission: string): string => { + switch (permission) { + case 'admin': return 'error'; + default: return 'success'; + } + }; + + /** + * 获取权限标签 + */ + const getPermissionLabel = (permission: string, translations: { admin: string; everyone: string }): string => { + switch (permission) { + case 'admin': return translations.admin; + default: return translations.everyone; + } + }; + + /** + * 获取状态显示信息 + */ + const getStatusInfo = ( + cmd: CommandItem, + translations: { conflict: string; enabled: string; disabled: string } + ): StatusInfo => { + if (cmd.has_conflict) { + return { text: translations.conflict, color: 'warning', variant: 'flat' }; + } + if (cmd.enabled) { + return { text: translations.enabled, color: 'success', variant: 'flat' }; + } + return { text: translations.disabled, color: 'error', variant: 'outlined' }; + }; + + /** + * 获取表格行属性(用于冲突高亮和子指令样式) + */ + const getRowProps = ({ item }: { item: CommandItem }) => { + const classes: string[] = []; + if (item.has_conflict) { + classes.push('conflict-row'); + } + if (item.type === 'sub_command') { + classes.push('sub-command-row'); + } + if (item.is_group) { + classes.push('group-row'); + } + return classes.length > 0 ? { class: classes.join(' ') } : {}; + }; + + return { + // 状态 + renameDialog, + detailsDialog, + + // 方法 + toggleCommand, + openRenameDialog, + confirmRename, + openDetailsDialog, + getTypeInfo, + getPermissionColor, + getPermissionLabel, + getStatusInfo, + getRowProps + }; +} + diff --git a/dashboard/src/views/command/composables/useCommandData.ts b/dashboard/src/views/command/composables/useCommandData.ts new file mode 100644 index 000000000..73c33f59e --- /dev/null +++ b/dashboard/src/views/command/composables/useCommandData.ts @@ -0,0 +1,62 @@ +/** + * 指令数据管理 Composable + */ +import { ref, reactive } from 'vue'; +import axios from 'axios'; +import type { CommandItem, CommandSummary, SnackbarState } from '../types'; + +export function useCommandData() { + const loading = ref(false); + const commands = ref([]); + const summary = reactive({ + disabled: 0, + conflicts: 0 + }); + + const snackbar = reactive({ + show: false, + message: '', + color: 'success' + }); + + /** + * 显示 Toast 消息 + */ + const toast = (message: string, color: string = 'success') => { + snackbar.message = message; + snackbar.color = color; + snackbar.show = true; + }; + + /** + * 获取指令列表 + */ + const fetchCommands = async (errorMessage: string) => { + loading.value = true; + try { + const res = await axios.get('/api/commands'); + if (res.data.status === 'ok') { + commands.value = res.data.data.items || []; + const s = res.data.data.summary || {}; + summary.disabled = s.disabled || 0; + summary.conflicts = s.conflicts || 0; + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } finally { + loading.value = false; + } + }; + + return { + loading, + commands, + summary, + snackbar, + toast, + fetchCommands + }; +} + diff --git a/dashboard/src/views/command/composables/useCommandFilters.ts b/dashboard/src/views/command/composables/useCommandFilters.ts new file mode 100644 index 000000000..02e954c2c --- /dev/null +++ b/dashboard/src/views/command/composables/useCommandFilters.ts @@ -0,0 +1,197 @@ +/** + * 指令过滤逻辑 Composable + */ +import { ref, computed, type Ref } from 'vue'; +import type { CommandItem, FilterState } from '../types'; + +export function useCommandFilters(commands: Ref) { + // 过滤状态 + const searchQuery = ref(''); + const pluginFilter = ref('all'); + const permissionFilter = ref('all'); + const statusFilter = ref('all'); + const typeFilter = ref('all'); + const showSystemPlugins = ref(false); + + // 展开的指令组 + const expandedGroups = ref>(new Set()); + + /** + * 检查是否有涉及系统插件的冲突 + */ + const hasSystemPluginConflict = computed(() => { + return commands.value.some(cmd => cmd.has_conflict && cmd.reserved); + }); + + /** + * 实际是否显示系统插件(如果有系统插件冲突则强制显示) + */ + const effectiveShowSystemPlugins = computed(() => { + return showSystemPlugins.value || hasSystemPluginConflict.value; + }); + + /** + * 获取可用的插件列表(用于过滤下拉框) + */ + const availablePlugins = computed(() => { + const plugins = new Set( + commands.value + .filter(cmd => effectiveShowSystemPlugins.value || !cmd.reserved) + .map(cmd => cmd.plugin) + ); + return Array.from(plugins).sort(); + }); + + /** + * 检查指令是否匹配过滤条件 + */ + const matchesFilters = (cmd: CommandItem, query: string): boolean => { + // 系统插件过滤(除非显示系统插件) + if (!effectiveShowSystemPlugins.value && cmd.reserved) { + return false; + } + + // 搜索过滤 + if (query) { + const matchesSearch = + cmd.effective_command?.toLowerCase().includes(query) || + cmd.description?.toLowerCase().includes(query) || + cmd.plugin?.toLowerCase().includes(query); + if (!matchesSearch) return false; + } + + // 插件过滤 + if (pluginFilter.value !== 'all' && cmd.plugin !== pluginFilter.value) { + return false; + } + + // 权限过滤 + if (permissionFilter.value !== 'all') { + if (permissionFilter.value === 'everyone') { + if (cmd.permission !== 'everyone' && cmd.permission !== 'member') return false; + } else if (cmd.permission !== permissionFilter.value) { + return false; + } + } + + // 状态过滤 + if (statusFilter.value !== 'all') { + if (statusFilter.value === 'enabled' && !cmd.enabled) return false; + if (statusFilter.value === 'disabled' && cmd.enabled) return false; + if (statusFilter.value === 'conflict' && !cmd.has_conflict) return false; + } + + // 类型过滤 + if (typeFilter.value !== 'all') { + if (typeFilter.value === 'group' && cmd.type !== 'group') return false; + if (typeFilter.value === 'command' && cmd.type !== 'command') return false; + if (typeFilter.value === 'sub_command' && cmd.type !== 'sub_command') return false; + } + + return true; + }; + + /** + * 过滤后的指令列表(支持层级结构) + */ + const filteredCommands = computed(() => { + const query = searchQuery.value.toLowerCase(); + const conflictCmds: CommandItem[] = []; + const normalCmds: CommandItem[] = []; + + for (const cmd of commands.value) { + // 对于指令组,检查组本身或子指令是否匹配 + if (cmd.is_group) { + const groupMatches = matchesFilters(cmd, query); + const matchingSubCmds = (cmd.sub_commands || []).filter(sub => matchesFilters(sub, query)); + + // 如果组匹配或有匹配的子指令,则包含它 + if (groupMatches || matchingSubCmds.length > 0) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } + + // 如果组已展开,添加匹配的子指令 + if (expandedGroups.value.has(cmd.handler_full_name)) { + const subsToShow = query ? matchingSubCmds : (cmd.sub_commands || []); + for (const sub of subsToShow) { + if (sub.has_conflict) { + conflictCmds.push(sub); + } else { + normalCmds.push(sub); + } + } + } + } + } else if (cmd.type !== 'sub_command') { + // 普通指令(子指令通过组处理) + if (matchesFilters(cmd, query)) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } + } + } + } + + // 按 effective_command 排序冲突指令,使其分组在一起 + conflictCmds.sort((a, b) => (a.effective_command || '').localeCompare(b.effective_command || '')); + + return [...conflictCmds, ...normalCmds]; + }); + + /** + * 切换指令组的展开/折叠状态 + */ + const toggleGroupExpand = (cmd: CommandItem) => { + if (!cmd.is_group) return; + if (expandedGroups.value.has(cmd.handler_full_name)) { + expandedGroups.value.delete(cmd.handler_full_name); + } else { + expandedGroups.value.add(cmd.handler_full_name); + } + }; + + /** + * 检查指令组是否已展开 + */ + const isGroupExpanded = (cmd: CommandItem): boolean => { + return expandedGroups.value.has(cmd.handler_full_name); + }; + + // 导出过滤状态 + const filterState: FilterState = { + searchQuery: searchQuery.value, + pluginFilter: pluginFilter.value, + permissionFilter: permissionFilter.value, + statusFilter: statusFilter.value, + typeFilter: typeFilter.value, + showSystemPlugins: showSystemPlugins.value + }; + + return { + // 状态 + searchQuery, + pluginFilter, + permissionFilter, + statusFilter, + typeFilter, + showSystemPlugins, + expandedGroups, + + // 计算属性 + hasSystemPluginConflict, + effectiveShowSystemPlugins, + availablePlugins, + filteredCommands, + + // 方法 + matchesFilters, + toggleGroupExpand, + isGroupExpanded + }; +} + diff --git a/dashboard/src/views/command/index.vue b/dashboard/src/views/command/index.vue new file mode 100644 index 000000000..aee899261 --- /dev/null +++ b/dashboard/src/views/command/index.vue @@ -0,0 +1,184 @@ + + + + + + + + + + + + mdi-console-line + {{ tm('summary.total') }}: + {{ filteredCommands.length }} + + + + mdi-close-circle-outline + {{ tm('summary.disabled') }}: + {{ summary.disabled }} + + + + + + + + mdi-alert-circle + + + {{ tm('conflictAlert.title') }} + + + {{ tm('conflictAlert.description', { count: summary.conflicts }) }} + + + mdi-lightbulb-outline + {{ tm('conflictAlert.hint') }} + + + + + + + + + + + + + + + + + + + {{ snackbar.message }} + + diff --git a/dashboard/src/views/command/types.ts b/dashboard/src/views/command/types.ts new file mode 100644 index 000000000..cf5f012cf --- /dev/null +++ b/dashboard/src/views/command/types.ts @@ -0,0 +1,84 @@ +/** + * 指令管理模块 - 类型定义 + */ + +/** 指令项接口 */ +export interface CommandItem { + handler_full_name: string; + handler_name: string; + plugin: string; + plugin_display_name: string | null; + module_path: string; + description: string; + type: CommandType; + parent_signature: string; + parent_group_handler: string; + original_command: string; + current_fragment: string; + effective_command: string; + aliases: string[]; + permission: PermissionType; + enabled: boolean; + is_group: boolean; + has_conflict: boolean; + reserved: boolean; + sub_commands: CommandItem[]; +} + +/** 指令类型 */ +export type CommandType = 'command' | 'group' | 'sub_command'; + +/** 权限类型 */ +export type PermissionType = 'admin' | 'everyone' | 'member'; + +/** 指令摘要统计 */ +export interface CommandSummary { + disabled: number; + conflicts: number; +} + +/** 过滤器状态 */ +export interface FilterState { + searchQuery: string; + pluginFilter: string; + permissionFilter: string; + statusFilter: string; + typeFilter: string; + showSystemPlugins: boolean; +} + +/** 重命名对话框状态 */ +export interface RenameDialogState { + show: boolean; + command: CommandItem | null; + newName: string; + loading: boolean; +} + +/** 详情对话框状态 */ +export interface DetailsDialogState { + show: boolean; + command: CommandItem | null; +} + +/** Toast 消息状态 */ +export interface SnackbarState { + show: boolean; + message: string; + color: string; +} + +/** 类型信息展示 */ +export interface TypeInfo { + text: string; + color: string; + icon: string; +} + +/** 状态信息展示 */ +export interface StatusInfo { + text: string; + color: string; + variant: 'flat' | 'outlined' | 'text' | 'elevated' | 'tonal' | 'plain'; +} +
{{ item.effective_command }}
{{ command.handler_name }}
{{ command.module_path }}
{{ command.original_command }}
{{ command.effective_command }}
{{ command.parent_signature }}