feat(command-management): 新增指令层级管理与UI展示

- 【后端】
  - `CommandDescriptor` 新增 `parent_group_handler` 和 `sub_commands` 字段,支持指令层级结构定义。
  - `list_commands` 函数重构,实现指令的层级收集与构建,将子指令正确挂载到其父指令组下。
  - 新增 `_collect_all_descriptors` 和 `_find_parent_group_handler` 辅助函数,用于全面收集指令并定位父指令组。
  - `_build_descriptor` 优化指令类型判断逻辑,明确区分普通指令、指令组和子指令。
  - `_descriptor_to_dict` 递归处理子指令,确保 API 返回完整的指令层级数据。
- 【前端】
  - 指令管理页面 (`CommandPage.vue`) 增加指令类型筛选器,并支持指令组的展开/折叠功能。
  - 表格展示优化,为指令组和子指令添加不同的样式和缩进,提升层级结构的视觉可读性。
  - 指令详情对话框新增指令类型、所属指令组和子指令列表的展示。
  - 更新 `CommandItem` 接口,以适配后端提供的层级数据结构。
- 【i18n】
  - 新增指令类型(指令、指令组、子指令)的国际化文本。
  - 更新指令管理相关 UI 文本,包括表格头部、详情对话框字段和筛选器选项。
