Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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": "其他"
}
}
35 changes: 34 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">
<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>
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: 列表项标题中重复的内联样式可以提取到 CSS 类中。

考虑为这些共享样式创建一个 CSS 类,以增强代码可维护性并确保一致的样式。

建议的实现:

    <v-list-item-title class="nav-list-item-title">{{ t(item.title) }}</v-list-item-title>

</code>

<style scoped>
.nav-list-item-title {
  font-size: 14px;
  line-height: 1.2;
  word-break: break-word;
}
</style>

Original comment in English

suggestion: Repeated inline styles for list item titles could be extracted into a CSS class.

Consider creating a CSS class for these shared styles to enhance code maintainability and ensure consistent styling.

Suggested implementation:

    <v-list-item-title class="nav-list-item-title">{{ t(item.title) }}</v-list-item-title>

</code>

<style scoped>
.nav-list-item-title {
  font-size: 14px;
  line-height: 1.2;
  word-break: break-word;
}
</style>

<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
{{ item.subCaption }}
</v-list-item-subtitle>
Expand Down
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
Loading