refactor(command): 模块化指令管理面板前端代码
This commit is contained in:
@@ -19,7 +19,7 @@ const MainRoutes = {
|
||||
{
|
||||
name: 'Commands',
|
||||
path: '/commands',
|
||||
component: () => import('@/views/CommandPage.vue')
|
||||
component: () => import('@/views/command/index.vue')
|
||||
},
|
||||
{
|
||||
name: 'ExtensionMarketplace',
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
availablePlugins: string[];
|
||||
hasSystemPluginConflict: boolean;
|
||||
effectiveShowSystemPlugins: boolean;
|
||||
pluginFilter: string;
|
||||
typeFilter: string;
|
||||
permissionFilter: string;
|
||||
statusFilter: string;
|
||||
showSystemPlugins: boolean;
|
||||
searchQuery: string;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:pluginFilter', value: string): void;
|
||||
(e: 'update:typeFilter', value: string): void;
|
||||
(e: 'update:permissionFilter', value: string): void;
|
||||
(e: 'update:statusFilter', value: string): void;
|
||||
(e: 'update:showSystemPlugins', value: boolean): void;
|
||||
(e: 'update:searchQuery', value: string): void;
|
||||
}>();
|
||||
|
||||
// Computed items for selects
|
||||
const pluginItems = computed(() => [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
...props.availablePlugins.map(p => ({ title: p, value: p }))
|
||||
]);
|
||||
|
||||
const typeItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('type.group'), value: 'group' },
|
||||
{ title: tm('type.command'), value: 'command' },
|
||||
{ title: tm('type.subCommand'), value: 'sub_command' }
|
||||
];
|
||||
|
||||
const permissionItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('permission.everyone'), value: 'everyone' },
|
||||
{ title: tm('permission.admin'), value: 'admin' }
|
||||
];
|
||||
|
||||
const statusItems = [
|
||||
{ title: tm('filters.all'), value: 'all' },
|
||||
{ title: tm('filters.enabled'), value: 'enabled' },
|
||||
{ title: tm('filters.disabled'), value: 'disabled' },
|
||||
{ title: tm('filters.conflict'), value: 'conflict' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 过滤器行 -->
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
:model-value="pluginFilter"
|
||||
@update:model-value="emit('update:pluginFilter', $event)"
|
||||
:items="pluginItems"
|
||||
:label="tm('filters.byPlugin')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="typeFilter"
|
||||
@update:model-value="emit('update:typeFilter', $event)"
|
||||
:items="typeItems"
|
||||
:label="tm('filters.byType')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="permissionFilter"
|
||||
@update:model-value="emit('update:permissionFilter', $event)"
|
||||
:items="permissionItems"
|
||||
:label="tm('filters.byPermission')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
:model-value="statusFilter"
|
||||
@update:model-value="emit('update:statusFilter', $event)"
|
||||
:items="statusItems"
|
||||
:label="tm('filters.byStatus')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 搜索栏 + 统计信息行 -->
|
||||
<div class="mb-4 d-flex flex-wrap align-center ga-4">
|
||||
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
|
||||
<v-text-field
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="emit('update:searchQuery', $event)"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-4">
|
||||
<slot name="stats"></slot>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<v-checkbox
|
||||
:model-value="effectiveShowSystemPlugins"
|
||||
@update:model-value="emit('update:showSystemPlugins', !!$event)"
|
||||
:label="tm('filters.showSystemPlugins')"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="hasSystemPluginConflict"
|
||||
class="system-plugin-checkbox"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<span class="text-body-2">{{ tm('filters.showSystemPlugins') }}</span>
|
||||
<v-tooltip v-if="hasSystemPluginConflict" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" size="16" color="warning" class="ml-1">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
{{ tm('filters.systemPluginConflictHint') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-plugin-checkbox {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.system-plugin-checkbox :deep(.v-selection-control) {
|
||||
min-height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem, TypeInfo, StatusInfo } from '../types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
items: CommandItem[];
|
||||
expandedGroups: Set<string>;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-expand', cmd: CommandItem): void;
|
||||
(e: 'toggle-command', cmd: CommandItem): void;
|
||||
(e: 'rename', cmd: CommandItem): void;
|
||||
(e: 'view-details', cmd: CommandItem): void;
|
||||
}>();
|
||||
|
||||
// 表格表头
|
||||
const commandHeaders = computed(() => [
|
||||
{ title: tm('table.headers.command'), key: 'effective_command', minWidth: '100px' },
|
||||
{ title: tm('table.headers.type'), key: 'type', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.plugin'), key: 'plugin', width: '140px' },
|
||||
{ title: tm('table.headers.description'), key: 'description', sortable: false },
|
||||
{ title: tm('table.headers.permission'), key: 'permission', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '100px' },
|
||||
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '140px' }
|
||||
]);
|
||||
|
||||
// 检查组是否展开
|
||||
const isGroupExpanded = (cmd: CommandItem): boolean => {
|
||||
return props.expandedGroups.has(cmd.handler_full_name);
|
||||
};
|
||||
|
||||
// 获取类型信息
|
||||
const getTypeInfo = (type: string): TypeInfo => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return { text: tm('type.group'), color: 'info', icon: 'mdi-folder-outline' };
|
||||
case 'sub_command':
|
||||
return { text: tm('type.subCommand'), color: 'secondary', icon: 'mdi-subdirectory-arrow-right' };
|
||||
default:
|
||||
return { text: tm('type.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): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return tm('permission.admin');
|
||||
default: return tm('permission.everyone');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = (cmd: CommandItem): StatusInfo => {
|
||||
if (cmd.has_conflict) {
|
||||
return { text: tm('status.conflict'), color: 'warning', variant: 'flat' };
|
||||
}
|
||||
if (cmd.enabled) {
|
||||
return { text: tm('status.enabled'), color: 'success', variant: 'flat' };
|
||||
}
|
||||
return { text: tm('status.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(' ') } : {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<v-data-table
|
||||
:headers="commandHeaders"
|
||||
:items="items"
|
||||
item-key="handler_full_name"
|
||||
hover
|
||||
:row-props="getRowProps"
|
||||
>
|
||||
<template v-slot:item.effective_command="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<!-- 展开/折叠按钮(针对指令组) -->
|
||||
<v-btn
|
||||
v-if="item.is_group && item.sub_commands?.length > 0"
|
||||
icon
|
||||
variant="text"
|
||||
size="x-small"
|
||||
class="mr-1"
|
||||
@click.stop="emit('toggle-expand', item)"
|
||||
>
|
||||
<v-icon size="18">{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
<!-- 子指令缩进 -->
|
||||
<div v-else-if="item.type === 'sub_command'" class="ml-6"></div>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
<code :class="{ 'sub-command-code': item.type === 'sub_command' }">{{ item.effective_command }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeInfo(item.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(item.type).icon }}</v-icon>
|
||||
{{ getTypeInfo(item.type).text }}{{ item.is_group && item.sub_commands?.length > 0 ? `(${item.sub_commands.length})` : '' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.plugin="{ item }">
|
||||
<div class="text-body-2">{{ item.plugin_display_name || item.plugin }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.permission="{ item }">
|
||||
<v-chip :color="getPermissionColor(item.permission)" size="small" class="font-weight-medium">
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.enabled="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusInfo(item).color"
|
||||
size="small"
|
||||
class="font-weight-medium"
|
||||
:variant="getStatusInfo(item).variant"
|
||||
>
|
||||
{{ getStatusInfo(item).text }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="default" variant="text" color="primary">
|
||||
<v-btn
|
||||
v-if="!item.enabled"
|
||||
icon
|
||||
size="small"
|
||||
color="success"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
|
||||
<v-icon size="22">mdi-pencil</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" @click="emit('view-details', item)">
|
||||
<v-icon size="22">mdi-information</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewDetails') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-console-line</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tm('empty.noCommands') }}</div>
|
||||
<div class="text-body-1 mb-4">{{ tm('empty.noCommandsDesc') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
code.sub-command-code {
|
||||
background-color: rgba(var(--v-theme-secondary), 0.1);
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 冲突行高亮 */
|
||||
.v-data-table .conflict-row {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.15) 0%, rgba(var(--v-theme-warning), 0.05) 100%) !important;
|
||||
border-left: 3px solid rgb(var(--v-theme-warning)) !important;
|
||||
}
|
||||
|
||||
.v-data-table .conflict-row:hover {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.25) 0%, rgba(var(--v-theme-warning), 0.1) 100%) !important;
|
||||
}
|
||||
|
||||
/* 指令组行样式 */
|
||||
.v-data-table .group-row {
|
||||
background-color: rgba(var(--v-theme-info), 0.05);
|
||||
}
|
||||
|
||||
.v-data-table .group-row:hover {
|
||||
background-color: rgba(var(--v-theme-info), 0.08) !important;
|
||||
}
|
||||
|
||||
/* 子指令行样式 */
|
||||
.v-data-table .sub-command-row {
|
||||
background-color: rgba(var(--v-theme-info), 0.05);
|
||||
}
|
||||
|
||||
.v-data-table .sub-command-row:hover {
|
||||
background-color: rgba(var(--v-theme-info), 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem, TypeInfo } from '../types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void;
|
||||
}>();
|
||||
|
||||
// 获取类型信息
|
||||
const getTypeInfo = (type: string): TypeInfo => {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return { text: tm('type.group'), color: 'info', icon: 'mdi-folder-outline' };
|
||||
case 'sub_command':
|
||||
return { text: tm('type.subCommand'), color: 'secondary', icon: 'mdi-subdirectory-arrow-right' };
|
||||
default:
|
||||
return { text: tm('type.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): string => {
|
||||
switch (permission) {
|
||||
case 'admin': return tm('permission.admin');
|
||||
default: return tm('permission.everyone');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-card v-if="command">
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.details.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.type') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="getTypeInfo(command.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(command.type).icon }}</v-icon>
|
||||
{{ getTypeInfo(command.type).text }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.handler') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.handler_name }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.module') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.module_path }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.originalCommand') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.original_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.effectiveCommand') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.effective_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.parent_signature">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.parentGroup') }}</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.parent_signature }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.aliases.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.aliases') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip v-for="alias in command.aliases" :key="alias" size="small" class="mr-1">
|
||||
{{ alias }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.is_group && command.sub_commands?.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.subCommands') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap ga-1 mt-1">
|
||||
<v-chip
|
||||
v-for="sub in command.sub_commands"
|
||||
:key="sub.handler_full_name"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ sub.current_fragment }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.permission') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip :color="getPermissionColor(command.permission)" size="small">
|
||||
{{ getPermissionLabel(command.permission) }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.has_conflict">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.conflictStatus') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip color="warning" size="small">{{ tm('status.conflict') }}</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="text" @click="emit('update:show', false)">
|
||||
{{ t('core.actions.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { CommandItem } from '../types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// Props
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
command: CommandItem | null;
|
||||
newName: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void;
|
||||
(e: 'update:newName', value: string): void;
|
||||
(e: 'confirm'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.rename.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
:model-value="newName"
|
||||
@update:model-value="emit('update:newName', $event)"
|
||||
:label="tm('dialogs.rename.newName')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
autofocus
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="emit('update:show', false)">
|
||||
{{ tm('dialogs.rename.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="loading"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
{{ tm('dialogs.rename.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -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<void>
|
||||
) {
|
||||
// 重命名对话框状态
|
||||
const renameDialog = reactive<RenameDialogState>({
|
||||
show: false,
|
||||
command: null,
|
||||
newName: '',
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 详情对话框状态
|
||||
const detailsDialog = reactive<DetailsDialogState>({
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<CommandItem[]>([]);
|
||||
const summary = reactive<CommandSummary>({
|
||||
disabled: 0,
|
||||
conflicts: 0
|
||||
});
|
||||
|
||||
const snackbar = reactive<SnackbarState>({
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 指令过滤逻辑 Composable
|
||||
*/
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import type { CommandItem, FilterState } from '../types';
|
||||
|
||||
export function useCommandFilters(commands: Ref<CommandItem[]>) {
|
||||
// 过滤状态
|
||||
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<Set<string>>(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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 指令管理页面 - 主入口
|
||||
*
|
||||
* 模块化结构:
|
||||
* - types.ts: 类型定义
|
||||
* - composables/useCommandData.ts: 数据获取和状态管理
|
||||
* - composables/useCommandFilters.ts: 过滤逻辑
|
||||
* - composables/useCommandActions.ts: 操作方法
|
||||
* - components/CommandFilters.vue: 过滤器组件
|
||||
* - components/CommandTable.vue: 表格组件
|
||||
* - components/RenameDialog.vue: 重命名对话框
|
||||
* - components/DetailsDialog.vue: 详情对话框
|
||||
*/
|
||||
import { onMounted } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
// Composables
|
||||
import { useCommandData } from './composables/useCommandData';
|
||||
import { useCommandFilters } from './composables/useCommandFilters';
|
||||
import { useCommandActions } from './composables/useCommandActions';
|
||||
|
||||
// Components
|
||||
import CommandFilters from './components/CommandFilters.vue';
|
||||
import CommandTable from './components/CommandTable.vue';
|
||||
import RenameDialog from './components/RenameDialog.vue';
|
||||
import DetailsDialog from './components/DetailsDialog.vue';
|
||||
|
||||
// Types
|
||||
import type { CommandItem } from './types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// 数据管理
|
||||
const {
|
||||
loading,
|
||||
commands,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands
|
||||
} = useCommandData();
|
||||
|
||||
// 过滤逻辑
|
||||
const {
|
||||
searchQuery,
|
||||
pluginFilter,
|
||||
permissionFilter,
|
||||
statusFilter,
|
||||
typeFilter,
|
||||
showSystemPlugins,
|
||||
expandedGroups,
|
||||
hasSystemPluginConflict,
|
||||
effectiveShowSystemPlugins,
|
||||
availablePlugins,
|
||||
filteredCommands,
|
||||
toggleGroupExpand
|
||||
} = useCommandFilters(commands);
|
||||
|
||||
// 操作方法
|
||||
const {
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
toggleCommand,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog
|
||||
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
|
||||
|
||||
// 处理切换指令状态
|
||||
const handleToggleCommand = async (cmd: CommandItem) => {
|
||||
await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed'));
|
||||
};
|
||||
|
||||
// 处理确认重命名
|
||||
const handleConfirmRename = async () => {
|
||||
await confirmRename(tm('messages.renameSuccess'), tm('messages.renameFailed'));
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card variant="flat" style="background-color: transparent">
|
||||
<v-card-text style="padding: 0px 12px;">
|
||||
<!-- 过滤器组件 -->
|
||||
<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"
|
||||
@toggle-expand="toggleGroupExpand"
|
||||
@toggle-command="handleToggleCommand"
|
||||
@rename="openRenameDialog"
|
||||
@view-details="openDetailsDialog"
|
||||
/>
|
||||
</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"
|
||||
:command="renameDialog.command"
|
||||
:loading="renameDialog.loading"
|
||||
@confirm="handleConfirmRename"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<DetailsDialog
|
||||
:show="detailsDialog.show"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
:command="detailsDialog.command"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user