This commit is contained in:
oneao committed 2026-10-08 22:38:34 +08:00
1 parent f572dce2f6
commit e99a9fb274
356 files changed
+28877 -1055

No files matched your search

@@ -0,0 +1,27 @@
<script setup lang="ts">
// 最基础:treeData 是嵌套结构,默认按 { key, title, children } 取值
import { ref } from 'vue'
import { G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData: TreeNodeData[] = [
{
key: 'design',
title: '设计',
children: [
{ key: 'design/ui', title: 'UI 规范' },
{ key: 'design/icon', title: '图标库' },
],
},
{ key: 'frontend', title: '前端' },
{ key: 'backend', title: '后端' },
]
const selectedKeys = ref<string[]>(['design/ui'])
</script>
<template>
<div style="max-width: 320px">
<G3Tree v-model:selected-keys="selectedKeys" :tree-data="treeData" default-expand-all />
<p style="margin: 8px 0 0; font-size: 13px; color: #6b7280">选中:{{ selectedKeys.join(', ') || '无' }}</p>
</div>
</template>
@@ -0,0 +1,36 @@
<script setup lang="ts">
// checkable:父子联动;checkStrictly 时父子互不影响(右侧)
import { ref } from 'vue'
import { G3Space, G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData: TreeNodeData[] = [
{
key: 'role',
title: '角色权限',
children: [
{ key: 'read', title: '查看' },
{ key: 'write', title: '编辑' },
{ key: 'delete', title: '删除' },
],
},
]
const linked = ref<string[]>(['read'])
const strict = ref<string[]>(['read'])
</script>
<template>
<G3Space :size="32" wrap align="start">
<div style="width: 240px">
<p style="margin: 0 0 8px; font-size: 13px; color: #6b7280">父子联动(半选会回传)</p>
<G3Tree v-model:checked-keys="linked" :tree-data="treeData" checkable default-expand-all block-node />
<p style="margin: 8px 0 0; font-size: 12px">checked: {{ linked }}</p>
</div>
<div style="width: 240px">
<p style="margin: 0 0 8px; font-size: 13px; color: #6b7280">checkStrictly(互不影响)</p>
<G3Tree v-model:checked-keys="strict" :tree-data="treeData" checkable check-strictly default-expand-all block-node />
<p style="margin: 8px 0 0; font-size: 12px">checked: {{ strict }}</p>
</div>
</G3Space>
</template>
@@ -0,0 +1,44 @@
<script setup lang="ts">
// contextMenu:右键节点或空白处弹出菜单;菜单点击通过 contextMenuClick 回传
import { ref } from 'vue'
import { G3Message, G3Tree, type TreeContextMenuItem, type TreeNodeData } from '@g3soft/ui'
import { Copy, Pencil, Trash2 } from '@lucide/vue'
const treeData: TreeNodeData[] = [
{ key: 'a', title: '文档 A' },
{ key: 'b', title: '文档 B' },
]
const lastAction = ref('暂无')
// 用函数形式:可以按节点动态生成菜单(node 为 null 表示在空白处右键)
function contextMenu(node: TreeNodeData | null): TreeContextMenuItem[] {
if (!node) {
return [{ key: 'create', label: '新建文档' }]
}
return [
{ key: 'rename', label: '重命名', icon: Pencil },
{ key: 'copy', label: '复制', icon: Copy },
{ divider: true },
{ key: 'delete', label: '删除', icon: Trash2, danger: true },
]
}
function onMenuClick(info: { item: TreeContextMenuItem; node: TreeNodeData | null }) {
lastAction.value = `${info.node ? info.node.title : '空白处'} → ${info.item.label}`
G3Message.info(lastAction.value)
}
</script>
<template>
<div style="max-width: 320px">
<G3Tree
:tree-data="treeData"
:context-menu="contextMenu"
default-expand-all
block-node
@context-menu-click="onMenuClick"
/>
<p style="margin: 8px 0 0; font-size: 12px; color: #6b7280">{{ lastAction }}</p>
</div>
</template>
@@ -0,0 +1,53 @@
<script setup lang="ts">
// fieldNames 适配后端结构;#title 插槽自定义节点渲染;节点字段可控制图标/禁用
import { ref } from 'vue'
import { G3Button, G3Tag, G3Tree, type TreeNodeData } from '@g3soft/ui'
import { Folder, FileText } from '@lucide/vue'
// 后端字段:id / name / nodes
const treeData = ref<TreeNodeData[]>([
{
id: 'f1',
name: '设计稿',
icon: Folder,
nodes: [
{ id: 'f1-1', name: '首页.sketch', icon: FileText },
{ id: 'f1-2', name: '详情页.sketch', icon: FileText, disabled: true },
],
},
{ id: 'f2', name: '需求文档.md', icon: FileText },
])
const selectedKeys = ref<string[]>([])
</script>
<template>
<div style="max-width: 360px">
<G3Tree
v-model:selected-keys="selectedKeys"
:tree-data="treeData"
:field-names="{ key: 'id', title: 'name', children: 'nodes' }"
default-expand-all
block-node
>
<!-- #title 插槽:node 是原始数据,可自由渲染 -->
<template #title="{ node, selected }">
<span style="display: inline-flex; align-items: center; gap: 8px; width: 100%">
<span>{{ node.name }}</span>
<G3Tag v-if="selected" variant="secondary">当前</G3Tag>
<G3Button
v-if="!node.disabled"
size="small"
type="link"
style="margin-left: auto"
@click.stop="selectedKeys = [String(node.id)]"
>
选中
</G3Button>
</span>
</template>
</G3Tree>
<p style="margin: 8px 0 0; font-size: 12px; color: #6b7280">选中:{{ selectedKeys.join(', ') || '无' }}</p>
</div>
</template>
@@ -0,0 +1,73 @@
<script setup lang="ts">
// draggable:拖到行的上/下 25% 是「前/后插入」,中间是「放入内部」
// drop 事件里自己重排数据(组件不直接改 treeData)
import { ref } from 'vue'
import { G3Message, G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData = ref<TreeNodeData[]>([
{ key: '1', title: '第一项' },
{ key: '2', title: '第二项' },
{
key: '3',
title: '第三项(可拖入)',
children: [{ key: '3-1', title: '子项' }],
},
])
function findNode(nodes: TreeNodeData[], key: string): TreeNodeData | null {
for (const node of nodes) {
if (String(node.key) === key) return node
const found = node.children ? findNode(node.children, key) : null
if (found) return found
}
return null
}
function removeNode(nodes: TreeNodeData[], key: string): TreeNodeData | null {
const index = nodes.findIndex((node) => String(node.key) === key)
if (index >= 0) return nodes.splice(index, 1)[0]
for (const node of nodes) {
if (node.children) {
const removed = removeNode(node.children, key)
if (removed) return removed
}
}
return null
}
function onDrop(info: { dragKey: string | number; dropKey: string | number; dropPosition: string }) {
const dragKey = String(info.dragKey)
const dropKey = String(info.dropKey)
const data = treeData.value
const moved = removeNode(data, dragKey)
if (!moved) return
if (info.dropPosition === 'inside') {
const target = findNode(data, dropKey)
if (target) {
target.children = [...(target.children ?? []), moved]
G3Message.success(`已移动到「${target.title}」内部`)
}
} else {
const insert = (nodes: TreeNodeData[]): boolean => {
const index = nodes.findIndex((node) => String(node.key) === dropKey)
if (index >= 0) {
nodes.splice(info.dropPosition === 'before' ? index : index + 1, 0, moved)
return true
}
return nodes.some((node) => (node.children ? insert(node.children) : false))
}
insert(data)
G3Message.success(`已${info.dropPosition === 'before' ? '前移' : '后移'}到「${dropKey}」`)
}
treeData.value = [...data]
}
</script>
<template>
<div style="max-width: 320px">
<G3Tree :tree-data="treeData" draggable default-expand-all block-node @drop="onDrop" />
<p style="margin: 8px 0 0; font-size: 12px; color: #6b7280">拖拽任意节点试试(子节点不能拖进自己的子树)</p>
</div>
</template>
@@ -0,0 +1,40 @@
<script setup lang="ts">
// 展开状态:受控 expandedKeys(传了就是受控)与非受控默认值
import { ref } from 'vue'
import { G3Button, G3Space, G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData: TreeNodeData[] = [
{
key: 'a',
title: 'A 组',
children: [
{ key: 'a1', title: 'A-1', children: [{ key: 'a11', title: 'A-1-1' }] },
{ key: 'a2', title: 'A-2' },
],
},
{ key: 'b', title: 'B 组', children: [{ key: 'b1', title: 'B-1' }] },
]
// 受控:所有展开/收起都要经过 update:expandedKeys
const expandedKeys = ref<string[]>(['a', 'a1'])
function expandAll() {
expandedKeys.value = ['a', 'a1', 'a2', 'b', 'b1']
}
function collapseAll() {
expandedKeys.value = []
}
</script>
<template>
<G3Space direction="vertical" :size="12" style="display: flex; max-width: 320px">
<G3Space :size="8">
<G3Button size="small" type="outline" @click="expandAll">全部展开</G3Button>
<G3Button size="small" type="outline" @click="collapseAll">全部收起</G3Button>
</G3Space>
<!-- 传了 expanded-keys 即受控;双击节点也能展开(会走 update 事件) -->
<G3Tree v-model:expanded-keys="expandedKeys" :tree-data="treeData" block-node />
</G3Space>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
// filter:命中节点与它的祖先都会显示,命中的文字会被高亮,且自动展开命中路径
import { ref } from 'vue'
import { G3Input, G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData: TreeNodeData[] = [
{
key: 'module',
title: '模块管理',
children: [
{ key: 'module/list', title: '模块列表' },
{ key: 'module/field', title: '字段设计' },
],
},
{
key: 'system',
title: '系统管理',
children: [
{ key: 'system/user', title: '用户管理' },
{ key: 'system/role', title: '角色权限' },
],
},
]
const keyword = ref('')
</script>
<template>
<div style="max-width: 320px">
<G3Input v-model="keyword" allow-clear placeholder="输入关键词,如「管理」" style="margin-bottom: 8px" />
<G3Tree :tree-data="treeData" :filter="keyword" default-expand-all block-node>
<template #empty>没有匹配的节点</template>
</G3Tree>
</div>
</template>
@@ -0,0 +1,30 @@
<script setup lang="ts">
// lazy + load:首次展开没有子节点的节点时请求数据,父层把 children 写回并把 key 记入 loadedKeys
import { ref } from 'vue'
import { G3Tree, type TreeNodeData } from '@g3soft/ui'
const treeData = ref<TreeNodeData[]>([
{ key: 'org-1', title: '总公司' },
{ key: 'org-2', title: '分公司' },
])
const loadedKeys = ref<string[]>([])
async function onLoad(node: TreeNodeData) {
// 模拟请求:200ms 后返回子节点
await new Promise((resolve) => setTimeout(resolve, 200))
node.children = [
{ key: `${node.key}-a`, title: `${node.title} / 部门 A` },
{ key: `${node.key}-b`, title: `${node.title} / 部门 B` },
]
treeData.value = [...treeData.value]
loadedKeys.value = [...loadedKeys.value, String(node.key)]
}
</script>
<template>
<div style="max-width: 320px">
<G3Tree v-model:loaded-keys="loadedKeys" :tree-data="treeData" lazy block-node @load="onLoad" />
<p style="margin: 8px 0 0; font-size: 12px; color: #6b7280">已加载:{{ loadedKeys.join(', ') || '无' }}</p>
</div>
</template>