feat: add ConfigRouteManagerDialog component for managing routing configurations
- Implemented a new dialog component for managing routes associated with configurations, allowing users to view, delete, and save routes. - Enhanced the ProviderSelector component with improved styling for better readability. - Updated English and Chinese localization files to include new strings for the route manager and profile sidebar. - Refactored ConfigPage.vue to integrate the new route management dialog and improve layout responsiveness. - Added methods for handling route management, including fetching, saving, and removing routes.
This commit is contained in:
@@ -48,18 +48,29 @@ class Group:
|
||||
|
||||
|
||||
class AstrBotMessage:
|
||||
"""AstrBot 的消息对象"""
|
||||
"""Represents a message received from the platform, after parsing and normalization.
|
||||
This is the main message object that will be passed to plugins and handlers."""
|
||||
|
||||
type: MessageType # 消息类型
|
||||
self_id: str # 机器人的识别id
|
||||
session_id: str # 会话id。取决于 unique_session 的设置。
|
||||
message_id: str # 消息id
|
||||
group: Group | None # 群组
|
||||
sender: MessageMember # 发送者
|
||||
message: list[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
|
||||
message_str: str # 最直观的纯文本消息字符串
|
||||
type: MessageType
|
||||
"""GroupMessage, FriendMessage, etc"""
|
||||
self_id: str
|
||||
"""Bot's ID"""
|
||||
session_id: str
|
||||
"""Session ID, which is the last part of UMO"""
|
||||
message_id: str
|
||||
"""Message ID"""
|
||||
group: Group | None
|
||||
"""The group info, None if it's a friend message"""
|
||||
sender: MessageMember
|
||||
"""The sender info"""
|
||||
message: list[BaseMessageComponent]
|
||||
"""Sorted list of message components after parsing"""
|
||||
message_str: str
|
||||
"""The parsed message text after parsing, without any formatting or special components"""
|
||||
raw_message: object
|
||||
timestamp: int # 消息时间戳
|
||||
"""The raw message object, the specific type depends on the platform"""
|
||||
timestamp: int
|
||||
"""The timestamp when the message is received, in seconds"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.timestamp = int(time.time())
|
||||
@@ -70,16 +81,12 @@ class AstrBotMessage:
|
||||
|
||||
@property
|
||||
def group_id(self) -> str:
|
||||
"""向后兼容的 group_id 属性
|
||||
群组id,如果为私聊,则为空
|
||||
"""
|
||||
if self.group:
|
||||
return self.group.group_id
|
||||
return ""
|
||||
|
||||
@group_id.setter
|
||||
def group_id(self, value: str | None) -> None:
|
||||
"""设置 group_id"""
|
||||
if value:
|
||||
if self.group:
|
||||
self.group.group_id = value
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="config-profile-sidebar">
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<h3 class="text-subtitle-1 font-weight-bold mb-0">
|
||||
<v-icon size="18" class="mr-1">mdi-format-list-bulleted-square</v-icon>
|
||||
{{ tm('profileSidebar.title') }}
|
||||
</h3>
|
||||
<v-tooltip :text="tm('configManagement.manageConfigs')" location="top">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn v-bind="tooltipProps" size="small" variant="text" icon="mdi-cog" :disabled="disabled"
|
||||
@click="emit('manage')" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="config-profile-list">
|
||||
<v-card v-for="config in configs" :key="config.id" class="profile-card" :class="{
|
||||
'profile-card--active': config.id === selectedConfigId,
|
||||
'profile-card--disabled': disabled
|
||||
}" variant="outlined" @click="onSelect(config.id)">
|
||||
<div class="profile-card__name text-h4 d-flex align-center">
|
||||
<v-icon size="24" class="mr-2">mdi-file-outline</v-icon>
|
||||
{{ config.name }}
|
||||
</div>
|
||||
<div class="mt-3 d-flex" style="align-items: start; justify-content: center;">
|
||||
<v-icon size="24" class="mr-1">mdi-routes</v-icon>
|
||||
<div class="profile-card__bindings">
|
||||
<template v-if="bindingsForConfig(config.id).length > 0">
|
||||
<v-tooltip v-for="binding in visibleBindings(bindingsForConfig(config.id))"
|
||||
:key="`${config.id}-${binding.platformId}`" location="top">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<button v-bind="tooltipProps" type="button" class="binding-pill"
|
||||
@click.stop="onManageRoutes(config.id)">
|
||||
<v-avatar size="22" class="binding-avatar" rounded="sm">
|
||||
<img v-if="getBindingIcon(binding)" :src="getBindingIcon(binding)" :alt="binding.platformId"
|
||||
class="binding-avatar__img" />
|
||||
<v-icon v-else size="14">mdi-robot-outline</v-icon>
|
||||
</v-avatar>
|
||||
<span class="binding-pill__label">
|
||||
{{ binding.platformId }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="binding-tooltip-content">
|
||||
<div class="text-caption font-weight-bold mb-1">
|
||||
{{ tm('profileSidebar.platformId') }}: {{ binding.platformId }}
|
||||
</div>
|
||||
<div class="text-caption mb-1">
|
||||
{{ tm('profileSidebar.umop') }}:
|
||||
</div>
|
||||
<div v-for="umop in binding.umops" :key="`${binding.platformId}-${umop}`" class="text-caption">
|
||||
{{ umop }}
|
||||
</div>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
<v-chip v-if="bindingsForConfig(config.id).length > maxVisibleBindings" size="x-small" variant="tonal"
|
||||
color="primary">
|
||||
+{{ bindingsForConfig(config.id).length - maxVisibleBindings }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span v-else class="text-caption text-medium-emphasis">
|
||||
{{ tm('profileSidebar.noBindings') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { getPlatformIcon } from '@/utils/platformUtils';
|
||||
|
||||
interface ConfigInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ConfigBinding {
|
||||
platformId: string;
|
||||
platformType?: string;
|
||||
umops: string[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
configs: ConfigInfo[];
|
||||
selectedConfigId: string | null;
|
||||
bindingsByConfigId: Record<string, ConfigBinding[]>;
|
||||
disabled?: boolean;
|
||||
}>(), {
|
||||
selectedConfigId: null,
|
||||
bindingsByConfigId: () => ({}),
|
||||
disabled: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [configId: string];
|
||||
manage: [];
|
||||
manageRoutes: [payload: { configId: string }];
|
||||
}>();
|
||||
|
||||
const { tm } = useModuleI18n('features/config');
|
||||
|
||||
const maxVisibleBindings = 6;
|
||||
|
||||
function onSelect(configId: string): void {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
emit('select', configId);
|
||||
}
|
||||
|
||||
function onManageRoutes(configId: string): void {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
emit('manageRoutes', { configId });
|
||||
}
|
||||
|
||||
function bindingsForConfig(configId: string): ConfigBinding[] {
|
||||
return props.bindingsByConfigId[configId] || [];
|
||||
}
|
||||
|
||||
function visibleBindings(bindings: ConfigBinding[]): ConfigBinding[] {
|
||||
return bindings.slice(0, maxVisibleBindings);
|
||||
}
|
||||
|
||||
function getBindingIcon(binding: ConfigBinding): string | undefined {
|
||||
if (binding.platformType) {
|
||||
return getPlatformIcon(binding.platformType);
|
||||
}
|
||||
return getPlatformIcon(binding.platformId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-profile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: calc(100vh - 210px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
.profile-card--active {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.profile-card--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.profile-card__name {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.profile-card__bindings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.binding-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 8px 2px 4px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.14);
|
||||
background: rgba(var(--v-theme-surface), 1);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.binding-pill:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.45);
|
||||
background: rgba(var(--v-theme-primary), 0.06);
|
||||
}
|
||||
|
||||
.binding-pill__label {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
}
|
||||
|
||||
.binding-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.binding-tooltip-content {
|
||||
max-width: 380px;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialogVisible" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h3 pa-2">{{ props.configName }} {{ tm('routeManager.title') }}</div>
|
||||
</div>
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialogVisible = false"></v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="loading" class="d-flex justify-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="text-caption text-medium-emphasis mb-4">
|
||||
{{ tm('routeManager.hint') }}
|
||||
</div>
|
||||
|
||||
<div v-if="groupedRoutes.length === 0" class="text-center py-4 text-medium-emphasis">
|
||||
{{ tm('routeManager.empty') }}
|
||||
</div>
|
||||
|
||||
<div v-for="(group, groupIndex) in groupedRoutes" :key="group.platformId">
|
||||
<v-divider v-if="groupIndex > 0" class="my-3" />
|
||||
<div class="route-group">
|
||||
<div class="route-group-platform">
|
||||
<v-avatar size="22" rounded="sm" class="route-platform-avatar">
|
||||
<img
|
||||
v-if="getRoutePlatformIcon(group.platformId)"
|
||||
:src="getRoutePlatformIcon(group.platformId)"
|
||||
:alt="group.platformId"
|
||||
class="route-platform-avatar__img"
|
||||
/>
|
||||
<v-icon v-else size="14">mdi-robot-outline</v-icon>
|
||||
</v-avatar>
|
||||
<span class="text-body-2 font-weight-medium">{{ group.platformId }}</span>
|
||||
<v-chip size="x-small" variant="tonal" color="primary">
|
||||
{{ group.routes.length }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="route-group-umops">
|
||||
<div
|
||||
v-for="route in group.routes"
|
||||
:key="route.id"
|
||||
class="route-umop-row"
|
||||
:class="{ 'route-umop-row--all': isAllSessionsRoute(route.umop) }"
|
||||
>
|
||||
<span class="text-body-2 route-umop-row__text">
|
||||
{{ isAllSessionsRoute(route.umop) ? tm('routeManager.allSessions') : route.umop }}
|
||||
</span>
|
||||
<div class="route-umop-row__actions">
|
||||
<v-tooltip :text="tm('routeManager.delete')" location="top">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
v-bind="tooltipProps"
|
||||
icon="mdi-delete-outline"
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
@click="emit('removeRoute', route.id)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialogVisible = false">
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" :loading="saving" @click="emit('save')">
|
||||
{{ tm('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { getPlatformIcon } from '@/utils/platformUtils';
|
||||
|
||||
interface RouteItem {
|
||||
id: string;
|
||||
platformId: string;
|
||||
umop: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: boolean;
|
||||
configId: string;
|
||||
configName: string;
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
items: RouteItem[];
|
||||
platformTypeMap: Record<string, string>;
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
configId: '',
|
||||
configName: '',
|
||||
loading: false,
|
||||
saving: false,
|
||||
items: () => [],
|
||||
platformTypeMap: () => ({})
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
removeRoute: [routeId: string];
|
||||
save: [];
|
||||
}>();
|
||||
|
||||
const { tm } = useModuleI18n('features/config');
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
const groupedRoutes = computed(() => {
|
||||
const groups: Record<string, RouteItem[]> = {};
|
||||
for (const item of props.items) {
|
||||
const platformId = String(item.platformId || '').trim();
|
||||
if (!platformId) {
|
||||
continue;
|
||||
}
|
||||
if (!groups[platformId]) {
|
||||
groups[platformId] = [];
|
||||
}
|
||||
groups[platformId].push(item);
|
||||
}
|
||||
|
||||
return Object.entries(groups)
|
||||
.map(([platformId, routes]) => ({
|
||||
platformId,
|
||||
routes: (() => {
|
||||
const sortedRoutes = routes.sort((a, b) => a.umop.localeCompare(b.umop));
|
||||
const allSessionsRoute = sortedRoutes.find((route) => isAllSessionsRoute(route.umop));
|
||||
if (allSessionsRoute) {
|
||||
return [allSessionsRoute];
|
||||
}
|
||||
return sortedRoutes;
|
||||
})()
|
||||
}))
|
||||
.sort((a, b) => a.platformId.localeCompare(b.platformId));
|
||||
});
|
||||
|
||||
function getRoutePlatformIcon(platformId: string): string | undefined {
|
||||
const platformType = props.platformTypeMap[platformId] || platformId;
|
||||
return getPlatformIcon(platformType);
|
||||
}
|
||||
|
||||
function isAllSessionsRoute(umop: string): boolean {
|
||||
return String(umop || '').endsWith(':*:*');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.route-group-platform {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.route-group-umops {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.route-umop-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 6px;
|
||||
padding: 2px 4px 2px 10px;
|
||||
gap: 10px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.03);
|
||||
}
|
||||
|
||||
.route-umop-row--all {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.route-umop-row__text {
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.route-umop-row__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.route-platform-avatar {
|
||||
background: rgba(var(--v-theme-surface), 1);
|
||||
}
|
||||
|
||||
.route-platform-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.route-group {
|
||||
display: grid;
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.route-group {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.route-group-platform {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.route-umop-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -372,6 +372,7 @@ function closeProviderDrawer() {
|
||||
white-space: nowrap;
|
||||
max-width: calc(100% - 80px);
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.selected-preview {
|
||||
|
||||
@@ -69,6 +69,26 @@
|
||||
"normalConfig": "Basic",
|
||||
"systemConfig": "System"
|
||||
},
|
||||
"profileSidebar": {
|
||||
"title": "Configuration Profiles",
|
||||
"platformId": "Platform ID",
|
||||
"umop": "Bound UMOP",
|
||||
"noBindings": "No platform bindings"
|
||||
},
|
||||
"routeManager": {
|
||||
"title": "Route Manager",
|
||||
"targetConfig": "Config: {config}",
|
||||
"hint": "AstrBot supports multiple config files, and routing decides which session uses which config. This dialog shows all routes handled by the current config: platform on the left and UMOP on the right; click Save after deleting routes.",
|
||||
"empty": "No routes available to manage.",
|
||||
"platform": "Platform",
|
||||
"umop": "UMOP",
|
||||
"allSessions": "All Sessions",
|
||||
"delete": "Delete Route",
|
||||
"loadFailed": "Failed to load routes",
|
||||
"saveSuccess": "Routes saved",
|
||||
"saveFailed": "Failed to save routes",
|
||||
"routeOccupied": "This route is already occupied by another config: {umop}"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search config items (key/description/hint)",
|
||||
"noResult": "No matching config items found"
|
||||
|
||||
@@ -69,6 +69,26 @@
|
||||
"normalConfig": "普通",
|
||||
"systemConfig": "系统"
|
||||
},
|
||||
"profileSidebar": {
|
||||
"title": "配置文件列表",
|
||||
"platformId": "平台 ID",
|
||||
"umop": "绑定 UMOP",
|
||||
"noBindings": "暂无平台绑定"
|
||||
},
|
||||
"routeManager": {
|
||||
"title": "路由管理",
|
||||
"targetConfig": "配置:{config}",
|
||||
"hint": "AstrBot 支持多配置文件,路由用于决定“哪个会话用哪个配置”。这里展示的是当前配置文件接管的全部路由:左侧是机器人 ID、右侧是匹配的消息会话来源。",
|
||||
"empty": "暂无可管理的路由。",
|
||||
"platform": "平台",
|
||||
"umop": "UMOP",
|
||||
"allSessions": "全部会话",
|
||||
"delete": "删除路由",
|
||||
"loadFailed": "加载路由失败",
|
||||
"saveSuccess": "路由已保存",
|
||||
"saveFailed": "保存路由失败",
|
||||
"routeOccupied": "该路由已被其他配置占用:{umop}"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索配置项(字段名/描述/提示)",
|
||||
"noResult": "未找到匹配的配置项"
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
<template>
|
||||
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
|
||||
style="display: flex; flex-direction: column; align-items: start;">
|
||||
<div class="config-page-wrap">
|
||||
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel">
|
||||
<div class="config-workbench" :class="{ 'config-workbench--system': isSystemConfig || !!initialConfigId }">
|
||||
<aside v-if="!isSystemConfig && !initialConfigId" class="config-sidebar">
|
||||
<ConfigProfileSidebar
|
||||
:configs="configInfoList"
|
||||
:selected-config-id="selectedConfigID"
|
||||
:bindings-by-config-id="configBindingsById"
|
||||
:disabled="initialConfigId !== null"
|
||||
@select="onConfigSelect"
|
||||
@manage="openConfigManageDialog"
|
||||
@manage-routes="openRouteManageDialog"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<div class="config-toolbar d-flex flex-row pr-4"
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; width: 100%; justify-content: space-between;">
|
||||
<div class="config-toolbar-controls d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select class="config-select" style="min-width: 130px;" :model-value="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
</v-select>
|
||||
<section class="config-main">
|
||||
<div class="config-toolbar d-flex flex-row">
|
||||
<div class="config-toolbar-controls d-flex flex-row align-center">
|
||||
<div v-if="!isSystemConfig" class="config-current-title">
|
||||
<h2 class="config-current-title__name">
|
||||
{{ selectedConfigInfo.name || selectedConfigID }}
|
||||
</h2>
|
||||
<div class="config-current-title__id text-caption text-medium-emphasis">
|
||||
ID: {{ selectedConfigID }}
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-if="!isSystemConfig && !initialConfigId"
|
||||
class="config-select config-select--mobile"
|
||||
:model-value="selectedConfigID"
|
||||
:items="configSelectItems"
|
||||
item-title="name"
|
||||
:disabled="initialConfigId !== null"
|
||||
item-value="id"
|
||||
:label="tm('configSelection.selectConfig')"
|
||||
hide-details
|
||||
density="compact"
|
||||
rounded="md"
|
||||
variant="outlined"
|
||||
@update:model-value="onConfigSelect"
|
||||
/>
|
||||
<v-tooltip v-if="!isSystemConfig && !initialConfigId" :text="tm('configManagement.manageConfigs')" location="top">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
v-bind="tooltipProps"
|
||||
class="config-manage-mobile"
|
||||
variant="text"
|
||||
icon="mdi-cog"
|
||||
:disabled="initialConfigId !== null"
|
||||
@click="openConfigManageDialog"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-text-field
|
||||
class="config-search-input"
|
||||
v-model="configSearchKeyword"
|
||||
@@ -20,14 +62,12 @@
|
||||
density="compact"
|
||||
rounded="md"
|
||||
variant="outlined"
|
||||
style="min-width: 280px;"
|
||||
/>
|
||||
<!-- <a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<v-slide-y-transition>
|
||||
<div v-if="fetched && hasUnsavedChanges" class="unsaved-changes-banner-wrap">
|
||||
<v-fade-transition>
|
||||
<div v-if="fetched && hasUnsavedChanges && !isLoadingConfig" class="unsaved-changes-banner-wrap">
|
||||
<v-banner
|
||||
icon="$warning"
|
||||
lines="one"
|
||||
@@ -36,12 +76,10 @@
|
||||
{{ tm('messages.unsavedChangesNotice') }}
|
||||
</v-banner>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<!-- <v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear> -->
|
||||
</v-fade-transition>
|
||||
|
||||
<v-slide-y-transition mode="out-in">
|
||||
<div v-if="(selectedConfigID || isSystemConfig) && fetched" :key="configContentKey" class="config-content" style="width: 100%;">
|
||||
<!-- 可视化编辑 -->
|
||||
<v-fade-transition mode="out-in">
|
||||
<div v-if="(selectedConfigID || isSystemConfig) && fetched" :key="configContentKey" class="config-content">
|
||||
<AstrBotCoreConfigWrapper
|
||||
:metadata="metadata"
|
||||
:config_data="config_data"
|
||||
@@ -72,10 +110,10 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
|
||||
</v-fade-transition>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +196,18 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<ConfigRouteManagerDialog
|
||||
v-model="routeManageDialog"
|
||||
:config-id="routeManageConfigId"
|
||||
:config-name="routeManageConfigName"
|
||||
:loading="routeManageLoading"
|
||||
:saving="routeManageSaving"
|
||||
:items="routeManageItems"
|
||||
:platform-type-map="routeManagePlatformTypeMap"
|
||||
@remove-route="removeRouteItem"
|
||||
@save="saveRouteManageDialog"
|
||||
/>
|
||||
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
@@ -201,6 +251,8 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
||||
import ConfigProfileSidebar from '@/components/config/ConfigProfileSidebar.vue';
|
||||
import ConfigRouteManagerDialog from '@/components/config/ConfigRouteManagerDialog.vue';
|
||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
@@ -216,6 +268,8 @@ export default {
|
||||
name: 'ConfigPage',
|
||||
components: {
|
||||
AstrBotCoreConfigWrapper,
|
||||
ConfigProfileSidebar,
|
||||
ConfigRouteManagerDialog,
|
||||
VueMonacoEditor,
|
||||
WaitingForRestart,
|
||||
StandaloneChat,
|
||||
@@ -295,19 +349,7 @@ export default {
|
||||
return this.configInfoList.find(info => info.id === this.selectedConfigID) || {};
|
||||
},
|
||||
configSelectItems() {
|
||||
const items = [...this.configInfoList];
|
||||
items.push({
|
||||
id: '_%manage%_',
|
||||
name: this.tm('configManagement.manageConfigs'),
|
||||
umop: []
|
||||
});
|
||||
return items;
|
||||
},
|
||||
hasUnsavedChanges() {
|
||||
if (!this.fetched) {
|
||||
return false;
|
||||
}
|
||||
return this.getConfigSnapshot(this.config_data) !== this.lastSavedConfigSnapshot;
|
||||
return [...this.configInfoList];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -317,7 +359,7 @@ export default {
|
||||
config_data: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.fetched) {
|
||||
if (this.fetched && !this.isLoadingConfig) {
|
||||
this.hasUnsavedChanges = this.configHasChanges;
|
||||
}
|
||||
}
|
||||
@@ -338,6 +380,13 @@ export default {
|
||||
return {
|
||||
codeEditorDialog: false,
|
||||
configManageDialog: false,
|
||||
routeManageDialog: false,
|
||||
routeManageLoading: false,
|
||||
routeManageSaving: false,
|
||||
routeManageConfigId: '',
|
||||
routeManageConfigName: '',
|
||||
routeManageItems: [],
|
||||
routeManagePlatformTypeMap: {},
|
||||
showConfigForm: false,
|
||||
isEditingConfig: false,
|
||||
config_data_has_changed: false,
|
||||
@@ -345,13 +394,13 @@ export default {
|
||||
config_data: {
|
||||
config: {}
|
||||
},
|
||||
isLoadingConfig: false,
|
||||
fetched: false,
|
||||
metadata: {},
|
||||
save_message_snack: false,
|
||||
save_message: "",
|
||||
save_message_success: "",
|
||||
configContentKey: 0,
|
||||
lastSavedConfigSnapshot: '',
|
||||
|
||||
// 配置类型切换
|
||||
configType: 'normal', // 'normal' 或 'system'
|
||||
@@ -364,6 +413,7 @@ export default {
|
||||
selectedConfigID: null, // 用于存储当前选中的配置项信息
|
||||
currentConfigId: null, // 跟踪当前正在编辑的配置id
|
||||
configInfoList: [],
|
||||
configBindingsById: {},
|
||||
configFormData: {
|
||||
name: '',
|
||||
},
|
||||
@@ -409,16 +459,12 @@ export default {
|
||||
methods: {
|
||||
// 处理语言切换事件,重新加载配置以获取插件的 i18n 数据
|
||||
handleLocaleChange() {
|
||||
// 重新加载当前配置
|
||||
if (this.selectedConfigID) {
|
||||
this.getConfig(this.selectedConfigID);
|
||||
} else if (this.isSystemConfig) {
|
||||
this.getConfig();
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
extractConfigTypeFromHash(hash) {
|
||||
const rawHash = String(hash || '');
|
||||
const lastHashIndex = rawHash.lastIndexOf('#');
|
||||
@@ -438,10 +484,232 @@ export default {
|
||||
await this.onConfigTypeToggle();
|
||||
return true;
|
||||
},
|
||||
openConfigManageDialog() {
|
||||
this.configManageDialog = true;
|
||||
},
|
||||
parseUmop(umop) {
|
||||
const raw = String(umop || '');
|
||||
const parts = raw.split(':');
|
||||
if (parts.length < 3) {
|
||||
return {
|
||||
platformId: raw || '*',
|
||||
messageType: '*',
|
||||
sessionId: '*'
|
||||
};
|
||||
}
|
||||
return {
|
||||
platformId: parts[0] || '*',
|
||||
messageType: parts[1] || '*',
|
||||
sessionId: parts.slice(2).join(':') || '*'
|
||||
};
|
||||
},
|
||||
createRouteItem(umop) {
|
||||
const parsed = this.parseUmop(umop);
|
||||
return {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
platformId: parsed.platformId,
|
||||
umop
|
||||
};
|
||||
},
|
||||
isRouteEntryForConfig(umop, confId, targetConfigId) {
|
||||
if (String(confId || '') !== String(targetConfigId || '')) {
|
||||
return false;
|
||||
}
|
||||
const parsed = this.parseUmop(umop);
|
||||
return parsed.platformId !== 'webchat';
|
||||
},
|
||||
async openRouteManageDialog(payload) {
|
||||
const configId = payload?.configId;
|
||||
if (!configId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.routeManageDialog = true;
|
||||
this.routeManageLoading = true;
|
||||
this.routeManageConfigId = configId;
|
||||
this.routeManageConfigName = this.configInfoList.find((item) => item.id === configId)?.name || configId;
|
||||
this.routeManageItems = [];
|
||||
this.routeManagePlatformTypeMap = {};
|
||||
|
||||
try {
|
||||
const [routeRes, platformRes] = await Promise.all([
|
||||
axios.get('/api/config/umo_abconf_routes'),
|
||||
axios.get('/api/config/platform/list')
|
||||
]);
|
||||
const routing = routeRes?.data?.data?.routing || {};
|
||||
const platforms = platformRes?.data?.data?.platforms || [];
|
||||
|
||||
const typeMap = {};
|
||||
for (const platform of platforms) {
|
||||
const pid = String(platform?.id || '').trim();
|
||||
if (!pid) {
|
||||
continue;
|
||||
}
|
||||
typeMap[pid] = platform.platform_type || platform.type || pid;
|
||||
}
|
||||
this.routeManagePlatformTypeMap = typeMap;
|
||||
|
||||
const matched = [];
|
||||
for (const [umop, conf] of Object.entries(routing)) {
|
||||
if (!this.isRouteEntryForConfig(umop, conf, configId)) {
|
||||
continue;
|
||||
}
|
||||
matched.push(this.createRouteItem(umop));
|
||||
}
|
||||
this.routeManageItems = matched.sort((a, b) => {
|
||||
const platformCompare = a.platformId.localeCompare(b.platformId);
|
||||
if (platformCompare !== 0) {
|
||||
return platformCompare;
|
||||
}
|
||||
return a.umop.localeCompare(b.umop);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load routes for route manager:', err);
|
||||
this.save_message = this.tm('routeManager.loadFailed');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
this.routeManageItems = [];
|
||||
} finally {
|
||||
this.routeManageLoading = false;
|
||||
}
|
||||
},
|
||||
removeRouteItem(entryId) {
|
||||
this.routeManageItems = this.routeManageItems.filter((item) => item.id !== entryId);
|
||||
},
|
||||
async saveRouteManageDialog() {
|
||||
if (!this.routeManageConfigId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.routeManageSaving = true;
|
||||
try {
|
||||
const res = await axios.get('/api/config/umo_abconf_routes');
|
||||
const routing = res?.data?.data?.routing || {};
|
||||
const entries = Object.entries(routing);
|
||||
const nonTargetEntries = [];
|
||||
const nonTargetUmopSet = new Set();
|
||||
let firstTargetIndex = -1;
|
||||
|
||||
entries.forEach(([umop, confId], index) => {
|
||||
if (this.isRouteEntryForConfig(umop, confId, this.routeManageConfigId)) {
|
||||
if (firstTargetIndex === -1) {
|
||||
firstTargetIndex = index;
|
||||
}
|
||||
return;
|
||||
}
|
||||
nonTargetEntries.push([umop, confId]);
|
||||
nonTargetUmopSet.add(umop);
|
||||
});
|
||||
|
||||
const targetEntries = [];
|
||||
for (const item of this.routeManageItems) {
|
||||
const umop = String(item.umop || '').trim();
|
||||
if (!umop) {
|
||||
continue;
|
||||
}
|
||||
if (nonTargetUmopSet.has(umop)) {
|
||||
this.save_message = this.tm('routeManager.routeOccupied', { umop });
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
this.routeManageSaving = false;
|
||||
return;
|
||||
}
|
||||
targetEntries.push([umop, this.routeManageConfigId]);
|
||||
}
|
||||
|
||||
const insertIndex = firstTargetIndex === -1 ? nonTargetEntries.length : Math.min(firstTargetIndex, nonTargetEntries.length);
|
||||
const mergedEntries = [
|
||||
...nonTargetEntries.slice(0, insertIndex),
|
||||
...targetEntries,
|
||||
...nonTargetEntries.slice(insertIndex)
|
||||
];
|
||||
const mergedRouting = Object.fromEntries(mergedEntries);
|
||||
|
||||
await axios.post('/api/config/umo_abconf_route/update_all', {
|
||||
routing: mergedRouting
|
||||
});
|
||||
|
||||
this.routeManageDialog = false;
|
||||
this.save_message = this.tm('routeManager.saveSuccess');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
await this.refreshConfigBindings();
|
||||
} catch (err) {
|
||||
console.error('Failed to save routes for route manager:', err);
|
||||
this.save_message = this.tm('routeManager.saveFailed');
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
} finally {
|
||||
this.routeManageSaving = false;
|
||||
}
|
||||
},
|
||||
buildConfigBindingMap(routingTable, platforms) {
|
||||
const platformTypeMap = {};
|
||||
for (const platform of platforms || []) {
|
||||
if (!platform?.id) {
|
||||
continue;
|
||||
}
|
||||
platformTypeMap[platform.id] = platform.platform_type || platform.type || platform.id;
|
||||
}
|
||||
|
||||
const grouped = {};
|
||||
for (const [umop, confId] of Object.entries(routingTable || {})) {
|
||||
const resolvedConfigId = String(confId || 'default');
|
||||
const parsed = this.parseUmop(umop);
|
||||
const platformId = parsed.platformId || '*';
|
||||
if (platformId === 'webchat') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!grouped[resolvedConfigId]) {
|
||||
grouped[resolvedConfigId] = {};
|
||||
}
|
||||
if (!grouped[resolvedConfigId][platformId]) {
|
||||
grouped[resolvedConfigId][platformId] = {
|
||||
platformId,
|
||||
platformType: platformTypeMap[platformId] || platformId,
|
||||
umops: []
|
||||
};
|
||||
}
|
||||
grouped[resolvedConfigId][platformId].umops.push(umop);
|
||||
}
|
||||
|
||||
const bindingMap = {};
|
||||
for (const [confId, platformsById] of Object.entries(grouped)) {
|
||||
bindingMap[confId] = Object.values(platformsById).sort((a, b) => {
|
||||
return a.platformId.localeCompare(b.platformId);
|
||||
});
|
||||
}
|
||||
return bindingMap;
|
||||
},
|
||||
async refreshConfigBindings() {
|
||||
try {
|
||||
const [routesRes, platformsRes] = await Promise.all([
|
||||
axios.get('/api/config/umo_abconf_routes'),
|
||||
axios.get('/api/config/platform/list')
|
||||
]);
|
||||
const routing = routesRes?.data?.data?.routing || {};
|
||||
const platforms = platformsRes?.data?.data?.platforms || [];
|
||||
this.configBindingsById = this.buildConfigBindingMap(routing, platforms);
|
||||
} catch (err) {
|
||||
console.error('Failed to load config bindings:', err);
|
||||
this.configBindingsById = {};
|
||||
}
|
||||
},
|
||||
getConfigInfoList(abconf_id) {
|
||||
// 获取配置列表
|
||||
axios.get('/api/config/abconfs').then((res) => {
|
||||
this.configInfoList = res.data.data.info_list;
|
||||
const infoList = Array.isArray(res.data?.data?.info_list) ? res.data.data.info_list : [];
|
||||
this.configInfoList = [...infoList].sort((a, b) => {
|
||||
if (a.id === 'default' && b.id !== 'default') {
|
||||
return -1;
|
||||
}
|
||||
if (a.id !== 'default' && b.id === 'default') {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
this.refreshConfigBindings();
|
||||
|
||||
if (abconf_id) {
|
||||
let matched = false;
|
||||
@@ -466,9 +734,12 @@ export default {
|
||||
this.save_message = this.messages.loadError;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
this.configBindingsById = {};
|
||||
});
|
||||
},
|
||||
getConfig(abconf_id) {
|
||||
this.isLoadingConfig = true;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.fetched = false
|
||||
const params = {};
|
||||
|
||||
@@ -482,22 +753,20 @@ export default {
|
||||
params: params
|
||||
}).then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.fetched = true
|
||||
this.metadata = res.data.data.metadata;
|
||||
this.configContentKey += 1;
|
||||
// 获取配置后更新
|
||||
this.$nextTick(() => {
|
||||
this.originalConfigData = JSON.parse(JSON.stringify(this.config_data));
|
||||
this.hasUnsavedChanges = false;
|
||||
this.configContentKey += 1;
|
||||
if (!this.isSystemConfig) {
|
||||
this.currentConfigId = abconf_id || this.selectedConfigID;
|
||||
}
|
||||
});
|
||||
this.fetched = true;
|
||||
}).catch((err) => {
|
||||
this.save_message = this.messages.loadError;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "error";
|
||||
}).finally(() => {
|
||||
this.isLoadingConfig = false;
|
||||
});
|
||||
},
|
||||
updateConfig() {
|
||||
@@ -515,7 +784,6 @@ export default {
|
||||
|
||||
return axios.post('/api/config/astrbot/update', postData).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.save_message = res.data.message || this.messages.saveSuccess;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
@@ -584,17 +852,10 @@ export default {
|
||||
});
|
||||
},
|
||||
async onConfigSelect(value) {
|
||||
if (value === '_%manage%_') {
|
||||
this.configManageDialog = true;
|
||||
// 重置选择到之前的值
|
||||
this.$nextTick(() => {
|
||||
this.selectedConfigID = this.selectedConfigInfo.id || 'default';
|
||||
this.getConfig(this.selectedConfigID);
|
||||
});
|
||||
} else {
|
||||
// 检查是否有未保存的更改
|
||||
if (!value || value === this.selectedConfigID) {
|
||||
return;
|
||||
}
|
||||
if (this.hasUnsavedChanges) {
|
||||
// 获取之前正在编辑的配置id
|
||||
const prevConfigId = this.isSystemConfig ? 'default' : (this.currentConfigId || this.selectedConfigID || 'default');
|
||||
const message = this.tm('unsavedChangesWarning.switchConfig');
|
||||
const saveAndSwitch = await this.$refs.unsavedChangesDialog?.open({
|
||||
@@ -604,14 +865,11 @@ export default {
|
||||
cancelHint: `${this.tm('unsavedChangesWarning.options.discardAndSwitch')}:${this.tm('unsavedChangesWarning.options.cancel')}`,
|
||||
closeHint: `${this.tm('unsavedChangesWarning.options.closeCard')}:"x"`
|
||||
});
|
||||
// 关闭弹窗不切换
|
||||
if (saveAndSwitch === 'close') {
|
||||
return;
|
||||
}
|
||||
if (saveAndSwitch) {
|
||||
// 设置临时变量保存切换后的id
|
||||
const currentSelectedId = this.selectedConfigID;
|
||||
// 把id设置回切换前的用于保存上一次的配置,保存完后恢复id为切换后的
|
||||
this.selectedConfigID = prevConfigId;
|
||||
const result = await this.updateConfig();
|
||||
this.selectedConfigID = currentSelectedId;
|
||||
@@ -620,17 +878,13 @@ export default {
|
||||
this.getConfig(value);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// 取消保存并切换配置
|
||||
}
|
||||
this.selectedConfigID = value;
|
||||
this.getConfig(value);
|
||||
}
|
||||
} else {
|
||||
// 无未保存更改直接切换
|
||||
this.selectedConfigID = value;
|
||||
this.getConfig(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
startCreateConfig() {
|
||||
this.showConfigForm = true;
|
||||
@@ -758,6 +1012,7 @@ export default {
|
||||
// 切换到系统配置
|
||||
this.getConfig();
|
||||
} else {
|
||||
this.refreshConfigBindings();
|
||||
// 切换回普通配置,如果有选中的配置文件则加载,否则加载default
|
||||
if (this.selectedConfigID) {
|
||||
this.getConfig(this.selectedConfigID);
|
||||
@@ -785,9 +1040,6 @@ export default {
|
||||
closeTestChat() {
|
||||
this.testChatDrawer = false;
|
||||
this.testConfigId = null;
|
||||
},
|
||||
getConfigSnapshot(config) {
|
||||
return JSON.stringify(config ?? {});
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -799,6 +1051,80 @@ export default {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.config-page-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: min(1160px, calc(100vw - 48px));
|
||||
}
|
||||
|
||||
.config-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.config-workbench--system {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.config-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--v-layout-top, 64px) + 16px);
|
||||
}
|
||||
|
||||
.config-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-current-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-current-title__name {
|
||||
font-family: inherit;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.config-current-title__id {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.config-toolbar {
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-toolbar-controls {
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.config-search-input {
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.config-select--mobile,
|
||||
.config-manage-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -852,35 +1178,53 @@ export default {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.config-panel {
|
||||
width: 750px;
|
||||
@media (max-width: 959px) {
|
||||
.config-workbench {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.config-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-select--mobile,
|
||||
.config-manage-mobile {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.config-select--mobile {
|
||||
min-width: 180px;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.v-container {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
width: 100%;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.config-toolbar {
|
||||
padding-right: 0 !important;
|
||||
.config-page-wrap {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.config-toolbar-controls {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-select,
|
||||
.config-select--mobile,
|
||||
.config-search-input {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-manage-mobile {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
|
||||
Reference in New Issue
Block a user