This commit is contained in:
Ocetars
2025-12-03 17:58:52 +08:00
parent 97c0be85e4
commit 7fa71c538e
4 changed files with 336 additions and 58 deletions
+87 -9
View File
@@ -26,10 +26,11 @@ class CommandDescriptor:
plugin_display_name: str | None = None
module_path: str = ""
description: str = ""
command_type: str = "command"
command_type: str = "command" # "command" | "group" | "sub_command"
raw_command_name: str | None = None
current_fragment: str | None = None
parent_signature: str = ""
parent_group_handler: str = "" # 父指令组的 handler_full_name
original_command: str | None = None
effective_command: str | None = None
aliases: list[str] = field(default_factory=list)
@@ -39,6 +40,7 @@ class CommandDescriptor:
is_sub_command: bool = False
config: CommandConfig | None = None
has_conflict: bool = False
sub_commands: list[CommandDescriptor] = field(default_factory=list)
async def sync_command_configs() -> None:
@@ -127,7 +129,7 @@ async def rename_command(
async def list_commands() -> list[dict[str, Any]]:
descriptors = _collect_raw_descriptors()
descriptors = _collect_all_descriptors()
config_records = await db_helper.get_command_configs()
config_map = {cfg.handler_full_name: cfg for cfg in config_records}
@@ -135,7 +137,7 @@ async def list_commands() -> list[dict[str, Any]]:
if cfg := config_map.get(desc.handler_full_name):
_bind_descriptor_with_config(desc, cfg)
# 检测冲突:按 effective_command 分组
# 检测冲突:按 effective_command 分组(只检测已启用的指令)
conflict_groups: dict[str, list[CommandDescriptor]] = defaultdict(list)
for desc in descriptors:
if desc.effective_command and desc.enabled:
@@ -147,10 +149,36 @@ async def list_commands() -> list[dict[str, Any]]:
for desc in group:
conflict_handler_names.add(desc.handler_full_name)
result = []
# 构建层级结构:将子指令挂载到父指令组
group_map: dict[str, CommandDescriptor] = {}
sub_commands: list[CommandDescriptor] = []
root_commands: list[CommandDescriptor] = []
for desc in descriptors:
desc.has_conflict = desc.handler_full_name in conflict_handler_names
if desc.is_group:
group_map[desc.handler_full_name] = desc
elif desc.is_sub_command:
sub_commands.append(desc)
else:
root_commands.append(desc)
# 将子指令挂载到对应的指令组
for sub in sub_commands:
sub.has_conflict = sub.handler_full_name in conflict_handler_names
if sub.parent_group_handler and sub.parent_group_handler in group_map:
group_map[sub.parent_group_handler].sub_commands.append(sub)
else:
# 如果找不到父指令组,作为独立指令处理
root_commands.append(sub)
# 合并结果:指令组(含子指令)+ 普通指令
result = []
for desc in group_map.values():
result.append(_descriptor_to_dict(desc))
for desc in root_commands:
result.append(_descriptor_to_dict(desc))
return result
@@ -193,6 +221,7 @@ async def list_command_conflicts() -> list[dict[str, Any]]:
def _collect_raw_descriptors() -> list[CommandDescriptor]:
"""收集所有根级指令(不含子指令)。"""
descriptors: list[CommandDescriptor] = []
for handler in star_handlers_registry:
desc = _build_descriptor(handler)
@@ -202,6 +231,17 @@ def _collect_raw_descriptors() -> list[CommandDescriptor]:
return descriptors
def _collect_all_descriptors() -> list[CommandDescriptor]:
"""收集所有指令,包括子指令。"""
descriptors: list[CommandDescriptor] = []
for handler in star_handlers_registry:
desc = _build_descriptor(handler)
if not desc:
continue
descriptors.append(desc)
return descriptors
def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
filter_ref = _locate_primary_filter(handler)
if filter_ref is None:
@@ -213,12 +253,20 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
) or handler.handler_module_path
plugin_display = plugin_meta.display_name if plugin_meta else None
is_sub_command = bool(handler.extras_configs.get("sub_command"))
parent_group_handler = ""
if isinstance(filter_ref, CommandFilter):
raw_fragment = getattr(
filter_ref, "_original_command_name", filter_ref.command_name
)
current_fragment = filter_ref.command_name
parent_signature = (filter_ref.parent_command_names or [""])[0].strip()
# 如果是子指令,尝试找到父指令组的 handler_full_name
if is_sub_command and parent_signature:
parent_group_handler = _find_parent_group_handler(
handler.handler_module_path, parent_signature
)
else:
raw_fragment = getattr(
filter_ref, "_original_group_name", filter_ref.group_name
@@ -229,6 +277,14 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
original_command = _compose_command(parent_signature, raw_fragment)
effective_command = _compose_command(parent_signature, current_fragment)
# 确定 command_type
if isinstance(filter_ref, CommandGroupFilter):
command_type = "group"
elif is_sub_command:
command_type = "sub_command"
else:
command_type = "command"
descriptor = CommandDescriptor(
handler=handler,
filter_ref=filter_ref,
@@ -238,19 +294,18 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
plugin_display_name=plugin_display,
module_path=handler.handler_module_path,
description=handler.desc or "",
command_type="group"
if isinstance(filter_ref, CommandGroupFilter)
else "command",
command_type=command_type,
raw_command_name=raw_fragment,
current_fragment=current_fragment,
parent_signature=parent_signature,
parent_group_handler=parent_group_handler,
original_command=original_command,
effective_command=effective_command,
aliases=sorted(getattr(filter_ref, "alias", set())),
permission=_determine_permission(handler),
enabled=handler.enabled,
is_group=isinstance(filter_ref, CommandGroupFilter),
is_sub_command=bool(handler.extras_configs.get("sub_command")),
is_sub_command=is_sub_command,
)
return descriptor
@@ -291,6 +346,22 @@ def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str:
return " ".join(reversed(signatures)).strip()
def _find_parent_group_handler(module_path: str, parent_signature: str) -> str:
"""根据模块路径和父级签名,找到对应的指令组 handler_full_name。"""
parent_sig_normalized = parent_signature.strip()
for handler in star_handlers_registry:
if handler.handler_module_path != module_path:
continue
filter_ref = _locate_primary_filter(handler)
if not isinstance(filter_ref, CommandGroupFilter):
continue
# 检查该指令组的完整指令名是否匹配 parent_signature
group_names = filter_ref.get_complete_command_names()
if parent_sig_normalized in group_names:
return handler.handler_full_name
return ""
def _compose_command(parent_signature: str, fragment: str | None) -> str:
fragment = (fragment or "").strip()
parent_signature = parent_signature.strip()
@@ -353,7 +424,7 @@ def _is_command_in_use(
def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
return {
result = {
"handler_full_name": desc.handler_full_name,
"handler_name": desc.handler_name,
"plugin": desc.plugin_name,
@@ -362,6 +433,7 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
"description": desc.description,
"type": desc.command_type,
"parent_signature": desc.parent_signature,
"parent_group_handler": desc.parent_group_handler,
"original_command": desc.original_command,
"current_fragment": desc.current_fragment,
"effective_command": desc.effective_command,
@@ -371,3 +443,9 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
"is_group": desc.is_group,
"has_conflict": desc.has_conflict,
}
# 如果是指令组,包含子指令列表
if desc.is_group and desc.sub_commands:
result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands]
else:
result["sub_commands"] = []
return result
@@ -13,6 +13,7 @@
"table": {
"headers": {
"command": "Command",
"type": "Type",
"plugin": "Plugin",
"description": "Description",
"permission": "Permission",
@@ -20,6 +21,12 @@
"actions": "Actions"
}
},
"type": {
"command": "Command",
"group": "Group",
"subCommand": "Sub-command"
},
"subCommandCount": "sub-commands",
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
@@ -44,10 +51,13 @@
},
"details": {
"title": "Command Details",
"type": "Command Type",
"handler": "Handler",
"module": "Module Path",
"originalCommand": "Original Command",
"effectiveCommand": "Effective Command",
"parentGroup": "Parent Group",
"subCommands": "Sub-commands",
"aliases": "Aliases",
"permission": "Permission",
"conflictStatus": "Conflict Status"
@@ -73,6 +83,7 @@
"disabled": "Disabled",
"conflict": "Conflict",
"byPlugin": "Filter by plugin",
"byType": "Filter by type",
"byPermission": "Filter by permission",
"byStatus": "Filter by status"
}
@@ -13,6 +13,7 @@
"table": {
"headers": {
"command": "指令",
"type": "类型",
"plugin": "所属插件",
"description": "描述",
"permission": "权限",
@@ -20,6 +21,12 @@
"actions": "操作"
}
},
"type": {
"command": "指令",
"group": "指令组",
"subCommand": "子指令"
},
"subCommandCount": "个子指令",
"status": {
"enabled": "已启用",
"disabled": "已禁用",
@@ -44,10 +51,13 @@
},
"details": {
"title": "指令详情",
"type": "指令类型",
"handler": "处理函数",
"module": "模块路径",
"originalCommand": "原始指令",
"effectiveCommand": "生效指令",
"parentGroup": "所属指令组",
"subCommands": "子指令列表",
"aliases": "别名",
"permission": "权限要求",
"conflictStatus": "冲突状态"
@@ -73,6 +83,7 @@
"disabled": "已禁用",
"conflict": "有冲突",
"byPlugin": "按插件筛选",
"byType": "按类型筛选",
"byPermission": "按权限筛选",
"byStatus": "按状态筛选"
}
+227 -49
View File
@@ -10,8 +10,9 @@ interface CommandItem {
plugin_display_name: string | null;
module_path: string;
description: string;
type: string;
type: string; // "command" | "group" | "sub_command"
parent_signature: string;
parent_group_handler: string;
original_command: string;
current_fragment: string;
effective_command: string;
@@ -20,6 +21,7 @@ interface CommandItem {
enabled: boolean;
is_group: boolean;
has_conflict: boolean;
sub_commands: CommandItem[];
}
interface CommandSummary {
@@ -49,6 +51,10 @@ const searchQuery = ref('');
const pluginFilter = ref('all');
const permissionFilter = ref('all');
const statusFilter = ref('all');
const typeFilter = ref('all');
// Track expanded groups
const expandedGroups = ref<Set<string>>(new Set());
// Rename dialog
const renameDialog = reactive({
@@ -66,12 +72,13 @@ const detailsDialog = reactive({
// Table headers
const commandHeaders = computed(() => [
{ title: tm('table.headers.command'), key: 'effective_command', width: '180px' },
{ title: tm('table.headers.command'), key: 'effective_command', width: '150px' },
{ title: tm('table.headers.type'), key: 'type', sortable: false, width: '110px' },
{ title: tm('table.headers.plugin'), key: 'plugin', width: '140px' },
{ title: tm('table.headers.description'), key: 'description', sortable: false, maxWidth: '260px' },
{ title: tm('table.headers.description'), key: 'description', sortable: false, maxWidth: '240px' },
{ title: tm('table.headers.permission'), key: 'permission', sortable: false, width: '100px' },
{ title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '120px' },
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '160px' }
{ title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '100px' },
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '140px' }
]);
// Computed: unique plugins for filter
@@ -80,66 +87,126 @@ const availablePlugins = computed(() => {
return Array.from(plugins).sort();
});
// Computed: filtered commands
const filteredCommands = computed(() => {
let result = commands.value;
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter(cmd =>
// Helper: check if a command matches filters
const matchesFilters = (cmd: CommandItem, query: string): boolean => {
// Search filter
if (query) {
const matchesSearch =
cmd.effective_command?.toLowerCase().includes(query) ||
cmd.description?.toLowerCase().includes(query) ||
cmd.plugin?.toLowerCase().includes(query)
);
cmd.plugin?.toLowerCase().includes(query);
if (!matchesSearch) return false;
}
if (pluginFilter.value !== 'all') {
result = result.filter(cmd => cmd.plugin === pluginFilter.value);
// Plugin filter
if (pluginFilter.value !== 'all' && cmd.plugin !== pluginFilter.value) {
return false;
}
// Permission filter
if (permissionFilter.value !== 'all') {
if (permissionFilter.value === 'everyone') {
// "所有人"筛选:包括 everyone 和 member 权限(当前 member 权限实际作用与 everyone 相同)
result = result.filter(cmd => cmd.permission === 'everyone' || cmd.permission === 'member');
} else {
result = result.filter(cmd => cmd.permission === permissionFilter.value);
if (cmd.permission !== 'everyone' && cmd.permission !== 'member') return false;
} else if (cmd.permission !== permissionFilter.value) {
return false;
}
}
// Status filter
if (statusFilter.value !== 'all') {
if (statusFilter.value === 'enabled') {
result = result.filter(cmd => cmd.enabled);
} else if (statusFilter.value === 'disabled') {
result = result.filter(cmd => !cmd.enabled);
} else if (statusFilter.value === 'conflict') {
result = result.filter(cmd => cmd.has_conflict);
}
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;
}
// Sort: conflict commands first, grouped by effective_command
// Type filter
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;
};
// Computed: filtered commands with hierarchy support
const filteredCommands = computed(() => {
const query = searchQuery.value.toLowerCase();
const result: CommandItem[] = [];
const conflictCmds: CommandItem[] = [];
const normalCmds: CommandItem[] = [];
const conflictGroupMap: Map<string, CommandItem[]> = new Map();
for (const cmd of result) {
if (cmd.has_conflict) {
const key = cmd.effective_command || '';
if (!conflictGroupMap.has(key)) {
conflictGroupMap.set(key, []);
for (const cmd of commands.value) {
// For groups, check if group or any sub-command matches
if (cmd.is_group) {
const groupMatches = matchesFilters(cmd, query);
const matchingSubCmds = (cmd.sub_commands || []).filter(sub => matchesFilters(sub, query));
// If group matches or has matching sub-commands, include it
if (groupMatches || matchingSubCmds.length > 0) {
if (cmd.has_conflict) {
conflictCmds.push(cmd);
} else {
normalCmds.push(cmd);
}
// If group is expanded, add matching sub-commands
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') {
// Regular commands (not sub-commands, they're handled via groups)
if (matchesFilters(cmd, query)) {
if (cmd.has_conflict) {
conflictCmds.push(cmd);
} else {
normalCmds.push(cmd);
}
}
conflictGroupMap.get(key)!.push(cmd);
} else {
normalCmds.push(cmd);
}
}
for (const [_, group] of conflictGroupMap) {
conflictCmds.push(...group);
}
// Sort conflicts by effective_command to group them together
conflictCmds.sort((a, b) => (a.effective_command || '').localeCompare(b.effective_command || ''));
return [...conflictCmds, ...normalCmds];
});
// Toggle group expansion
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);
}
};
// Check if group is expanded
const isGroupExpanded = (cmd: CommandItem): boolean => {
return expandedGroups.value.has(cmd.handler_full_name);
};
// Get type display info
const getTypeInfo = (type: string) => {
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' };
}
};
// Toast helper
const toast = (message: string, color: string = 'success') => {
snackbar.message = message;
@@ -250,12 +317,19 @@ const getStatusInfo = (cmd: CommandItem) => {
return { text: tm('status.disabled'), color: 'error', variant: 'outlined' as const };
};
// Get row props for conflict highlighting
// Get row props for conflict highlighting and sub-command styling
const getRowProps = ({ item }: { item: CommandItem }) => {
const classes: string[] = [];
if (item.has_conflict) {
return { class: 'conflict-row' };
classes.push('conflict-row');
}
return {};
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(' ') } : {};
};
onMounted(async () => {
@@ -270,7 +344,7 @@ onMounted(async () => {
<v-card-text style="padding: 0px 12px;">
<!-- Filters Row (Top) -->
<v-row class="mb-4" align="center">
<v-col cols="12" sm="4" md="3">
<v-col cols="12" sm="6" md="3">
<v-select
v-model="pluginFilter"
:items="[{ title: tm('filters.all'), value: 'all' }, ...availablePlugins.map(p => ({ title: p, value: p }))]"
@@ -280,7 +354,22 @@ onMounted(async () => {
hide-details
/>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-col cols="12" sm="6" md="2">
<v-select
v-model="typeFilter"
:items="[
{ 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' }
]"
:label="tm('filters.byType')"
density="compact"
variant="outlined"
hide-details
/>
</v-col>
<v-col cols="12" sm="6" md="2">
<v-select
v-model="permissionFilter"
:items="[
@@ -294,7 +383,7 @@ onMounted(async () => {
hide-details
/>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-col cols="12" sm="6" md="2">
<v-select
v-model="statusFilter"
:items="[
@@ -383,14 +472,47 @@ onMounted(async () => {
<template v-slot:item.effective_command="{ item }">
<div class="d-flex align-center py-2">
<!-- Expand/collapse button for groups -->
<v-btn
v-if="item.is_group && item.sub_commands?.length > 0"
icon
variant="text"
size="x-small"
class="mr-1"
@click.stop="toggleGroupExpand(item)"
>
<v-icon size="18">{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
</v-btn>
<!-- Indent for sub-commands -->
<div v-else-if="item.type === 'sub_command'" class="ml-6"></div>
<div>
<div class="text-subtitle-1 font-weight-medium">
<code>{{ item.effective_command }}</code>
<code :class="{ 'sub-command-code': item.type === 'sub_command' }">{{ item.effective_command }}</code>
<v-chip
v-if="item.is_group && item.sub_commands?.length > 0"
size="x-small"
color="info"
variant="tonal"
class="ml-2"
>
{{ item.sub_commands.length }} {{ tm('subCommandCount') }}
</v-chip>
</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 }}
</v-chip>
</template>
<template v-slot:item.plugin="{ item }">
<div class="text-body-2">{{ item.plugin_display_name || item.plugin }}</div>
</template>
@@ -505,6 +627,19 @@ onMounted(async () => {
<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(detailsDialog.command.type).color"
size="small"
variant="tonal"
>
<v-icon start size="14">{{ getTypeInfo(detailsDialog.command.type).icon }}</v-icon>
{{ getTypeInfo(detailsDialog.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>{{ detailsDialog.command.handler_name }}</code></v-list-item-subtitle>
@@ -521,6 +656,10 @@ onMounted(async () => {
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.effectiveCommand') }}</v-list-item-title>
<v-list-item-subtitle><code>{{ detailsDialog.command.effective_command }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="detailsDialog.command.parent_signature">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.parentGroup') }}</v-list-item-title>
<v-list-item-subtitle><code>{{ detailsDialog.command.parent_signature }}</code></v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="detailsDialog.command.aliases.length > 0">
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.aliases') }}</v-list-item-title>
<v-list-item-subtitle>
@@ -529,6 +668,21 @@ onMounted(async () => {
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="detailsDialog.command.is_group && detailsDialog.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 detailsDialog.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>
@@ -567,6 +721,11 @@ code {
border-radius: 4px;
font-size: 0.9em;
}
code.sub-command-code {
background-color: rgba(var(--v-theme-secondary), 0.1);
color: rgb(var(--v-theme-secondary));
}
</style>
<style>
@@ -579,4 +738,23 @@ code {
.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;
}
/* Group row styling */
.v-data-table .group-row {
background-color: rgba(var(--v-theme-info), 0.03);
}
.v-data-table .group-row:hover {
background-color: rgba(var(--v-theme-info), 0.08) !important;
}
/* Sub-command row styling */
.v-data-table .sub-command-row {
/* background-color: rgba(var(--v-theme-surface-variant), 0.3); */
border-left: 2px solid rgba(var(--v-theme-secondary), 0.3);
}
/* .v-data-table .sub-command-row:hover {
background-color: rgba(var(--v-theme-surface-variant), 0.5) !important;
} */
</style>