refactor: enhance settings section with resources and update sidebar styles

This commit is contained in:
Fridemn
2026-07-07 16:10:26 +08:00
parent 19962523ac
commit 2e57f86bc5
5 changed files with 147 additions and 245 deletions

View File

@@ -20,6 +20,23 @@
},
"openapi": {
"title": "OpenAPI"
},
"resources": {
"title": "About"
}
},
"resources": {
"changelog": {
"subtitle": "Review changes in the current and previous versions."
},
"documentation": {
"subtitle": "Open the official AstrBot documentation."
},
"faq": {
"subtitle": "Read common questions and troubleshooting notes."
},
"github": {
"subtitle": "Visit the AstrBot GitHub repository."
}
},
"systemConfig": {

View File

@@ -20,6 +20,23 @@
},
"openapi": {
"title": "OpenAPI"
},
"resources": {
"title": "О программе"
}
},
"resources": {
"changelog": {
"subtitle": "Просмотреть изменения текущей и предыдущих версий."
},
"documentation": {
"subtitle": "Открыть официальную документацию AstrBot."
},
"faq": {
"subtitle": "Посмотреть частые вопросы и советы по устранению проблем."
},
"github": {
"subtitle": "Открыть репозиторий AstrBot на GitHub."
}
},
"systemConfig": {

View File

@@ -20,6 +20,23 @@
},
"openapi": {
"title": "OpenAPI"
},
"resources": {
"title": "关于"
}
},
"resources": {
"changelog": {
"subtitle": "查看当前版本和历史版本的更新内容。"
},
"documentation": {
"subtitle": "打开 AstrBot 官方文档。"
},
"faq": {
"subtitle": "查看常见问题与排障说明。"
},
"github": {
"subtitle": "访问 AstrBot GitHub 仓库。"
}
},
"systemConfig": {

View File

@@ -1,18 +1,15 @@
<script setup>
import { ref, shallowRef, computed, onMounted, onUnmounted, watch } from 'vue';
import { useTheme } from 'vuetify';
import { useCustomizerStore } from '../../../stores/customizer';
import { useI18n } from '@/i18n/composables';
import sidebarItems, { MORE_GROUP_KEY } from './sidebarItem';
import NavItem from './NavItem.vue';
import { applySidebarCustomization } from '@/utils/sidebarCustomization';
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
import { usePluginSidebarItems } from '@/composables/usePluginSidebarItems';
const { t, locale } = useI18n();
const { t } = useI18n();
const customizer = useCustomizerStore();
const theme = useTheme();
const { pluginItems } = usePluginSidebarItems();
function buildSidebarMenu() {
@@ -105,23 +102,11 @@ onUnmounted(() => {
window.removeEventListener('sidebar-customization-changed', handleCustomEvent);
});
const showIframe = ref(false);
const starCount = ref(null);
// 更新日志对话框
const changelogDialog = ref(false);
const sidebarWidth = ref(235);
const minSidebarWidth = 200;
const maxSidebarWidth = 300;
const isResizing = ref(false);
const isDark = computed(() => customizer.uiTheme === 'PurpleThemeDark');
const themeColors = computed(() => theme.current.value.colors);
const iframeBackground = computed(() => isDark.value ? themeColors.value.surface || 'white' : 'white');
const dragHeaderBackground = computed(() => isDark.value ? themeColors.value.mcpCardBg || themeColors.value.surface || 'white' : '#f0f0f0');
const frameBorder = computed(() => `1px solid ${isDark.value ? (themeColors.value.borderLight || '#ccc') : '#ccc'}`);
const isMobile = window.innerWidth < 768;
const isRailSidebar = !isMobile;
if (isMobile) {
@@ -130,137 +115,10 @@ if (isMobile) {
customizer.SET_MINI_SIDEBAR(true);
}
const dragPos = ref({ left: '', top: '' });
const iframeStyle = computed(() => {
const base = isMobile
? { position: 'fixed', top: '10%', left: '0%', width: '100%', height: '80%', zIndex: '1002' }
: { position: 'fixed', bottom: '16px', right: '16px', width: '490px', height: '640px', zIndex: '10000000' };
const pos = dragPos.value.left ? { left: dragPos.value.left, top: dragPos.value.top, bottom: 'auto', right: 'auto' } : {};
return {
...base,
...pos,
minWidth: '300px',
minHeight: '200px',
background: iframeBackground.value,
resize: 'both',
overflow: 'auto',
borderRadius: '12px',
boxShadow: isDark.value ? '0px 4px 16px rgba(0, 0, 0, 0.5)' : '0px 4px 12px rgba(0, 0, 0, 0.1)',
};
});
const iframeInnerStyle = computed(() => ({
width: '100%',
height: 'calc(100% - 66px)',
border: 'none',
borderBottomLeftRadius: '12px',
borderBottomRightRadius: '12px',
filter: isDark.value ? 'invert(0.88) hue-rotate(180deg)' : 'none',
}));
const dragHeaderStyle = computed(() => ({
width: '100%',
padding: '8px',
background: dragHeaderBackground.value,
borderBottom: frameBorder.value,
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'move'
}));
function toggleIframe() {
showIframe.value = !showIframe.value;
}
function openIframeLink(url) {
if (typeof window !== 'undefined') {
let url_ = url || "https://docs.astrbot.app";
window.open(url_, "_blank");
}
}
function openFaqLink() {
const faqUrl = locale.value === 'en-US'
? 'https://docs.astrbot.app/en/faq.html'
: 'https://docs.astrbot.app/faq.html';
openIframeLink(faqUrl);
}
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function startDrag(clientX, clientY) {
isDragging = true;
const dm = document.getElementById('draggable-iframe');
const rect = dm.getBoundingClientRect();
offsetX = clientX - rect.left;
offsetY = clientY - rect.top;
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
}
function onMouseDown(event) {
startDrag(event.clientX, event.clientY);
}
function onMouseMove(event) {
if (isDragging) {
moveAt(event.clientX, event.clientY);
}
}
function onMouseUp() {
endDrag();
}
function onTouchStart(event) {
if (event.touches.length === 1) {
const touch = event.touches[0];
startDrag(touch.clientX, touch.clientY);
}
}
function onTouchMove(event) {
if (isDragging && event.touches.length === 1) {
event.preventDefault();
const touch = event.touches[0];
moveAt(touch.clientX, touch.clientY);
}
}
function onTouchEnd() {
endDrag();
}
function moveAt(clientX, clientY) {
const dm = document.getElementById('draggable-iframe');
const newLeft = clamp(clientX - offsetX, 0, window.innerWidth - dm.offsetWidth);
const newTop = clamp(clientY - offsetY, 0, window.innerHeight - dm.offsetHeight);
// Sync dragged position to reactive variable
dragPos.value = { left: newLeft + 'px', top: newTop + 'px' };
}
function endDrag() {
isDragging = false;
document.body.style.userSelect = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
}
function startSidebarResize(event) {
isResizing.value = true;
document.body.style.userSelect = 'none';
@@ -294,30 +152,6 @@ function startSidebarResize(event) {
document.addEventListener('mouseup', onMouseUpResize);
}
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
async function fetchStarCount() {
try {
const response = await fetch('https://cloud.astrbot.app/api/v1/github/repo-info');
const data = await response.json();
if (data.data && data.data.stargazers_count) {
starCount.value = data.data.stargazers_count;
console.debug('Fetched star count:', starCount.value);
}
} catch (error) {
console.debug('Failed to fetch star count:', error);
}
}
fetchStarCount();
// 打开更新日志对话框
function openChangelogDialog() {
changelogDialog.value = true;
}
</script>
<template>
@@ -345,43 +179,6 @@ function openChangelogDialog() {
<template v-else>{{ t('core.navigation.settings') }}</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.settings')" open-delay="180" />
</v-btn>
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
variant="text" :prepend-icon="isRailSidebar ? undefined : 'mdi-note-text-outline'"
:aria-label="t('core.navigation.changelog')" @click="openChangelogDialog">
<v-icon v-if="isRailSidebar" icon="mdi-note-text-outline" />
<template v-else>{{ t('core.navigation.changelog') }}</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.changelog')" open-delay="180" />
</v-btn>
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
variant="text" :prepend-icon="isRailSidebar ? undefined : 'mdi-book-open-variant'"
:aria-label="t('core.navigation.documentation')" @click="toggleIframe">
<v-icon v-if="isRailSidebar" icon="mdi-book-open-variant" />
<template v-else>{{ t('core.navigation.documentation') }}</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.documentation')" open-delay="180" />
</v-btn>
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
variant="text" :prepend-icon="isRailSidebar ? undefined : 'mdi-frequently-asked-questions'"
:aria-label="t('core.navigation.faq')" @click="openFaqLink">
<v-icon v-if="isRailSidebar" icon="mdi-frequently-asked-questions" />
<template v-else>{{ t('core.navigation.faq') }}</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.faq')" open-delay="180" />
</v-btn>
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
variant="text" :prepend-icon="isRailSidebar ? undefined : 'mdi-github'" :aria-label="t('core.navigation.github')"
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
<v-icon v-if="isRailSidebar" icon="mdi-github" />
<template v-else>
{{ t('core.navigation.github') }}
<v-chip
v-if="starCount"
size="x-small"
variant="outlined"
class="ml-2"
style="font-weight: normal;"
>{{ formatNumber(starCount) }}</v-chip>
</template>
<v-tooltip v-if="isRailSidebar" activator="parent" location="right" :text="t('core.navigation.github')" open-delay="180" />
</v-btn>
</div>
</div>
@@ -394,44 +191,6 @@ function openChangelogDialog() {
</div>
</v-navigation-drawer>
<div
v-if="showIframe"
id="draggable-iframe"
:style="iframeStyle"
>
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
<div style="display: flex; align-items: center;">
<v-icon icon="mdi-cursor-move" />
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
</div>
<div style="display: flex; gap: 8px;">
<v-btn
icon
@click.stop="openIframeLink('https://docs.astrbot.app')"
@mousedown.stop
:style="{ borderRadius: '8px', border: frameBorder }"
>
<v-icon icon="mdi-open-in-new" />
</v-btn>
<v-btn
icon
@click.stop="toggleIframe"
@mousedown.stop
:style="{ borderRadius: '8px', border: frameBorder }"
>
<v-icon icon="mdi-close" />
</v-btn>
</div>
</div>
<iframe
src="https://docs.astrbot.app"
:style="iframeInnerStyle"
></iframe>
</div>
<!-- 更新日志对话框 -->
<ChangelogDialog v-model="changelogDialog" />
</template>
<style scoped>

View File

@@ -11,7 +11,10 @@
:key="item.id"
type="button"
class="settings-nav__item"
:class="{ 'settings-nav__item--active': activeSettingsSection === item.id }"
:class="{
'settings-nav__item--active': activeSettingsSection === item.id,
'settings-nav__item--divider': item.dividerBefore
}"
:aria-pressed="activeSettingsSection === item.id"
@click="activeSettingsSection = item.id"
>
@@ -395,12 +398,39 @@
</div>
</div>
</section>
<section id="settings-resources" class="settings-section" v-show="activeSettingsSection === 'resources'">
<div class="settings-section__heading">
<div class="settings-section__title">{{ tm('sections.resources.title') }}</div>
</div>
<div class="settings-section__content">
<div class="settings-list-card">
<div
v-for="item in resourceItems"
:key="item.key"
class="settings-item"
>
<div class="settings-item__label">
<div class="settings-item__title">{{ item.title }}</div>
<div class="settings-item__subtitle">{{ item.subtitle }}</div>
</div>
<div class="settings-item__control">
<v-btn variant="tonal" @click="item.action()">
<v-icon class="mr-2">{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
</div>
<WaitingForRestart ref="wfr" />
<BackupDialog ref="backupDialog" />
<ChangelogDialog v-model="changelogDialog" />
<DashboardTwoFactorDialog
v-model="configSave2faDialogVisible"
:error-message="configSave2faError"
@@ -421,9 +451,10 @@ import SidebarCustomizer from '@/components/shared/SidebarCustomizer.vue';
import BackupDialog from '@/components/shared/BackupDialog.vue';
import StorageCleanupPanel from '@/components/shared/StorageCleanupPanel.vue';
import DashboardTwoFactorDialog from '@/components/shared/DashboardTwoFactorDialog.vue';
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
import { restartAstrBot as restartAstrBotRuntime } from '@/utils/restartAstrBot';
import { copyToClipboard } from '@/utils/clipboard';
import { useModuleI18n } from '@/i18n/composables';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
import { PurpleTheme } from '@/theme/LightTheme';
import { useToastStore } from '@/stores/toast';
@@ -431,6 +462,7 @@ import { askForConfirmation, useConfirmDialog } from '@/utils/confirmDialog';
const { tm } = useModuleI18n('features/settings');
const { tm: tmMeta } = useModuleI18n('features/config-metadata');
const { t, locale } = useI18n();
const toastStore = useToastStore();
const confirmDialog = useConfirmDialog();
const theme = useTheme();
@@ -497,6 +529,7 @@ const configSave2faRotationHint = ref('');
const configSavePendingData = ref(null);
const systemConfigAutoSaveTimer = ref(null);
const activeSettingsSection = ref('general');
const changelogDialog = ref(false);
const apiKeyExpiryOptions = computed(() => [
{ title: tm('apiKey.expiryOptions.day1'), value: 1 },
@@ -526,7 +559,50 @@ const settingsNavItems = computed(() => [
{ id: 'network', label: tm('sections.network.title'), icon: 'mdi mdi-lan-connect' },
{ id: 'security', label: tm('sections.security.title'), icon: 'mdi mdi-shield-lock-outline' },
{ id: 'maintenance', label: tm('sections.maintenance.title'), icon: 'mdi mdi-tools' },
{ id: 'openapi', label: tm('sections.openapi.title'), icon: 'mdi mdi-api' }
{ id: 'openapi', label: tm('sections.openapi.title'), icon: 'mdi mdi-api' },
{ id: 'resources', label: tm('sections.resources.title'), icon: 'mdi mdi-information-outline', dividerBefore: true }
]);
const openExternalLink = (url) => {
if (typeof window === 'undefined') return;
window.open(url, '_blank', 'noopener,noreferrer');
};
const openFaqLink = () => {
openExternalLink(locale.value === 'en-US'
? 'https://docs.astrbot.app/en/faq.html'
: 'https://docs.astrbot.app/faq.html');
};
const resourceItems = computed(() => [
{
key: 'changelog',
title: t('core.navigation.changelog'),
subtitle: tm('resources.changelog.subtitle'),
icon: 'mdi-note-text-outline',
action: () => { changelogDialog.value = true; }
},
{
key: 'documentation',
title: t('core.navigation.documentation'),
subtitle: tm('resources.documentation.subtitle'),
icon: 'mdi-book-open-variant',
action: () => openExternalLink('https://docs.astrbot.app')
},
{
key: 'faq',
title: t('core.navigation.faq'),
subtitle: tm('resources.faq.subtitle'),
icon: 'mdi-frequently-asked-questions',
action: openFaqLink
},
{
key: 'github',
title: t('core.navigation.github'),
subtitle: tm('resources.github.subtitle'),
icon: 'mdi-github',
action: () => openExternalLink('https://github.com/AstrBotDevs/AstrBot')
}
]);
const configIncludedScopes = ['bot', 'provider'];
@@ -916,6 +992,8 @@ onMounted(async () => {
activeSettingsSection.value = 'maintenance';
} else if (hash.includes('settings-openapi')) {
activeSettingsSection.value = 'openapi';
} else if (hash.includes('settings-resources')) {
activeSettingsSection.value = 'resources';
}
});
@@ -995,6 +1073,20 @@ onUnmounted(() => {
color: rgb(var(--v-theme-on-surface));
}
.settings-nav__item--divider {
margin-top: 13px;
}
.settings-nav__item--divider::after {
position: absolute;
top: -9px;
right: 0;
left: 0;
height: 1px;
background: var(--settings-divider);
content: "";
}
.settings-nav__item--active {
background: rgba(var(--v-theme-on-surface), 0.07);
color: rgb(var(--v-theme-on-surface));