| <script setup lang="ts"> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { computed, onActivated, onMounted, ref, watch} from 'vue'; |
| import axios from 'axios'; |
| import { useModuleI18n } from '@/i18n/composables'; |
| import { normalizeTextInput } from '@/utils/inputValue'; |
| |
| |
| import { useComponentData } from './composables/useComponentData'; |
| import { useCommandFilters } from './composables/useCommandFilters'; |
| import { useCommandActions } from './composables/useCommandActions'; |
| |
| |
| import CommandFilters from './components/CommandFilters.vue'; |
| import CommandTable from './components/CommandTable.vue'; |
| import ToolTable from './components/ToolTable.vue'; |
| import RenameDialog from './components/RenameDialog.vue'; |
| import DetailsDialog from './components/DetailsDialog.vue'; |
| |
| |
| import type { CommandItem, ToolItem } from './types'; |
| |
| defineOptions({ name: 'ComponentPanel' }); |
| const props = withDefaults(defineProps<{ active?: boolean }>(), { |
| active: true |
| }); |
| |
| const { tm } = useModuleI18n('features/command'); |
| const { tm: tmTool } = useModuleI18n('features/tooluse'); |
| |
| const viewMode = ref<'commands' | 'tools'>('commands'); |
| const toolSearch = ref(''); |
| |
| |
| const { |
| loading, |
| commands, |
| tools, |
| toolsLoading, |
| summary, |
| snackbar, |
| toast, |
| fetchCommands, |
| fetchTools |
| } = useComponentData(); |
| |
| |
| const { |
| searchQuery, |
| pluginFilter, |
| permissionFilter, |
| statusFilter, |
| typeFilter, |
| showSystemPlugins, |
| expandedGroups, |
| hasSystemPluginConflict, |
| effectiveShowSystemPlugins, |
| availablePlugins, |
| filteredCommands, |
| toggleGroupExpand |
| } = useCommandFilters(commands); |
| |
| |
| const { |
| renameDialog, |
| detailsDialog, |
| toggleCommand, |
| updatePermission, |
| openRenameDialog, |
| confirmRename, |
| openDetailsDialog |
| } = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed'))); |
| |
| const filteredTools = computed(() => { |
| const query = normalizeTextInput(toolSearch.value).trim().toLowerCase(); |
| if (!query) return tools.value; |
| return tools.value.filter(tool => |
| tool.name?.toLowerCase().includes(query) || |
| tool.description?.toLowerCase().includes(query) |
| ); |
| }); |
| |
| |
| const handleToggleCommand = async (cmd: CommandItem) => { |
| await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed')); |
| }; |
| |
| const handleUpdatePermission = async (cmd: CommandItem, permission: 'admin' | 'member') => { |
| await updatePermission(cmd, permission, tm('messages.updateSuccess'), tm('messages.updateFailed')); |
| }; |
| |
| const handleToggleTool = async (tool: ToolItem) => { |
| const previous = tool.active; |
| tool.active = !tool.active; |
| try { |
| const res = await axios.post('/api/tools/toggle-tool', { |
| name: tool.name, |
| activate: tool.active |
| }); |
| if (res.data.status === 'ok') { |
| toast(res.data.message || tmTool('messages.toggleToolSuccess')); |
| } else { |
| tool.active = previous; |
| toast(res.data.message || tmTool('messages.toggleToolError', { error: '' }), 'error'); |
| } |
| } catch (error: any) { |
| tool.active = previous; |
| toast(error?.response?.data?.message || error?.message || tmTool('messages.toggleToolError', { error: '' }), 'error'); |
| } |
| }; |
| |
| |
| const handleConfirmRename = async () => { |
| await confirmRename(tm('messages.renameSuccess'), tm('messages.renameFailed')); |
| }; |
| |
| |
| onMounted(async () => { |
| await Promise.all([ |
| fetchCommands(tm('messages.loadFailed')), |
| fetchTools(tmTool('messages.getToolsError', { error: '' })) |
| ]); |
| }); |
| |
| watch(() => props.active, async (isActive) => { |
| if (!isActive) return; |
| if (viewMode.value === 'commands') { |
| await fetchCommands(tm('messages.loadFailed')); |
| } else { |
| await fetchTools(tmTool('messages.getToolsError', { error: '' })); |
| } |
| }); |
| |
| watch(viewMode, async (mode) => { |
| if (mode === 'commands') { |
| await fetchCommands(tm('messages.loadFailed')); |
| } else { |
| await fetchTools(tmTool('messages.getToolsError', { error: '' })); |
| } |
| }); |
| </script> |
| |
| <template> |
| <v-row> |
| <v-col cols="12"> |
| <v-card variant="flat" style="background-color: transparent"> |
| <v-card-text style="padding: 20px 12px; padding-top: 0px;"> |
| <div class="d-flex justify-space-between align-center mb-6 flex-wrap ga-3"> |
| <v-btn-toggle v-model="viewMode" color="primary" variant="outlined" density="comfortable" mandatory> |
| <v-btn value="commands"> |
| <v-icon size="18" class="mr-1">mdi-console-line</v-icon> |
| {{ tm('type.command') }} |
| </v-btn> |
| <v-btn value="tools"> |
| <v-icon size="18" class="mr-1">mdi-function-variant</v-icon> |
| {{ tmTool('functionTools.title') }} |
| </v-btn> |
| </v-btn-toggle> |
| <v-progress-linear |
| v-if="viewMode === 'commands' && loading" |
| indeterminate |
| color="primary" |
| style="max-width: 220px; flex: 1;" |
| /> |
| <v-progress-linear |
| v-else-if="viewMode === 'tools' && toolsLoading" |
| indeterminate |
| color="primary" |
| style="max-width: 220px; flex: 1;" |
| /> |
| </div> |
| |
| <div v-if="viewMode === 'commands'"> |
| <CommandFilters |
| :plugin-filter="pluginFilter" |
| @update:plugin-filter="pluginFilter = $event" |
| :type-filter="typeFilter" |
| @update:type-filter="typeFilter = $event" |
| :permission-filter="permissionFilter" |
| @update:permission-filter="permissionFilter = $event" |
| :status-filter="statusFilter" |
| @update:status-filter="statusFilter = $event" |
| :show-system-plugins="showSystemPlugins" |
| @update:show-system-plugins="showSystemPlugins = $event" |
| :search-query="searchQuery" |
| @update:search-query="searchQuery = $event" |
| :available-plugins="availablePlugins" |
| :has-system-plugin-conflict="hasSystemPluginConflict" |
| :effective-show-system-plugins="effectiveShowSystemPlugins" |
| > |
| <template #stats> |
| <div class="d-flex align-center"> |
| <v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon> |
| <span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span> |
| <span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span> |
| </div> |
| <v-divider vertical class="mx-1" style="height: 20px;" /> |
| <div class="d-flex align-center"> |
| <v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon> |
| <span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span> |
| <span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span> |
| </div> |
| </template> |
| </CommandFilters> |
| |
| <v-alert |
| v-if="summary.conflicts > 0" |
| type="error" |
| variant="tonal" |
| class="mb-4" |
| prominent |
| border="start" |
| > |
| <template v-slot:prepend> |
| <v-icon size="28">mdi-alert-circle</v-icon> |
| </template> |
| <v-alert-title class="text-subtitle-1 font-weight-bold"> |
| {{ tm('conflictAlert.title') }} |
| </v-alert-title> |
| <div class="text-body-2 mt-1"> |
| {{ tm('conflictAlert.description', { count: summary.conflicts }) }} |
| </div> |
| <div class="text-body-2 mt-2"> |
| <v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon> |
| {{ tm('conflictAlert.hint') }} |
| </div> |
| </v-alert> |
| |
| <CommandTable |
| :items="filteredCommands" |
| :expanded-groups="expandedGroups" |
| :loading="loading" |
| @toggle-expand="toggleGroupExpand" |
| @toggle-command="handleToggleCommand" |
| @rename="openRenameDialog" |
| @view-details="openDetailsDialog" |
| @update-permission="handleUpdatePermission" |
| /> |
| </div> |
| |
| <div v-else> |
| <div class="d-flex flex-wrap align-center ga-3 mb-4"> |
| <div style="min-width: 240px; max-width: 380px; flex: 1;"> |
| <v-text-field |
| :model-value="toolSearch" |
| @update:model-value="toolSearch = normalizeTextInput($event)" |
| prepend-inner-icon="mdi-magnify" |
| :label="tmTool('functionTools.search')" |
| variant="outlined" |
| density="compact" |
| hide-details |
| clearable |
| /> |
| </div> |
| <div class="d-flex align-center ga-2"> |
| <div class="d-flex align-center"> |
| <v-icon size="18" color="primary" class="mr-1">mdi-function-variant</v-icon> |
| <span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span> |
| <span class="text-body-1 font-weight-bold text-primary">{{ filteredTools.length }}</span> |
| </div> |
| <v-divider vertical class="mx-1" style="height: 20px;" /> |
| <div class="d-flex align-center"> |
| <v-icon size="18" color="success" class="mr-1">mdi-check-circle-outline</v-icon> |
| <span class="text-body-2 text-medium-emphasis mr-1">{{ tm('status.enabled') }}:</span> |
| <span class="text-body-1 font-weight-bold text-success">{{ filteredTools.filter(t => t.active).length }}</span> |
| </div> |
| </div> |
| </div> |
| |
| <ToolTable |
| :items="filteredTools" |
| :loading="toolsLoading" |
| @toggle-tool="handleToggleTool" |
| /> |
| </div> |
| </v-card-text> |
| </v-card> |
| </v-col> |
| </v-row> |
| |
| |
| <RenameDialog |
| :show="renameDialog.show" |
| @update:show="renameDialog.show = $event" |
| :new-name="renameDialog.newName" |
| @update:new-name="renameDialog.newName = $event" |
| :aliases="renameDialog.aliases" |
| @update:aliases="renameDialog.aliases = $event" |
| :command="renameDialog.command" |
| :loading="renameDialog.loading" |
| @confirm="handleConfirmRename" |
| /> |
| |
| |
| <DetailsDialog |
| :show="detailsDialog.show" |
| @update:show="detailsDialog.show = $event" |
| :command="detailsDialog.command" |
| /> |
| |
| |
| <v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show"> |
| {{ snackbar.message }} |
| </v-snackbar> |
| </template> |
| |