Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 74 additions & 9 deletions dashboard/src/components/config/AstrBotCoreConfigWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
<v-tab v-for="section in visibleSections" :key="section.key" :value="section.key"
style="font-weight: 1000; font-size: 15px">
{{ tm(metadata[key]['name']) }}
{{ tm(section.value['name']) }}
</v-tab>
</v-tabs>
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
<v-tabs-window-item v-for="section in visibleSections" :key="section.key" :value="section.key">
<v-container fluid>
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
<div v-for="(val2, key2, index2) in section.value['metadata']" :key="key2">
<!-- Support both traditional and JSON selector metadata -->
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
:metadataKey="key2">
<AstrBotConfigV4
:metadata="{ [key2]: section.value['metadata'][key2] }"
:iterable="config_data"
:metadataKey="key2"
:search-keyword="searchKeyword"
>
</AstrBotConfigV4>
</div>
</v-container>
Expand All @@ -31,6 +35,11 @@

</v-tabs-window>
</div>
<v-container v-if="visibleSections.length === 0" fluid class="px-0">
<v-alert type="info" variant="tonal">
{{ tm('search.noResult') }}
</v-alert>
</v-container>
</template>

<script>
Expand All @@ -56,6 +65,10 @@ export default {
readonly: {
type: Boolean,
default: false
},
searchKeyword: {
type: String,
default: ''
}
},
setup() {
Expand All @@ -76,11 +89,63 @@ export default {
},
data() {
return {
tab: 0, // 用于切换配置标签页
tab: null, // 当前激活的配置标签页 key
}
},
computed: {
normalizedSearchKeyword() {
return String(this.searchKeyword || '').trim().toLowerCase();
},
visibleSections() {
if (!this.metadata || typeof this.metadata !== 'object') {
return [];
}
const allSections = Object.entries(this.metadata).map(([key, value]) => ({ key, value }));
if (!this.normalizedSearchKeyword) {
return allSections;
}
return allSections.filter((section) => this.sectionHasSearchMatch(section.value));
}
},
watch: {
visibleSections(newSections) {
const sectionKeys = newSections.map((section) => section.key);
if (!sectionKeys.includes(this.tab)) {
this.tab = sectionKeys[0] ?? null;
}
}
},
mounted() {
const sectionKeys = this.visibleSections.map((section) => section.key);
this.tab = sectionKeys[0] ?? null;
},
methods: {
// 如果需要添加其他方法,可以在这里添加
sectionHasSearchMatch(section) {
const keyword = this.normalizedSearchKeyword;
if (!keyword) {
return true;
}
const sectionMetadata = section?.metadata || {};
return Object.values(sectionMetadata).some((metaItem) => this.metaObjectHasSearchMatch(metaItem, keyword));
},
metaObjectHasSearchMatch(metaObject, keyword) {
if (!metaObject || typeof metaObject !== 'object') {
return false;
}
const target = [
this.tm(metaObject.description || ''),
this.tm(metaObject.hint || ''),
...Object.entries(metaObject.items || {}).flatMap(([itemKey, itemMeta]) => ([
itemKey,
this.tm(itemMeta?.description || ''),
this.tm(itemMeta?.hint || '')
]))
]
.join(' ')
.toLowerCase();

return target.includes(keyword);
}
}
}
</script>
Expand Down Expand Up @@ -112,4 +177,4 @@ export default {
margin-top: 16px;
}
}
</style>
</style>
44 changes: 33 additions & 11 deletions dashboard/src/components/shared/AstrBotConfigV4.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const props = defineProps({
metadataKey: {
type: String,
required: true
},
searchKeyword: {
type: String,
default: ''
}
})

Expand Down Expand Up @@ -124,16 +128,27 @@ function saveEditedContent() {
}

function shouldShowItem(itemMeta, itemKey) {
if (!itemMeta?.condition) {
return true
}
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
const actualValue = getValueBySelector(props.iterable, conditionKey)
if (actualValue !== expectedValue) {
return false
if (itemMeta?.condition) {
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
const actualValue = getValueBySelector(props.iterable, conditionKey)
if (actualValue !== expectedValue) {
return false
}
}
}
return true

const keyword = String(props.searchKeyword || '').trim().toLowerCase()
if (!keyword) {
return true
}

const searchableText = [
itemKey,
translateIfKey(itemMeta?.description || ''),
translateIfKey(itemMeta?.hint || '')
].join(' ').toLowerCase()

return searchableText.includes(keyword)
}

