侧边增加栏折叠和滚动条

This commit is contained in:
yinsx
2025-12-13 15:59:07 +08:00
parent 7586d56d4e
commit 58da270437
6 changed files with 184 additions and 15 deletions

View File

@ -17,10 +17,25 @@
<div class="main-content">
<aside class="node-sidebar">
<div v-for="category in nodeCategories" :key="category.key" class="node-section">
<div class="section-title">{{ category.label }}</div>
<div class="node-list">
<div v-for="n in category.nodes" :key="n.type" class="node-item" draggable="true" @dragstart="onDragStart($event, n.type)">
<div class="node-section-list">
<div
v-for="category in nodeCategories"
:key="category.key"
class="node-section"
:class="{ 'is-collapsed': isSectionCollapsed(category.key) }"
>
<button class="section-header" type="button" @click="toggleSection(category.key)">
<span class="section-title">{{ category.label }}</span>
<Icon :icon="isSectionCollapsed(category.key) ? 'mdi:chevron-down' : 'mdi:chevron-up'" width="16" />
</button>
<div class="node-list" v-show="!isSectionCollapsed(category.key)">
<div
v-for="n in category.nodes"
:key="n.type"
class="node-item"
draggable="true"
@dragstart="onDragStart($event, n.type)"
>
<span class="node-icon">
<Icon :icon="n.icon" width="18" />
</span>
@ -28,6 +43,7 @@
</div>
</div>
</div>
</div>
<div class="stats-bar">
<span>{{ stats.nodeCount }} 节点</span>
<span>{{ stats.edgeCount }} 连线</span>
@ -173,6 +189,18 @@ const contextMenu = ref({
})
const paneClickPosition = ref({ x: 0, y: 0 })
const collapsedSections = ref<Record<string, boolean>>(
nodeCategories.reduce((acc, category) => {
acc[category.key] = false
return acc
}, {} as Record<string, boolean>)
)
const toggleSection = (key: string) => {
collapsedSections.value[key] = !isSectionCollapsed(key)
}
const isSectionCollapsed = (key: string) => collapsedSections.value[key] ?? false
const spawnNodeAt = (type: string, screenPoint: { x: number; y: number }) => {
const position = screenToFlowCoordinate({ x: screenPoint.x, y: screenPoint.y })
@ -442,9 +470,27 @@ html, body, #app { height: 100%; font-family: -apple-system, BlinkMacSystemFont,
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.node-section-list {
flex: 1;
overflow-y: auto;
}
.node-section { padding: 16px; border-bottom: 1px solid #f0f0f0; }
.section-title { font-size: 12px; color: #999; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.section-header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background: none;
border: none;
padding: 0;
margin: 0 0 12px;
cursor: pointer;
color: #999;
}
.section-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.node-section.is-collapsed .section-title { color: #bbb; }
.node-list { display: flex; flex-direction: column; gap: 8px; }
.node-item {
display: flex;

View File

@ -1,3 +1,7 @@
:global(.vue-flow__node-dataInputNode) {
:global(.vue-flow__node-dataInputNode) {
min-width: 200px;
}
.data-input-node__file-input {
display: none;
}

View File

@ -1,4 +1,4 @@
<template>
<template>
<div class="data-input-node">
<StandardNode
:icon="definition.icon"
@ -10,18 +10,94 @@
:handles="definition.handles"
:body-items="bodyItems"
:selected="props.selected"
>
<template #body-append>
<div class="node-control">
<span class="node-control__label">Source</span>
<el-select
size="small"
:model-value="props.data.sourceType"
placeholder="Choose source"
@change="handleSourceTypeChange"
>
<el-option
v-for="option in sourceOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<div class="node-control">
<span class="node-control__label">File</span>
<div class="node-control__field">
<el-input
size="small"
:model-value="props.data.filePath"
placeholder="No file selected"
readonly
/>
<el-button size="small" type="primary" link @click="triggerFileDialog">Browse</el-button>
</div>
<input
ref="fileInputRef"
class="data-input-node__file-input"
type="file"
@change="handleFileSelect"
/>
</div>
<div class="node-control__actions">
<el-button size="small" type="success" plain @click="handleSync">
Load Data
</el-button>
</div>
</template>
</StandardNode>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Position } from '@vue-flow/core'
import { useFlowStore } from '@/stores/flow'
import StandardNode from '../shared/StandardNode.vue'
import { useStandardNode } from '../shared/useStandardNode'
import type { BaseNodeProps } from '../shared/types'
import type { DataInputNodeData } from './types'
const props = defineProps<BaseNodeProps<DataInputNodeData>>()
const store = useFlowStore()
const fileInputRef = ref<HTMLInputElement | null>(null)
const sourceOptions = [
{ label: 'File Upload', value: 'file' },
{ label: 'API Endpoint', value: 'api' },
{ label: 'Database', value: 'database' }
]
const handleSourceTypeChange = (value: string) => {
store.updateNodeData(props.id, { sourceType: value })
}
const triggerFileDialog = () => {
fileInputRef.value?.click()
}
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
store.updateNodeData(props.id, { filePath: file.name })
}
target.value = ''
}
const handleSync = () => {
store.updateNodeData(props.id, {
status: 'Preparing',
lastSync: new Date().toLocaleTimeString()
})
}
const { definition, bodyItems, badge, status } = useStandardNode(props, {
icon: 'mdi:database-import',
@ -31,11 +107,12 @@ const { definition, bodyItems, badge, status } = useStandardNode(props, {
handles: {
outputs: [Position.Right]
},
status: ({ data }) => data.status ?? '待运行',
status: ({ data }) => data.status ?? 'Idle',
badge: ({ data }) => data.format ?? '自动检测',
body: ({ data }) => [
{ label: '来源', value: data.sourceType ?? '未配置' },
{ label: '路径', value: data.filePath ?? '待绑定' }
{ label: '路径', value: data.filePath ?? '待绑定' },
{ label: 'Last Sync', value: data.lastSync ?? 'Never' }
]
})
</script>

View File

@ -5,6 +5,7 @@ export interface DataInputNodeData {
filePath?: string
format?: string
status?: string
lastSync?: string
}
export type DataInputNodeProps = BaseNodeProps<DataInputNodeData>

View File

@ -11,7 +11,7 @@
<span v-if="badge" class="standard-node__badge">{{ badge }}</span>
</div>
<div class="standard-node__body">
<slot name="body">
<slot name="body" :items="bodyItems">
<div
v-for="item in bodyItems"
:key="item.label"
@ -20,6 +20,9 @@
<span class="row-label">{{ item.label }}</span>
<span class="row-value">{{ item.value }}</span>
</div>
<div v-if="$slots['body-append']" class="standard-node__controls">
<slot name="body-append" />
</div>
</slot>
</div>
<div class="standard-node__footer">

View File

@ -68,6 +68,44 @@
color: #4a4a4a;
}
.standard-node__controls {
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
gap: 6px;
}
.standard-node__controls .node-control {
display: flex;
flex-direction: column;
gap: 4px;
}
.standard-node__controls .node-control__label {
font-size: 11px;
color: #8c8c8c;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.standard-node__controls .node-control__field {
display: flex;
align-items: center;
gap: 6px;
}
.standard-node__controls .node-control__actions {
display: flex;
justify-content: flex-end;
}
.standard-node__controls :deep(.el-select),
.standard-node__controls :deep(.el-input) {
width: 100%;
}
.standard-node__row {
display: flex;
justify-content: space-between;