1
This commit is contained in:
@@ -24,7 +24,63 @@
|
|||||||
"id": "mxfx11j",
|
"id": "mxfx11j",
|
||||||
"title": "树形展示器",
|
"title": "树形展示器",
|
||||||
"content": "新窗口内容",
|
"content": "新窗口内容",
|
||||||
"materialId": "TreeViewer"
|
"materialId": "TreeViewer",
|
||||||
|
"materialState": {
|
||||||
|
"treeData": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"label": "项目根目录",
|
||||||
|
"expanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1-1",
|
||||||
|
"label": "src",
|
||||||
|
"expanded": true,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1-1-2",
|
||||||
|
"label": "stores"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-4",
|
||||||
|
"label": "App.vue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-5",
|
||||||
|
"label": "main.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-1",
|
||||||
|
"label": "components"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-3",
|
||||||
|
"label": "types"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-2",
|
||||||
|
"label": "public",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1-3",
|
||||||
|
"label": "package.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-2-1",
|
||||||
|
"label": "favicon.ico"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-4",
|
||||||
|
"label": "vite.config.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jln5iq9",
|
"id": "jln5iq9",
|
||||||
@@ -33,31 +89,56 @@
|
|||||||
"materialId": "TestWidget2"
|
"materialId": "TestWidget2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "2emt1si"
|
"activeTabId": "mxfx11j"
|
||||||
},
|
},
|
||||||
"rightPanel": {
|
"rightPanel": {
|
||||||
"id": "right",
|
"id": "right",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
|
||||||
"id": "ojaw0e3",
|
|
||||||
"title": "新窗口 3",
|
|
||||||
"content": "新窗口内容"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "vrh9bl2",
|
"id": "vrh9bl2",
|
||||||
"title": "数据表格",
|
"title": "数据表格",
|
||||||
"content": "新窗口内容",
|
"content": "新窗口内容",
|
||||||
"materialId": "DataTable"
|
"materialId": "DataTable",
|
||||||
},
|
"materialState": {
|
||||||
{
|
"data": [
|
||||||
"id": "y2iwzgl",
|
{
|
||||||
"title": "测试组件A",
|
"property": "项目名称",
|
||||||
"content": "新窗口内容",
|
"value": "1111"
|
||||||
"materialId": "TestWidget1"
|
},
|
||||||
|
{
|
||||||
|
"property": "框架",
|
||||||
|
"value": "9999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "语言",
|
||||||
|
"value": "TypeScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "构建工具",
|
||||||
|
"value": "Vite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "状态管理",
|
||||||
|
"value": "Pinia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "版本",
|
||||||
|
"value": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "作者",
|
||||||
|
"value": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "许可证",
|
||||||
|
"value": "MIT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "ojaw0e3"
|
"activeTabId": "vrh9bl2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-20T12:28:20.283Z"
|
"lastUpdated": "2025-12-20T12:43:09.638Z"
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,20 @@ const activeMaterialComponent = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取当前物料组件的状态
|
||||||
|
const activeMaterialState = computed(() => {
|
||||||
|
return activeTab.value?.materialState
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理物料组件状态更新
|
||||||
|
const handleStateUpdate = (newState: Record<string, any>) => {
|
||||||
|
if (activeTab.value) {
|
||||||
|
activeTab.value.materialState = newState
|
||||||
|
// 触发保存
|
||||||
|
panelStore.saveConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleTabClick = (tabId: string) => {
|
const handleTabClick = (tabId: string) => {
|
||||||
panelStore.setActiveTab(props.panel.id, tabId)
|
panelStore.setActiveTab(props.panel.id, tabId)
|
||||||
}
|
}
|
||||||
@@ -69,7 +83,9 @@ const handleCloseTab = (tabId: string, event: Event) => {
|
|||||||
<component
|
<component
|
||||||
v-if="activeMaterialComponent"
|
v-if="activeMaterialComponent"
|
||||||
:is="activeMaterialComponent"
|
:is="activeMaterialComponent"
|
||||||
|
:materialState="activeMaterialState"
|
||||||
class="material-wrapper"
|
class="material-wrapper"
|
||||||
|
@update:state="handleStateUpdate"
|
||||||
/>
|
/>
|
||||||
<!-- 普通内容 -->
|
<!-- 普通内容 -->
|
||||||
<div v-else-if="activeTab" class="content-wrapper">
|
<div v-else-if="activeTab" class="content-wrapper">
|
||||||
|
|||||||
@@ -1,29 +1,108 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
|
interface TableRow {
|
||||||
|
property: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
materialProps?: Record<string, any>
|
materialProps?: Record<string, any>
|
||||||
|
materialState?: Record<string, any>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mergedProps = { ...config.props, ...props.materialProps }
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:state', state: Record<string, any>): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 初始化表格数据
|
||||||
|
const initTableData = (): TableRow[] => {
|
||||||
|
if (props.materialState?.data) {
|
||||||
|
return JSON.parse(JSON.stringify(props.materialState.data))
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(config.props.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableData = ref<TableRow[]>(initTableData())
|
||||||
|
const columns = config.props.columns
|
||||||
|
|
||||||
|
// 编辑状态
|
||||||
|
const editingCell = ref<{ row: number, field: 'property' | 'value' } | null>(null)
|
||||||
|
const editValue = ref('')
|
||||||
|
|
||||||
|
// 开始编辑
|
||||||
|
const startEdit = (rowIndex: number, field: 'property' | 'value') => {
|
||||||
|
editingCell.value = { row: rowIndex, field }
|
||||||
|
editValue.value = tableData.value[rowIndex][field]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成编辑
|
||||||
|
const finishEdit = () => {
|
||||||
|
if (editingCell.value) {
|
||||||
|
const { row, field } = editingCell.value
|
||||||
|
tableData.value[row][field] = editValue.value
|
||||||
|
editingCell.value = null
|
||||||
|
emitStateUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const cancelEdit = () => {
|
||||||
|
editingCell.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否正在编辑
|
||||||
|
const isEditing = (rowIndex: number, field: 'property' | 'value') => {
|
||||||
|
return editingCell.value?.row === rowIndex && editingCell.value?.field === field
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发状态更新
|
||||||
|
const emitStateUpdate = () => {
|
||||||
|
emit('update:state', { data: tableData.value })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="data-table">
|
<div class="data-table">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<span class="title">{{ config.name }}</span>
|
||||||
|
<span class="hint">双击单元格编辑</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-body">
|
<div class="table-body">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="col in mergedProps.columns" :key="col">{{ col }}</th>
|
<th v-for="col in columns" :key="col">{{ col }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, index) in mergedProps.data" :key="index">
|
<tr v-for="(row, index) in tableData" :key="index">
|
||||||
<td>{{ row.property }}</td>
|
<td @dblclick="startEdit(index, 'property')">
|
||||||
<td>{{ row.value }}</td>
|
<input
|
||||||
|
v-if="isEditing(index, 'property')"
|
||||||
|
v-model="editValue"
|
||||||
|
class="edit-input"
|
||||||
|
@blur="finishEdit"
|
||||||
|
@keyup.enter="finishEdit"
|
||||||
|
@keyup.escape="cancelEdit"
|
||||||
|
ref="editInput"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<span v-else class="cell-value">{{ row.property }}</span>
|
||||||
|
</td>
|
||||||
|
<td @dblclick="startEdit(index, 'value')">
|
||||||
|
<input
|
||||||
|
v-if="isEditing(index, 'value')"
|
||||||
|
v-model="editValue"
|
||||||
|
class="edit-input"
|
||||||
|
@blur="finishEdit"
|
||||||
|
@keyup.enter="finishEdit"
|
||||||
|
@keyup.escape="cancelEdit"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<span v-else class="cell-value">{{ row.value }}</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -43,6 +122,9 @@ const mergedProps = { ...config.props, ...props.materialProps }
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
border-bottom: 1px solid #3c3c3c;
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -51,6 +133,11 @@ const mergedProps = { ...config.props, ...props.materialProps }
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-body {
|
.table-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -78,9 +165,36 @@ const mergedProps = { ...config.props, ...props.materialProps }
|
|||||||
|
|
||||||
.table td {
|
.table td {
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table tbody tr:hover {
|
.table tbody tr:hover {
|
||||||
background: #2a2d2e;
|
background: #2a2d2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-value {
|
||||||
|
display: block;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: 1px solid #007acc;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-input:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
materialProps?: Record<string, any>
|
materialProps?: Record<string, any>
|
||||||
|
materialState?: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:state', state: Record<string, any>): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mergedProps = { ...config.props, ...props.materialProps }
|
const mergedProps = { ...config.props, ...props.materialProps }
|
||||||
const textContent = ref(mergedProps.defaultValue)
|
|
||||||
|
// 初始化文本内容
|
||||||
|
const initContent = (): string => {
|
||||||
|
if (props.materialState?.content !== undefined) {
|
||||||
|
return props.materialState.content
|
||||||
|
}
|
||||||
|
return mergedProps.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = ref(initContent())
|
||||||
|
|
||||||
|
// 监听内容变化,触发保存
|
||||||
|
let saveTimer: number | null = null
|
||||||
|
const debouncedSave = () => {
|
||||||
|
if (saveTimer) {
|
||||||
|
clearTimeout(saveTimer)
|
||||||
|
}
|
||||||
|
saveTimer = window.setTimeout(() => {
|
||||||
|
emit('update:state', { content: textContent.value })
|
||||||
|
}, 500) // 500ms 防抖,避免频繁保存
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(textContent, () => {
|
||||||
|
debouncedSave()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-editor">
|
<div class="text-editor">
|
||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<span class="title">{{ config.name }}</span>
|
||||||
|
<span class="hint">自动保存</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -38,6 +68,9 @@ const textContent = ref(mergedProps.defaultValue)
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
border-bottom: 1px solid #3c3c3c;
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -46,6 +79,11 @@ const textContent = ref(mergedProps.defaultValue)
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-body {
|
.editor-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
interface TreeNode {
|
interface TreeNode {
|
||||||
@@ -11,10 +12,22 @@ interface TreeNode {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
materialProps?: Record<string, any>
|
materialProps?: Record<string, any>
|
||||||
|
materialState?: Record<string, any>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const mergedProps = { ...config.props, ...props.materialProps }
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:state', state: Record<string, any>): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 初始化树数据
|
||||||
|
const initTreeData = (): TreeNode[] => {
|
||||||
|
if (props.materialState?.treeData) {
|
||||||
|
return JSON.parse(JSON.stringify(props.materialState.treeData))
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(config.props.treeData))
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = ref<TreeNode[]>(initTreeData())
|
||||||
const expandedNodes = ref<Set<string>>(new Set())
|
const expandedNodes = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// 初始化展开状态
|
// 初始化展开状态
|
||||||
@@ -28,9 +41,18 @@ const initExpanded = (nodes: TreeNode[]) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
initExpanded(mergedProps.treeData)
|
|
||||||
|
|
||||||
const toggleNode = (nodeId: string) => {
|
onMounted(() => {
|
||||||
|
initExpanded(treeData.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听数据变化,触发保存
|
||||||
|
const emitStateUpdate = () => {
|
||||||
|
emit('update:state', { treeData: treeData.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNode = (nodeId: string, event: Event) => {
|
||||||
|
event.stopPropagation()
|
||||||
if (expandedNodes.value.has(nodeId)) {
|
if (expandedNodes.value.has(nodeId)) {
|
||||||
expandedNodes.value.delete(nodeId)
|
expandedNodes.value.delete(nodeId)
|
||||||
} else {
|
} else {
|
||||||
@@ -39,55 +61,101 @@ const toggleNode = (nodeId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
||||||
|
|
||||||
|
// 拖拽结束处理
|
||||||
|
const onDragEnd = () => {
|
||||||
|
emitStateUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保节点有 children 数组
|
||||||
|
const ensureChildren = (node: TreeNode) => {
|
||||||
|
if (!node.children) {
|
||||||
|
node.children = []
|
||||||
|
}
|
||||||
|
return node.children
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tree-viewer">
|
<div class="tree-viewer">
|
||||||
<div class="viewer-header">
|
<div class="viewer-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<span class="title">{{ config.name }}</span>
|
||||||
|
<span class="hint">可跨级拖拽</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer-body">
|
<div class="viewer-body">
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<template v-for="node in mergedProps.treeData" :key="node.id">
|
<!-- 第一层 -->
|
||||||
<div class="tree-node-wrapper">
|
<draggable
|
||||||
<div
|
:list="treeData"
|
||||||
class="tree-node"
|
group="tree-nodes"
|
||||||
@click="toggleNode(node.id)"
|
item-key="id"
|
||||||
>
|
:animation="150"
|
||||||
<span class="expand-icon" v-if="node.children?.length">
|
ghost-class="node-ghost"
|
||||||
{{ isExpanded(node.id) ? '▼' : '▶' }}
|
@end="onDragEnd"
|
||||||
</span>
|
>
|
||||||
<span class="expand-icon empty" v-else></span>
|
<template #item="{ element: node }">
|
||||||
<span class="node-label">{{ node.label }}</span>
|
<div class="tree-node-wrapper">
|
||||||
</div>
|
<div class="tree-node">
|
||||||
<div class="tree-children" v-if="node.children && isExpanded(node.id)">
|
<span
|
||||||
<template v-for="child in node.children" :key="child.id">
|
class="expand-icon"
|
||||||
<div class="tree-node-wrapper">
|
:class="{ empty: !node.children?.length }"
|
||||||
<div
|
@click="toggleNode(node.id, $event)"
|
||||||
class="tree-node level-1"
|
>
|
||||||
@click="toggleNode(child.id)"
|
{{ node.children?.length ? (isExpanded(node.id) ? '▼' : '▶') : '' }}
|
||||||
>
|
</span>
|
||||||
<span class="expand-icon" v-if="child.children?.length">
|
<span class="node-label">{{ node.label }}</span>
|
||||||
{{ isExpanded(child.id) ? '▼' : '▶' }}
|
</div>
|
||||||
</span>
|
|
||||||
<span class="expand-icon empty" v-else></span>
|
<!-- 第二层 -->
|
||||||
<span class="node-label">{{ child.label }}</span>
|
<div class="tree-children" v-if="isExpanded(node.id)">
|
||||||
</div>
|
<draggable
|
||||||
<div class="tree-children" v-if="child.children && isExpanded(child.id)">
|
:list="ensureChildren(node)"
|
||||||
<div
|
group="tree-nodes"
|
||||||
v-for="subChild in child.children"
|
item-key="id"
|
||||||
:key="subChild.id"
|
:animation="150"
|
||||||
class="tree-node level-2"
|
ghost-class="node-ghost"
|
||||||
>
|
@end="onDragEnd"
|
||||||
<span class="expand-icon empty"></span>
|
>
|
||||||
<span class="node-label">{{ subChild.label }}</span>
|
<template #item="{ element: child }">
|
||||||
|
<div class="tree-node-wrapper">
|
||||||
|
<div class="tree-node level-1">
|
||||||
|
<span
|
||||||
|
class="expand-icon"
|
||||||
|
:class="{ empty: !child.children?.length }"
|
||||||
|
@click="toggleNode(child.id, $event)"
|
||||||
|
>
|
||||||
|
{{ child.children?.length ? (isExpanded(child.id) ? '▼' : '▶') : '' }}
|
||||||
|
</span>
|
||||||
|
<span class="node-label">{{ child.label }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三层 -->
|
||||||
|
<div class="tree-children" v-if="isExpanded(child.id)">
|
||||||
|
<draggable
|
||||||
|
:list="ensureChildren(child)"
|
||||||
|
group="tree-nodes"
|
||||||
|
item-key="id"
|
||||||
|
:animation="150"
|
||||||
|
ghost-class="node-ghost"
|
||||||
|
@end="onDragEnd"
|
||||||
|
>
|
||||||
|
<template #item="{ element: subChild }">
|
||||||
|
<div class="tree-node-wrapper">
|
||||||
|
<div class="tree-node level-2">
|
||||||
|
<span class="expand-icon empty"></span>
|
||||||
|
<span class="node-label">{{ subChild.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</draggable>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +173,9 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
border-bottom: 1px solid #3c3c3c;
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -113,6 +184,11 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.viewer-body {
|
.viewer-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -123,13 +199,22 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-node-wrapper {
|
||||||
|
margin: 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node:hover {
|
.tree-node:hover {
|
||||||
@@ -146,20 +231,47 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
|
|||||||
|
|
||||||
.expand-icon {
|
.expand-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #888888;
|
color: #888888;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon:hover:not(.empty) {
|
||||||
|
background: #3c3c3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-icon.empty {
|
.expand-icon.empty {
|
||||||
visibility: hidden;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-label {
|
.node-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-children {
|
.tree-children {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
min-height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽样式 */
|
||||||
|
.node-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
background: #094771;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-ghost .tree-node {
|
||||||
|
background: #094771;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-chosen > .tree-node {
|
||||||
|
background: #094771;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface TabItem {
|
|||||||
title: string
|
title: string
|
||||||
content?: string
|
content?: string
|
||||||
materialId?: string // 关联的物料组件ID
|
materialId?: string // 关联的物料组件ID
|
||||||
|
materialState?: Record<string, any> // 物料组件的状态数据
|
||||||
}
|
}
|
||||||
|
|
||||||
// 面板接口
|
// 面板接口
|
||||||
|
|||||||
Reference in New Issue
Block a user