// 检查最外层的 object 是否应该显示
Expand All @@ -148,7 +163,10 @@ function shouldShowSection() {
return false
}
}
return true

const sectionItems = props.metadata?.[props.metadataKey]?.items || {}
const hasVisibleItems = Object.entries(sectionItems).some(([itemKey, itemMeta]) => shouldShowItem(itemMeta, itemKey))
Comment on lines +167 to +168
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 带搜索的分区级可见性在只有 section 元数据匹配时,可能会导致标签页中没有可见内容。

shouldShowSection 中,通过 hasVisibleItems(经由 shouldShowItem,会应用搜索过滤)来做要求,这与 wrapper 中已经在使用的 visibleSections 存在冲突,后者会在 section 元数据和 items 上同时使用 sectionHasSearchMatch。这意味着,一个 section 可能因为其描述/提示文本匹配了关键词而被显示,但如果没有任何 item 匹配,shouldShowSection 仍然返回 false,最终得到的是一个空标签页。为避免这种不一致,你可以: (1) 在 searchKeyword 非空时跳过 hasVisibleItems 检查,并依赖 wrapper 的可见性判断,或者 (2) 将分区级的搜索逻辑移动到当前组件中,让 wrapper 只负责标签页的选择。

Original comment in English

issue (bug_risk): Section-level visibility with search can lead to tabs with no visible content when only the section-level metadata matches.

In shouldShowSection, requiring hasVisibleItems (via shouldShowItem, which applies search filtering) conflicts with the wrapper’s visibleSections, which already uses sectionHasSearchMatch on both section metadata and items. This means a section can be shown because its description/hint matches the keyword, but shouldShowSection still returns false when no items match, yielding an empty tab. To avoid this mismatch, either: (1) bypass the hasVisibleItems check when searchKeyword is non-empty and rely on the wrapper’s visibility decision, or (2) move the section-level search logic into this component and have the wrapper only manage tab selection.

return hasVisibleItems
}

function hasVisibleItemsAfter(items, currentIndex) {
Expand Down Expand Up @@ -436,9 +454,13 @@ function getSpecialSubtype(value) {
}

.property-info,
.type-indicator,
.type-indicator {
padding: 4px 8px;
}

.config-input {
padding: 4px;
padding-left: 24px;
padding-right: 24px;
}
}
</style>
4 changes: 4 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
"normalConfig": "Basic",
"systemConfig": "System"
},
"search": {
"placeholder": "Search config items (key/description/hint)",
"noResult": "No matching config items found"
},
"configManagement": {
"title": "Configuration Management",
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
"normalConfig": "普通",
"systemConfig": "系统"
},
"search": {
"placeholder": "搜索配置项(字段名/描述/提示)",
"noResult": "未找到匹配的配置项"
},
"configManagement": {
"title": "配置文件管理",
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
Expand Down
34 changes: 31 additions & 3 deletions dashboard/src/views/ConfigPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
style="display: flex; flex-direction: column; align-items: start;">

<div class="d-flex flex-row pr-4"
<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="d-flex flex-row align-center" style="gap: 12px;">
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
<div class="config-toolbar-controls d-flex flex-row align-center" style="gap: 12px;">
<v-select class="config-select" style="min-width: 130px;" v-model="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>
<v-text-field
class="config-search-input"
v-model="configSearchKeyword"
prepend-inner-icon="mdi-magnify"
:label="tm('search.placeholder')"
hide-details
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>
Expand All @@ -34,6 +45,7 @@
<AstrBotCoreConfigWrapper
:metadata="metadata"
:config_data="config_data"
:search-keyword="configSearchKeyword"
/>

<v-tooltip :text="tm('actions.save')" location="left">
Expand Down Expand Up @@ -290,6 +302,7 @@ export default {

// 配置类型切换
configType: 'normal', // 'normal' 或 'system'
configSearchKeyword: '',

// 系统配置开关
isSystemConfig: false,
Expand Down Expand Up @@ -702,6 +715,21 @@ export default {
.config-panel {
width: 100%;
}

.config-toolbar {
padding-right: 0 !important;
}

.config-toolbar-controls {
width: 100%;
flex-wrap: wrap;
}

.config-select,
.config-search-input {
width: 100%;
min-width: 0 !important;
}
}

/* 测试聊天抽屉样式 */
Expand Down