This commit is contained in:
2025-12-20 20:43:23 +08:00
parent 2f2dcf580f
commit 7c48e4148d
6 changed files with 429 additions and 67 deletions

View File

@@ -24,7 +24,63 @@
"id": "mxfx11j",
"title": "树形展示器",
"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",
@@ -33,31 +89,56 @@
"materialId": "TestWidget2"
}
],
"activeTabId": "2emt1si"
"activeTabId": "mxfx11j"
},
"rightPanel": {
"id": "right",
"tabs": [
{
"id": "ojaw0e3",
"title": "新窗口 3",
"content": "新窗口内容"
},
{
"id": "vrh9bl2",
"title": "数据表格",
"content": "新窗口内容",
"materialId": "DataTable"
"materialId": "DataTable",
"materialState": {
"data": [
{
"property": "项目名称",
"value": "1111"
},
{
"id": "y2iwzgl",
"title": "测试组件A",
"content": "新窗口内容",
"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"
}

View File

@@ -25,6 +25,20 @@ const activeMaterialComponent = computed(() => {
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) => {
panelStore.setActiveTab(props.panel.id, tabId)
}
@@ -69,7 +83,9 @@ const handleCloseTab = (tabId: string, event: Event) => {
<component
v-if="activeMaterialComponent"
:is="activeMaterialComponent"
:materialState="activeMaterialState"
class="material-wrapper"
@update:state="handleStateUpdate"
/>
<!-- 普通内容 -->
<div v-else-if="activeTab" class="content-wrapper">

View File

@@ -1,29 +1,108 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import config from './index.json'
interface TableRow {
property: string
value: string
}
const props = defineProps<{
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>
<template>
<div class="data-table">
<div class="table-header">
<span class="title">{{ config.name }}</span>
<span class="hint">双击单元格编辑</span>
</div>
<div class="table-body">
<table class="table">
<thead>
<tr>
<th v-for="col in mergedProps.columns" :key="col">{{ col }}</th>
<th v-for="col in columns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in mergedProps.data" :key="index">
<td>{{ row.property }}</td>
<td>{{ row.value }}</td>
<tr v-for="(row, index) in tableData" :key="index">
<td @dblclick="startEdit(index, 'property')">
<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>
</tbody>
</table>
@@ -43,6 +122,9 @@ const mergedProps = { ...config.props, ...props.materialProps }
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3c3c3c;
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
@@ -51,6 +133,11 @@ const mergedProps = { ...config.props, ...props.materialProps }
font-weight: 500;
}
.hint {
color: #666666;
font-size: 11px;
}
.table-body {
flex: 1;
padding: 12px;
@@ -78,9 +165,36 @@ const mergedProps = { ...config.props, ...props.materialProps }
.table td {
color: #d4d4d4;
cursor: pointer;
position: relative;
}
.table td:hover {
background: #2a2d2e;
}
.table tbody tr:hover {
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>

View File

@@ -1,19 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watch } from 'vue'
import config from './index.json'
const props = defineProps<{
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 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>
<template>
<div class="text-editor">
<div class="editor-header">
<span class="title">{{ config.name }}</span>
<span class="hint">自动保存</span>
</div>
<div class="editor-body">
<textarea
@@ -38,6 +68,9 @@ const textContent = ref(mergedProps.defaultValue)
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3c3c3c;
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
@@ -46,6 +79,11 @@ const textContent = ref(mergedProps.defaultValue)
font-weight: 500;
}
.hint {
color: #666666;
font-size: 11px;
}
.editor-body {
flex: 1;
padding: 12px;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, watch, onMounted } from 'vue'
import draggable from 'vuedraggable'
import config from './index.json'
interface TreeNode {
@@ -11,10 +12,22 @@ interface TreeNode {
const props = defineProps<{
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())
// 初始化展开状态
@@ -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)) {
expandedNodes.value.delete(nodeId)
} else {
@@ -39,55 +61,101 @@ const toggleNode = (nodeId: string) => {
}
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>
<template>
<div class="tree-viewer">
<div class="viewer-header">
<span class="title">{{ config.name }}</span>
<span class="hint">可跨级拖拽</span>
</div>
<div class="viewer-body">
<div class="tree-container">
<template v-for="node in mergedProps.treeData" :key="node.id">
<div class="tree-node-wrapper">
<div
class="tree-node"
@click="toggleNode(node.id)"
<!-- 第一层 -->
<draggable
:list="treeData"
group="tree-nodes"
item-key="id"
:animation="150"
ghost-class="node-ghost"
@end="onDragEnd"
>
<span class="expand-icon" v-if="node.children?.length">
{{ isExpanded(node.id) ? '' : '' }}
<template #item="{ element: node }">
<div class="tree-node-wrapper">
<div class="tree-node">
<span
class="expand-icon"
:class="{ empty: !node.children?.length }"
@click="toggleNode(node.id, $event)"
>
{{ node.children?.length ? (isExpanded(node.id) ? '▼' : '▶') : '' }}
</span>
<span class="expand-icon empty" v-else></span>
<span class="node-label">{{ node.label }}</span>
</div>
<div class="tree-children" v-if="node.children && isExpanded(node.id)">
<template v-for="child in node.children" :key="child.id">
<div class="tree-node-wrapper">
<div
class="tree-node level-1"
@click="toggleNode(child.id)"
<!-- 第二层 -->
<div class="tree-children" v-if="isExpanded(node.id)">
<draggable
:list="ensureChildren(node)"
group="tree-nodes"
item-key="id"
:animation="150"
ghost-class="node-ghost"
@end="onDragEnd"
>
<span class="expand-icon" v-if="child.children?.length">
{{ isExpanded(child.id) ? '' : '' }}
<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="expand-icon empty" v-else></span>
<span class="node-label">{{ child.label }}</span>
</div>
<div class="tree-children" v-if="child.children && isExpanded(child.id)">
<div
v-for="subChild in child.children"
:key="subChild.id"
class="tree-node level-2"
<!-- 第三层 -->
<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>
</div>
</template>
</draggable>
</div>
</div>
</template>
</draggable>
</div>
</div>
</template>
</draggable>
</div>
</div>
</div>
@@ -105,6 +173,9 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3c3c3c;
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
@@ -113,6 +184,11 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
font-weight: 500;
}
.hint {
color: #666666;
font-size: 11px;
}
.viewer-body {
flex: 1;
padding: 8px;
@@ -123,13 +199,22 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
font-size: 13px;
}
.tree-node-wrapper {
margin: 1px 0;
}
.tree-node {
display: flex;
align-items: center;
padding: 4px 8px;
cursor: pointer;
cursor: grab;
border-radius: 4px;
color: #cccccc;
user-select: none;
}
.tree-node:active {
cursor: grabbing;
}
.tree-node:hover {
@@ -146,20 +231,47 @@ const isExpanded = (nodeId: string) => expandedNodes.value.has(nodeId)
.expand-icon {
width: 16px;
height: 16px;
font-size: 10px;
color: #888888;
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 {
visibility: hidden;
cursor: default;
}
.node-label {
flex: 1;
pointer-events: none;
}
.tree-children {
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>

View File

@@ -4,6 +4,7 @@ export interface TabItem {
title: string
content?: string
materialId?: string // 关联的物料组件ID
materialState?: Record<string, any> // 物料组件的状态数据
}
// 面板接口