mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
refactor(dashboard): streamline sidebar navigation (#9233)
* refactor(dashboard): move resources to settings * style(dashboard): restore sidebar rail design
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 273 icons */
|
||||
/* Auto-generated MDI subset – 271 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -324,10 +324,6 @@
|
||||
content: "\F0CFD";
|
||||
}
|
||||
|
||||
.mdi-cursor-move::before {
|
||||
content: "\F01BE";
|
||||
}
|
||||
|
||||
.mdi-database::before {
|
||||
content: "\F01BC";
|
||||
}
|
||||
@@ -404,10 +400,6 @@
|
||||
content: "\F0209";
|
||||
}
|
||||
|
||||
.mdi-eye-outline::before {
|
||||
content: "\F06D0";
|
||||
}
|
||||
|
||||
.mdi-eyedropper::before {
|
||||
content: "\F020A";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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": {
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
},
|
||||
"openapi": {
|
||||
"title": "OpenAPI"
|
||||
},
|
||||
"resources": {
|
||||
"title": "О программе"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"changelog": {
|
||||
"subtitle": "Просмотреть изменения текущей и предыдущих версий."
|
||||
},
|
||||
"documentation": {
|
||||
"subtitle": "Открыть официальную документацию AstrBot."
|
||||
},
|
||||
"faq": {
|
||||
"subtitle": "Посмотреть частые вопросы и советы по устранению проблем."
|
||||
},
|
||||
"github": {
|
||||
"subtitle": "Открыть репозиторий AstrBot на GitHub."
|
||||
}
|
||||
},
|
||||
"systemConfig": {
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
},
|
||||
"openapi": {
|
||||
"title": "OpenAPI"
|
||||
},
|
||||
"resources": {
|
||||
"title": "关于"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"changelog": {
|
||||
"subtitle": "查看当前版本和历史版本的更新内容。"
|
||||
},
|
||||
"documentation": {
|
||||
"subtitle": "打开 AstrBot 官方文档。"
|
||||
},
|
||||
"faq": {
|
||||
"subtitle": "查看常见问题与排障说明。"
|
||||
},
|
||||
"github": {
|
||||
"subtitle": "访问 AstrBot GitHub 仓库。"
|
||||
}
|
||||
},
|
||||
"systemConfig": {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { useCustomizerStore } from '@/stores/customizer';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({ item: Object, level: Number });
|
||||
const props = defineProps({ item: Object, level: Number, rail: Boolean });
|
||||
const { t } = useI18n();
|
||||
const customizer = useCustomizerStore();
|
||||
const route = useRoute();
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const lvl = props.level ?? 0;
|
||||
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
|
||||
const indent = props.rail ? '0px' : `${lvl * 24}px`;
|
||||
return { '--indent-padding': indent };
|
||||
});
|
||||
|
||||
@@ -22,7 +20,11 @@ const isItemActive = computed(() => {
|
||||
const [path, hash] = props.item.to.split('#');
|
||||
return route.path === path && route.hash === `#${hash}`;
|
||||
}
|
||||
return route.path === props.item.to;
|
||||
const targetPath = props.item.to.replace(/\/$/, '') || '/';
|
||||
if (targetPath === '/') {
|
||||
return route.path === targetPath;
|
||||
}
|
||||
return route.path === targetPath || route.path.startsWith(`${targetPath}/`);
|
||||
});
|
||||
|
||||
const itemTitle = computed(() => {
|
||||
@@ -33,9 +35,19 @@ const itemTitle = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'rail-group': rail }">
|
||||
<template v-slot:activator="{ props: groupProps }">
|
||||
<v-tooltip v-if="rail" location="right" :text="itemTitle" open-delay="180">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-list-item v-bind="{ ...groupProps, ...tooltipProps }" rounded class="mb-1" color="secondary"
|
||||
:prepend-icon="item.icon" :style="{ '--indent-padding': '0px' }" :aria-label="itemTitle">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ itemTitle }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-list-item v-else v-bind="groupProps" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }">
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ itemTitle }}
|
||||
@@ -45,10 +57,33 @@ const itemTitle = computed(() => {
|
||||
|
||||
<!-- children -->
|
||||
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
|
||||
<NavItem :item="child" :level="(level || 0) + 1" />
|
||||
<NavItem :item="child" :level="(level || 0) + 1" :rail="rail" />
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<v-tooltip v-else-if="rail" location="right" :text="itemTitle" open-delay="180">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-list-item v-bind="tooltipProps" :to="item.type === 'external' ? '' : item.to"
|
||||
:href="item.type === 'external' ? item.to : ''" :active="isItemActive" rounded class="mb-1"
|
||||
color="secondary" :disabled="item.disabled" :target="item.type === 'external' ? '_blank' : ''"
|
||||
:style="itemStyle" :aria-label="itemTitle">
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title style="font-size: 14px;">{{ itemTitle }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
|
||||
{{ item.subCaption }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append v-if="item.chip">
|
||||
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
|
||||
{{ item.chip }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
|
||||
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
@@ -69,10 +104,16 @@ const itemTitle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 在折叠(mini)状态下,分组展开时给整个分组(母项+子项)加边框以便区分 */
|
||||
.group-bordered.v-list-group--open {
|
||||
border: 2px solid rgba(var(--v-theme-borderLight), 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--v-theme-borderLight), 0.04);
|
||||
.rail-group {
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.rail-group.v-list-group--open {
|
||||
background: rgba(var(--v-theme-primary), 0.06);
|
||||
}
|
||||
|
||||
.rail-group.v-list-group--open > .v-list-group__items {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,157 +102,17 @@ 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 = computed(() => !isMobile && customizer.mini_sidebar);
|
||||
if (isMobile) {
|
||||
customizer.Sidebar_drawer = false;
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
customizer.Sidebar_drawer = true;
|
||||
}
|
||||
|
||||
function startSidebarResize(event) {
|
||||
@@ -291,30 +148,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>
|
||||
@@ -326,46 +159,39 @@ function openChangelogDialog() {
|
||||
app
|
||||
class="leftSidebar"
|
||||
:width="sidebarWidth"
|
||||
:rail="customizer.mini_sidebar"
|
||||
:rail="isRailSidebar"
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<div v-if="!isRailSidebar" class="sidebar-brand">
|
||||
<img
|
||||
class="sidebar-brand-logo"
|
||||
src="@/assets/images/plugin_icon.png"
|
||||
alt="AstrBot logo"
|
||||
>
|
||||
<div class="sidebar-brand-copy">
|
||||
<span class="sidebar-brand-title">AstrBot</span>
|
||||
<span class="sidebar-brand-subtitle">WebUI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': isRailSidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
<NavItem :item="item" class="leftPadding" :rail="isRailSidebar" />
|
||||
</template>
|
||||
</v-list>
|
||||
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
|
||||
{{ t('core.navigation.settings') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-note-text-outline"
|
||||
@click="openChangelogDialog">
|
||||
{{ t('core.navigation.changelog') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-book-open-variant"
|
||||
@click="toggleIframe">
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink">
|
||||
{{ t('core.navigation.faq') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
{{ 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>
|
||||
<div class="sidebar-footer">
|
||||
<v-btn class="sidebar-footer-btn" :class="{ 'sidebar-footer-icon-btn': isRailSidebar }" :size="isRailSidebar ? 'default' : 'small'"
|
||||
variant="text" to="/settings"
|
||||
:prepend-icon="isRailSidebar ? undefined : 'mdi-cog'" :aria-label="t('core.navigation.settings')">
|
||||
<v-icon v-if="isRailSidebar" icon="mdi-cog" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
|
||||
v-if="!isRailSidebar && !isMobile && customizer.Sidebar_drawer"
|
||||
class="sidebar-resize-handle"
|
||||
@mousedown="startSidebarResize"
|
||||
:class="{ 'resizing': isResizing }"
|
||||
@@ -373,44 +199,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>
|
||||
@@ -455,4 +243,75 @@ function openChangelogDialog() {
|
||||
.leftSidebar .v-navigation-drawer__content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer {
|
||||
align-items: stretch;
|
||||
padding: 10px 16px 16px !important;
|
||||
border-top: 1px solid rgba(var(--v-theme-borderLight), 0.35);
|
||||
}
|
||||
|
||||
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
min-height: 40px;
|
||||
justify-content: flex-start !important;
|
||||
border-radius: 12px !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.76);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
padding-inline: 12px !important;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn:hover,
|
||||
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn.v-btn--active {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.09);
|
||||
}
|
||||
|
||||
.leftSidebar:not(.v-navigation-drawer--rail) .sidebar-footer-btn :deep(.v-btn__content) {
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 64px;
|
||||
padding: 14px 18px 10px;
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderLight), 0.35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sidebar-brand-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.sidebar-brand-title {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-brand-subtitle {
|
||||
margin-top: 3px;
|
||||
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,6 +65,101 @@
|
||||
.leftPadding {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.listitem {
|
||||
padding: 10px !important;
|
||||
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
max-width: 56px;
|
||||
min-height: 50px;
|
||||
margin-bottom: 8px !important;
|
||||
margin-inline: auto;
|
||||
padding-inline: 0 !important;
|
||||
grid-template-areas: "prepend" !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.rail-group {
|
||||
width: 56px;
|
||||
margin-bottom: 8px;
|
||||
margin-inline: auto;
|
||||
padding: 4px 0 2px;
|
||||
|
||||
.v-list-item {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
max-width: 56px;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__prepend {
|
||||
grid-area: prepend;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0 !important;
|
||||
justify-content: center;
|
||||
margin-inline-end: 0;
|
||||
|
||||
.v-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__spacer,
|
||||
.v-list-item__append,
|
||||
.v-list-item__content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
width: 100%;
|
||||
padding: 12px 10px 18px !important;
|
||||
gap: 8px;
|
||||
|
||||
.sidebar-footer-icon-btn {
|
||||
width: 56px !important;
|
||||
min-width: 56px !important;
|
||||
max-width: 56px !important;
|
||||
height: 52px !important;
|
||||
border-radius: 14px !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.82);
|
||||
margin-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
|
||||
.v-btn__content {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
font-size: 22px;
|
||||
margin: 0 !important;
|
||||
opacity: 0.98;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.v-btn--active {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.09);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1170px) {
|
||||
.mini-sidebar {
|
||||
@@ -122,3 +217,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-navigation-drawer--rail.leftSidebar {
|
||||
.sidebar-container .sidebar-footer {
|
||||
.sidebar-footer-btn.sidebar-footer-icon-btn {
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
|
||||
.v-btn__content {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.leftSidebar {
|
||||
.rail-group,
|
||||
.sidebar-footer-btn,
|
||||
.sidebar-footer-icon-btn {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user