Skip to content
Closed
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
7 changes: 6 additions & 1 deletion dashboard/src/i18n/locales/en-US/core/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@
"settings": "Settings",
"documentation": "Documentation",
"github": "GitHub",
"drag": "Drag"
"drag": "Drag",
"groups": {
"platform": "Platform",
"configuration": "Configuration",
"others": "Others"
}
}
7 changes: 6 additions & 1 deletion dashboard/src/i18n/locales/zh-CN/core/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@
"settings": "设置",
"documentation": "官方文档",
"github": "GitHub",
"drag": "拖拽"
"drag": "拖拽",
"groups": {
"platform": "平台",
"configuration": "配置",
"others": "其他"
}
}
44 changes: 43 additions & 1 deletion dashboard/src/layouts/full/vertical-sidebar/NavItem.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
<script setup>
import { useI18n } from '@/i18n/composables';
import { useCustomizerStore } from '@/stores/customizer';
import { computed } from 'vue';

const props = defineProps({ item: Object, level: Number });
const { t } = useI18n();
const customizer = useCustomizerStore();

const itemStyle = computed(() => {
const lvl = props.level ?? 0;
const indent = customizer.mini_sidebar ? '0px' : `${lvl * 24}px`;
return { '--indent-padding': indent };
});
</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"
:style="{ '--indent-padding': '0px' }"
>
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
{{ t(item.title) }}
</v-list-item-title>
</v-list-item>
</template>

<!-- children -->
<template v-for="(child, index) in item.children" :key="index">
<NavItem :item="child" :level="(level || 0) + 1" />
</template>
</v-list-group>

<v-list-item
v-else
:to="item.type === 'external' ? '' : item.to"
:href="item.type === 'external' ? item.to : ''"
rounded
class="mb-1"
color="secondary"
:disabled="item.disabled"
:target="item.type === 'external' ? '_blank' : ''"
:style="itemStyle"
>
<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;">{{ t(item.title) }}</v-list-item-title>
<v-list-item-title style="font-size: 14px; line-height: 1.2; word-break: break-word;">{{ t(item.title) }}</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>
Expand All @@ -35,3 +68,12 @@ const { t } = useI18n();
</template>
</v-list-item>
</template>

<style scoped>
/* 在折叠(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);
}
</style>
102 changes: 89 additions & 13 deletions dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ const sidebarMenu = shallowRef(sidebarItems);

const showIframe = ref(false);

// 默认桌面端 iframe 样式
const sidebarWidth = ref(260);
const minSidebarWidth = 200;
const maxSidebarWidth = 300;
const isResizing = ref(false);

const iframeStyle = ref({
position: 'fixed',
bottom: '16px',
Expand All @@ -29,14 +33,13 @@ const iframeStyle = ref({
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
});

// 如果为移动端,则采用百分比尺寸,并设置初始位置
if (window.innerWidth < 768) {
iframeStyle.value = {
position: 'fixed',
top: '10%',
left: '0%',
width: '100%',
height: '50%',
height: '80%',
minWidth: '300px',
minHeight: '200px',
background: 'white',
Expand All @@ -46,7 +49,6 @@ if (window.innerWidth < 768) {
borderRadius: '12px',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
};
// 移动端默认关闭侧边栏
customizer.Sidebar_drawer = false;
}

Expand Down Expand Up @@ -74,12 +76,10 @@ function openIframeLink(url) {
}
}

// 拖拽相关变量与函数
let offsetX = 0;
let offsetY = 0;
let isDragging = false;

// 辅助函数:限制数值在一定范围内
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
Expand All @@ -91,7 +91,6 @@ function startDrag(clientX, clientY) {
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 });
Expand Down Expand Up @@ -149,6 +148,34 @@ function endDrag() {
document.removeEventListener('touchend', onTouchEnd);
}

function startSidebarResize(event) {
isResizing.value = true;
document.body.style.userSelect = 'none';
document.body.style.cursor = 'ew-resize';

const startX = event.clientX;
const startWidth = sidebarWidth.value;

function onMouseMoveResize(event) {
if (!isResizing.value) return;

const deltaX = event.clientX - startX;
const newWidth = Math.max(minSidebarWidth, Math.min(maxSidebarWidth, startWidth + deltaX));
sidebarWidth.value = newWidth;
}

function onMouseUpResize() {
isResizing.value = false;
document.body.style.userSelect = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onMouseMoveResize);
document.removeEventListener('mouseup', onMouseUpResize);
}

document.addEventListener('mousemove', onMouseMoveResize);
document.addEventListener('mouseup', onMouseUpResize);
}

</script>

<template>
Expand All @@ -159,7 +186,7 @@ function endDrag() {
rail-width="80"
app
class="leftSidebar"
width="220"
:width="sidebarWidth"
:rail="customizer.mini_sidebar"
>
<div class="sidebar-container">
Expand All @@ -180,21 +207,28 @@ function endDrag() {
</v-btn>
</div>
</div>

<div
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
class="sidebar-resize-handle"
@mousedown="startSidebarResize"
:class="{ 'resizing': isResizing }"
>
</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://astrbot.app')"
Expand All @@ -203,7 +237,6 @@ function endDrag() {
>
<v-icon icon="mdi-open-in-new" />
</v-btn>
<!-- 关闭按钮 -->
<v-btn
icon
@click.stop="toggleIframe"
Expand All @@ -214,10 +247,53 @@ function endDrag() {
</v-btn>
</div>
</div>
<!-- iframe 区域 -->
<iframe
src="https://astrbot.app"
style="width: 100%; height: calc(100% - 56px); border: none; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;"
></iframe>
</div>
</template>
</template>

<style scoped>
.sidebar-resize-handle {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background: transparent;
cursor: ew-resize;
user-select: none;
z-index: 1000;
transition: background-color 0.2s ease;
}

.sidebar-resize-handle:hover,
.sidebar-resize-handle.resizing {
background: rgba(var(--v-theme-primary), 0.3);
}

.sidebar-resize-handle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 30px;
background: rgba(var(--v-theme-on-surface), 0.3);
border-radius: 1px;
opacity: 0;
transition: opacity 0.2s ease;
}

.sidebar-resize-handle:hover::before,
.sidebar-resize-handle.resizing::before {
opacity: 1;
}

/* 确保侧边栏容器支持相对定位 */
.leftSidebar .v-navigation-drawer__content {
position: relative;
}
</style>
Loading