对话页面接入版本树

This commit is contained in:
X1627315083@163.com
2026-02-23 10:54:17 +08:00
parent 1e21e3b408
commit d1e70c166c
20 changed files with 150 additions and 95 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
import VersionTreeIndex from './versionTree/index.vue'
import VersionTreeIndex from '@/components/versionTree/index.vue'
import GenerateSketch from './generateSketch/index.vue'
//const props = defineProps({
//})
@@ -73,14 +74,25 @@ const generateSketch = ()=>{
sketchRestore('2')
}
const sketchRestore = (id)=>{
generateData.value.list.forEach((item)=>{
if(item.id == id){
item.type = 'waiting'
const sketchRestore = (item)=>{
generateData.value.list.forEach((generateDataItem)=>{
if(item.id == generateDataItem.id){
generateDataItem.type = 'waiting'
}
})
}
const sketchDelete = (item)=>{
console.log(item)
generateData.value.list.forEach((generateDataItem,index)=>{
if(item.id == generateDataItem.id){
generateData.value.list.splice(index,1)
}
})
}
onMounted(()=>{
})
onUnmounted(()=>{
@@ -99,7 +111,7 @@ const {} = toRefs(data);
</div>
<VersionTreeIndex v-model:versionTreeData="versionTreeData" />
<div class="generateSketchBox">
<GenerateSketch v-model:generateData="generateData"></GenerateSketch>
<GenerateSketch v-model:generateData="generateData" @sketchRestore="sketchRestore" @sketchDelete="sketchDelete"></GenerateSketch>
</div>
</div>
</template>

View File

@@ -1,19 +1,35 @@
<template>
<div class="agent-wrapper flex space-between">
<Agent :title="agentTitle" />
<div class="preview-wrapper">
<Preview :type="previewType" />
<div class="openVersionTree">
<div class="btn" @click="versionTreeData.drawer = true">
Version Tree
</div>
</div>
<div class="content-wrapper">
<Agent :title="agentTitle" />
<div class="preview-wrapper">
<Preview :type="previewType" />
</div>
</div>
<VersionTreeIndex v-model:versionTreeData="versionTreeData" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import Agent from './components/Agent.vue'
import Preview from './components/Preview.vue'
import VersionTreeIndex from '@/components/versionTree/index.vue'
const agentTitle = ref('Retro Sofa Sketch')
const previewType = ref<'sketch' | 'report'>('sketch')
const versionTreeData = ref({
drawer:false,
list:computed(()=>{
return []
})
})
</script>
<style lang="less" scoped>
@@ -21,7 +37,39 @@
width: 100%;
height: 100%;
border-top: 0.1rem solid #c9c9c9;
padding: 8rem 2.3rem 6rem 2.8rem;
padding: 0rem 2.3rem 6rem 2.8rem;
flex-direction: column;
.openVersionTree{
padding: 2rem 2.5rem;
width: 100%;
display: flex;
justify-content: flex-end;
height: min-content;
> .btn{
padding: 1.5rem 1.45rem;
font-weight: 500;
font-size: 1.4rem;
border-radius: 2rem;
position: relative;
background: white;
cursor: pointer;
&::before {
content: '';
position: absolute;
inset: -1px;
background: linear-gradient(119.03deg, rgba(233, 121, 60, 0.8) 1.61%, rgba(255, 207, 144, 0.8) 101.01%);
border-radius: 2rem;
z-index: -1;
}
}
}
.content-wrapper{
display: flex;
flex: 1;
justify-content: space-between;
overflow: hidden;
}
.c-svg {
width: initial;

View File

@@ -12,8 +12,10 @@ const props = defineProps({
},
},
})
//const emit = defineEmits([
//])
const emit = defineEmits([
'sketchRestore',
'sketchDelete',
])
let data = reactive({
})
onMounted(()=>{
@@ -26,7 +28,7 @@ const {} = toRefs(data);
<template>
<div class="generateSketch">
<div v-for="item in generateData.list" :key="item.id" class="item">
<GenerateItem :item="item" />
<GenerateItem :item="item" @sketchRestore="()=>emit('sketchRestore',item)" @sketchDelete="()=>emit('sketchDelete',item)" />
</div>
</div>
</template>

View File

@@ -1,139 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
const props = defineProps({
textData: {
type: Object,
default: () => {}
} as any,
styleData: {
type: Object,
default: () => {}
} as any,
callBack: {
type: Function,
default: () => {}
}
})
// const emit = defineEmits([
// ])
let data = reactive({
})
const dialogFormVisible = ref(false)
const confirm = async ()=>{
if(props.callBack)await props.callBack()
dialogFormVisible.value = false
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({open:()=>dialogFormVisible.value = true})
const {} = toRefs(data);
</script>
<template>
<div class="dialog">
<el-dialog
:align-center="true"
v-model="dialogFormVisible"
:close-on-click-modal="false"
:title="props.textData?.title"
:show-close="false"
:width="props.styleData?.width || '50%'">
<template #header="{ close, titleId, titleClass }">
<div class="my-header">
<div class="text">{{ props.textData?.title }}</div>
<div class="icon" @click="dialogFormVisible = false">
<SvgIcon
name="close"
size="8"
/>
</div>
</div>
</template>
<div class="boundary"></div>
<div class="dialog-content">
{{ props.textData?.text }}
</div>
<template #footer>
<div class="dialog-footer">
<div class="dialog-footer-cancel" @click="dialogFormVisible = false">
{{ props.textData?.cancelText || 'Cancel' }}
</div>
<div class="dialog-footer-confirm" type="primary" @click="confirm">
{{ props.textData?.submitText || 'Confirm' }}
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<style lang="less" scoped>
.dialog{
--el-dialog-padding-primary: 1.6rem 1.2rem;
--el-dialog-border-radius: .8rem;
:deep(.el-dialog){
.my-header{
display: flex;
padding: 0 .5rem;
justify-content: space-between;
--el-dialog-padding-primary: 1.2rem
.text{
font-family: 'Semibold';
font-size: 1.4rem;
line-height: 2rem;
letter-spacing: -0.18px;
}
.icon{
cursor: pointer;
width: 2rem;
height: 2rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.boundary{
border-bottom: .7px solid rgba(0, 0, 0, 0.1);
width: 100%;
}
.dialog-content{
padding: 0 .5rem;
padding-top: 1.2rem;
font-family: 'Regular';
font-weight: 400;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: -0.18px;
}
.dialog-footer{
display: flex;
justify-content: flex-end;
--el-dialog-padding-primary: 1.7rem;
> div{
font-weight: 500;
font-size: 1.2rem;
line-height: 2rem;
letter-spacing: -0.18px;
width: 5.9rem;
text-align: center;
cursor: pointer;
border-radius: 1.5rem;
padding: .3rem 0 .4rem;
}
.dialog-footer-cancel{
color: #000;
margin-right: .6rem;
border: 0.7px solid #0000001A;
background: #FFFFFF;
}
.dialog-footer-confirm{
color: #fff;
background-color: #f74545;
}
}
}
}
</style>

View File

@@ -1,106 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import userHead from '@/assets/images/chatVersion/userHead.png'
import robotHead from '@/assets/images/chatVersion/robotHead.png'
import { useI18n } from 'vue-i18n'
const { t: $t } = useI18n()
const props = defineProps({
type: {
type: String,
default: 'user'
},
})
//const emit = defineEmits([
//])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="ChatHistory">
<div class="titleInfo">
<div class="userInfo">
<img :src="type == 'user'? userHead:robotHead" alt="">
<span>{{ type == 'user'? $t('VersionTree.input'): $t('VersionTree.sketch') }}</span>
</div>
<div class="time">
<span>12:00:00</span>
</div>
</div>
<div class="infoTitle">{{ type == 'user'? $t('VersionTree.userRequest'): $t('VersionTree.generateResult') }}</div>
<div class="history">
Design a modern yellow sofa that combines comfort, elegance, and contemporary aesthetics. The sofa features a warm, soft yellow tone (mustard or light ochre), with a minimalist silhouette and clean lines. Upholstered in high-quality fabric with a subtle texture, offering a cozy and inviting feel. The seat is deep and plush, with generous cushioning for relaxation, while the backrest and armrests are softly rounded to enhance comfort.
</div>
</div>
</template>
<style lang="less" scoped>
.ChatHistory{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
> .titleInfo{
height: 2.4rem;
display: flex;
justify-content: space-between;
> .userInfo{
display: flex;
align-items: center;
> img{
margin-right: .2rem;
}
> span{
font-family: 'SemiBold';
font-weight: 600;
font-size: 1.4rem;
line-height: 2rem;
color: #000;
opacity: .5;
}
}
> .time{
font-family: 'SemiBold';
font-weight: 600;
font-size: 1.4rem;
line-height: 2rem;
opacity: .5;
}
}
> .infoTitle{
margin-top: .6rem;
font-size: 1.2rem;
line-height: 2rem;
margin-left: 3rem;
}
> .history{
margin-top: .8rem;
font-family: 'Regular';
font-weight: 400;
font-size: 1.2rem;
line-height: 1.4rem;
letter-spacing: Letter Spacing;
flex: 1;
overflow-y: auto;
margin-left: 3rem;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #ababab;
}
&::-webkit-scrollbar-track {
border-radius: 4px;
background: #d9d9d9;
}
}
}
</style>

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import VersionDetail from './versionDetail.vue'
import ChatDetail from './chatDetail.vue'
//const props = defineProps({
//})
const emit = defineEmits([
])
const detailData = ref({
id:1,
versionDetail:{
version:'1.0.0',
versionTime:'2023-08-01 10:00:00',
versionSketch:'Version 1 - Sketch',
versionSketchTime:'2023-08-01 10:00:00',
},
userChatDetail:{
}
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
// const {} = toRefs(data);
</script>
<template>
<div class="detailBox">
<div class="versionDetail">
<VersionDetail
:versionDetail="detailData.versionDetail"
></VersionDetail>
</div>
<div class="useInput">
<ChatDetail type="user"></ChatDetail>
</div>
<div class="systemInput">
<ChatDetail type="robot"></ChatDetail>
</div>
</div>
</template>
<style lang="less" scoped>
.detailBox{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
> div{
border-radius: var(--border-radius, 1rem);
border: 1px solid #D9D9D9;
background-color: #f7f7f7;
margin-bottom: 3.6rem;
padding: 1.5rem 1.4rem;
width: 100%;
&.versionDetail{
height: 21rem;
}
&.useInput{
height: 21.5rem;
}
&.systemInput{
flex: 1;
}
&:last-child{
margin-bottom: 0;
}
}
}
</style>

View File

@@ -1,76 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import { useI18n } from 'vue-i18n'
const { t: $t } = useI18n()
const props = defineProps({
versionDetail: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits([
'versionRestore',
'versionDelete',
])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="versionDetail">
<div class="title">
<span class="titleText">
{{ $t('VersionTree.versionInformation') }}
</span>
</div>
<div class="version">{{versionDetail.version}}</div>
<div class="time marBott1">{{versionDetail.versionTime}}</div>
<div class="version gray">{{versionDetail.versionSketch}}</div>
<div class="time gray">{{versionDetail.versionSketchTime}}</div>
</div>
</template>
<style lang="less" scoped>
.versionDetail{
width: 100%;
height: 100%;
position: relative;
> .title{
line-height: 3.2rem;
font-size: 1.4rem;
color: #000;
display: flex;
justify-content: space-between;
height: 3.2rem;
margin-bottom: 2rem;
> .titleText{
opacity: .5;
font-weight: 600;
font-family: 'SemiBold';
}
}
> .version{
font-family: 'SemiBold';
font-weight: 600;
font-size: 1.4rem;
margin-bottom: .2rem;
line-height: 2rem;
}
> .marBott1{
margin-bottom: 1.2rem;
}
> .time{
font-weight: 500;
font-size: 1.2rem;
line-height: 2rem;
}
> .gray{
color: #C1C1C1;
}
}
</style>

View File

@@ -1,247 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from 'vue'
import Tree from './tree/index.vue'
import Detail from './detail/index.vue'
import { versionsList } from './tools/versionsData'
import { findAndAddChild, findAndRemoveChild } from './tools/tools'
const props = defineProps({
versionTreeData: {
type: Object,
default: () => {
return {
drawer: false,
list: []
}
}
}
})
//const emit = defineEmits([
//])
const treeRef = ref(null)
const treeKey = ref(0)
const treeState = ref(true)//
const selectItem:any = ref({})
const openTree = (state)=>{
treeState.value = state
}
const versionRestore = ()=>{
let id = ''
if(selectItem.value?.child?.length > 0){
function findMaxForYourFormat(items) {
let max = 0
for (const item of items) {
// 直接分割并取最后一部分
const parts = item.id.split('-')
const lastNumber = parseInt(parts[parts.length - 1], 10)
if (lastNumber > max) {
max = lastNumber
}
}
return max
}
id = `${selectItem.value?.id}-${findMaxForYourFormat(selectItem.value?.child) + 1}`
}else{
id = `${selectItem.value?.id}-1`
}
let addObj = {
id,
name:`V${id}`
}
findAndAddChild(versionsList, selectItem.value?.id, addObj)
selectItem.value = {...addObj}
treeKey.value++
}
const versionDelete = (versionDetail)=>{
if(!selectItem.value.id)return
findAndRemoveChild(versionsList, selectItem.value.id)
treeKey.value++
}
let data = reactive({})
onMounted(() => {})
onUnmounted(() => {})
defineExpose({})
const {} = toRefs(data)
</script>
<template>
<div class="versionTree">
<el-drawer
v-model="versionTreeData.drawer"
:close-on-press-escape="false"
:close-on-click-modal="false"
:size="treeState ? '109rem' : '49rem'"
body-class="versionTreeBody"
:with-header="false"
>
<div class="versionTreeTitle">
<span>Version Tree: Retro Sofa Sketch</span>
<div class="closeBtn" @click="versionTreeData.drawer = false">
<div class="closeIcon">
<SvgIcon name="close" />
</div>
</div>
</div>
<!-- <div class="expandBtnBox">
<div class="btn" @click="openTree(true)">
<div class="bg left" :class="{'active':treeState}"></div>
<span>{{ $t('VersionTree.linearNodeTree') }}</span>
</div>
<div class="btn" @click="openTree(false)">
<div class="bg right" :class="{'active':!treeState}"></div>
<span>{{ $t('VersionTree.branchingNodeTree') }}</span>
</div>
</div> -->
<div class="versionTreeBox">
<div class="tree">
<Tree
ref="treeRef"
:versionsList="versionsList"
:treeState="treeState"
v-model:selectItem="selectItem"
@versionRestore="versionRestore"
@versionDelete="versionDelete"
:key="treeKey"
></Tree>
</div>
<div class="detail">
<Detail
v-model:selectItem="selectItem"
></Detail>
</div>
</div>
</el-drawer>
</div>
</template>
<style lang="less" scoped>
.versionTree{
--border-radius: 1rem;
--treeItem-width: 5.4rem;
--treeItem-height: 5.4rem;
--treeItem-raduis: 50%;
--treeItem-border: 2px solid #C1C1C1;
--treeItem-background: #ffffff;
--treeItem-active-background: #e6e6e6;
:deep(.versionTreeBody) {
--el-drawer-padding-primary: 0rem;
display: flex;
flex-direction: column;
}
.versionTreeTitle {
width: 100%;
height: 8rem;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0.8rem 0 2.4rem;
border-bottom: 1px solid #c9c9c9;
> span {
font-size: 2rem;
font-weight: 600;
font-family: 'SemiBold';
}
.versionTreeTitle{
width: 100%;
height: 8rem;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 .8rem 0 2.4rem;
border-bottom: 1px solid #C9C9C9;
> span{
font-size: 2rem;
font-weight: 600;
font-family: 'SemiBold';
}
> .closeBtn{
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
}
}
.expandBtnBox{
padding: .4rem .7rem;
border-radius: 1.1rem;
background-color: #f3f3f3;
display: flex;
margin-left: auto;
margin-top: 2.1rem;
margin-right: 3rem;
> .btn{
padding: .6rem .5rem;
position: relative;
cursor: pointer;
> .bg{
position: absolute;
background-color: #fff;
border-radius: .6rem;
width: 0;
height: 100%;
transition: all .3s;
top: 0;
&.active{
width: 100%;
}
&.left{
right: 0;
}
&.right{
left: 0;
}
}
> span{
position: relative;
font-size: 1.3rem;
line-height: 1.8rem;
font-weight: 500;
letter-spacing: -0.08px;
}
}
}
.versionTreeBox{
flex: 1;
display: flex;
overflow: hidden;
> .tree{
flex: 1;
height: 100%;
overflow: hidden;
padding: 2.1rem 0 5.4rem 2.2rem;
}
> .detail{
width: 35rem;
margin: 2.1rem 3rem 5.4rem 3.4rem;
height: calc(100% - 2.1rem - 5.4rem);
overflow: hidden;
}
}
}
.expandBtnBox {
}
.versionTreeBox {
flex: 1;
display: flex;
overflow: hidden;
> .tree {
flex: 1;
height: 100%;
overflow: hidden;
padding: 2.1rem 0 5.4rem 2.2rem;
}
> .detail {
width: 35rem;
margin: 2.1rem 3rem 5.4rem 3.4rem;
height: calc(100% - 2.1rem - 5.4rem);
overflow: hidden;
}
}
}
</style>

View File

@@ -1,145 +0,0 @@
// import dagre from '@dagrejs/dagre'
import dagre from 'dagre'
import { Position, useVueFlow } from '@vue-flow/core'
import { ref } from 'vue'
/**
* Composable to run the layout algorithm on the graph.
* It uses the `dagre` library to calculate the layout of the nodes and edges.
*/
export function useLayout() {
const { findNode } = useVueFlow()
const graph = ref(new dagre.graphlib.Graph())
const previousDirection = ref('LR')
function layout(nodes, edges, direction = 'LR') {
// 验证和规范化方向参数
const validDirections = ['TB', 'BT', 'LR', 'RL']
const layoutDirection = validDirections.includes(direction) ? direction : 'LR'
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
const dagreGraph = new dagre.graphlib.Graph()
graph.value = dagreGraph
dagreGraph.setDefaultEdgeLabel(() => ({}))
// 根据方向判断是否为水平布局
const isHorizontal = layoutDirection === 'LR' || layoutDirection === 'RL'
dagreGraph.setGraph({ rankdir: layoutDirection })
previousDirection.value = layoutDirection
for (const node of nodes) {
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
const graphNode = findNode(node.id)
dagreGraph.setNode(node.id, {
width: graphNode.dimensions.width || 150,
height: graphNode.dimensions.height || 50
})
}
for (const edge of edges) {
dagreGraph.setEdge(edge.source, edge.target)
}
dagre.layout(dagreGraph)
// set nodes with updated positions
return nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id)
// 根据方向动态计算连接点位置
let targetPosition, sourcePosition
switch (layoutDirection) {
case 'BT': // 从上到下 (Top to Bottom)
targetPosition = Position.Bottom // 目标节点连接点在下方
sourcePosition = Position.Top // 源节点连接点在上方
break
case 'TB': // 从下到上 (Bottom to Top)
targetPosition = Position.Top // 目标节点连接点在上方
sourcePosition = Position.Bottom // 源节点连接点在下方
break
case 'LR': // 从左到右 (Left to Right)
targetPosition = Position.Left
sourcePosition = Position.Right
break
case 'RL': // 从右到左 (Right to Left)
targetPosition = Position.Right
sourcePosition = Position.Left
break
default:
targetPosition = Position.Top
sourcePosition = Position.Bottom
}
return {
...node,
targetPosition,
sourcePosition,
position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
}
})
}
return { graph, layout, previousDirection }
}
/**
* 递归查找指定ID的节点并添加子节点
* @param {Array} items - 要搜索的数组
* @param {string} targetId - 要查找的节点ID
* @param {Object} newChild - 要添加的新子节点
* @returns {boolean} 是否成功添加
*/
export function findAndAddChild(items, targetId, newChild) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
// 如果找到目标节点
if (item.id === targetId) {
// 初始化child数组如果不存在
if (!item.child) {
item.child = []
}
// 添加新子节点
item.child.push(newChild)
return true
}
// 递归搜索子节点
if (item.child && item.child.length > 0) {
const found = findAndAddChild(item.child, targetId, newChild)
if (found) return true
}
}
return false
}
/**
* 递归删除指定ID的节点
* @param {Array} items - 要搜索的数组
* @param {string} targetId - 要删除的节点ID
* @returns {boolean} 是否成功删除
*/
export function findAndRemoveChild(items, targetId) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
// 如果找到目标节点,从当前数组中删除
if (item.id === targetId) {
items.splice(i, 1)
return true
}
// 递归搜索子节点
if (item.child && item.child.length > 0) {
const found = findAndRemoveChild(item.child, targetId)
if (found) return true
}
}
return false
}

View File

@@ -1,60 +0,0 @@
export const versionsList = [
{
id: '1',
name:'V1',
child:[
{
id: '1-1',
name:'V1-1',
child:[
{
id: '1-1-1',
name:'V1-1-1',
child:[
{
id: '1-1-1-1',
name:'V1-1-1-1',
},{
id: '1-1-1-2',
name:'V1-1-1-2',
},
]
}
]
},
{
id: '1-2',
name:'V1-2',
child:[
{
id: '1-2-1',
name:'V1-2-1',
child:[
{
id: '1-2-1-1',
name:'V1-2-1-1',
}
]
},{
id: '1-2-2',
name:'V1-2-2',
child:[
{
id: '1-2-2-1',
name:'V1-2-2-1',
},{
id: '1-2-2-2',
name:'V1-2-2-2',
}
]
},
]
},
{
id: '1-3',
name:'V1-3',
}
]
}
]

View File

@@ -1,142 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs, watch, nextTick } from "vue";
import view1Item from './view1Item.vue'
import view2 from './view2/index.vue'
const props = defineProps({
versionsList: {
type: Array,
default: () => []
},
treeState:{
default:false,
},
selectItem: {
type: Object,
default: () => ({})
} as any,
})
const emit = defineEmits([
'update:selectItem',
'versionRestore',
'versionDelete',
])
let data = reactive({
})
const view1Ref = ref(null)
const isLoad = ref(false)
const treeStateTime = ref(true)
watch(()=>props.treeState,(newVal,oldVal)=>{
treeStateTime.value = false
setTimeout(()=>{
treeStateTime.value = true
},250)
})
const view2Ref = ref(null)
const pushView2Item = (item)=>{
view2Ref.value.push(item)
}
const treeList = ref([
])
function traverseArray(items, callback) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
callback(item, i)
if (item.child && Array.isArray(item.child) && item.child.length > 0) {
traverseArray(item.child, callback)
}
}
}
const initialize = ()=>{
isLoad.value = false
treeList.value = []
treeList.value.push({id: null,name:'index',})
traverseArray(props.versionsList, (item, index) => {
treeList.value.push(item)
})
isLoad.value = true
if(!props.selectItem?.id)setSelectItem(treeList.value[treeList.value.length - 1])
}
const setSelectItem = (item)=>{
if(!item.id)return
emit('update:selectItem', {...item})
}
// 滚动到选中项
const scrollToActive = ()=>{
nextTick(() => {
const activeEl = view1Ref.value?.querySelector('.active')
console.log(activeEl)
activeEl.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
})
}
watch(()=>treeStateTime.value,(newVal,oldVal)=>{
if((props.treeState + '') == 'false'){
// scrollToActive()
}
})
watch(()=>props.selectItem,(newVal,oldVal)=>{
// scrollToActive()
},{immediate: true})
onMounted(()=>{
initialize()
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="tree" v-show="treeStateTime" v-if="isLoad">
<!-- <div v-show="!treeState" class="box view1" ref="view1Ref">
<view1Item v-for="item in treeList" :key="item.name" :selectItem="props.selectItem" :item="item" @click="setSelectItem(item)"></view1Item>
</div> -->
<div v-show="treeState" class="box view2">
<view2
ref="view2Ref"
@setSelectItem="setSelectItem"
:treeList="treeList"
:selectItem="props.selectItem"
@versionRestore="()=>emit('versionRestore')"
@versionDelete="emit('versionDelete')"
></view2>
</div>
</div>
</template>
<style lang="less" scoped>
.tree{
width: 100%;
height: 100%;
position: relative;
.box{
width: 100%;
height: 100%;
box-sizing: border-box;
&.view1{
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
&.view2{
border: 1px solid #D9D9D9;
background-color: #f7f7f7;
border-radius: var(--border-radius, 1rem);
}
}
}
</style>

View File

@@ -1,75 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
const props = defineProps({
item: {
type: Object,
default: () => ({})
},
selectItem: {
type: Object,
default: () => {},
},
})
//const emit = defineEmits([
//])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="btn" :class="{'active': item.id === props.selectItem?.id}">
{{ item.name }}
</div>
</template>
<style lang="less" scoped>
.btn{
font-size: 1.2rem;
width: var(--treeItem-width);
height: var(--treeItem-height);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--treeItem-raduis);
border: var(--treeItem-border);
color: #000;
cursor: pointer;
margin-bottom: 4rem;
background-color: var(--treeItem-background);
position: relative;
&::after{
content: '';
cursor: auto;
position: absolute;
top: calc(100% + 2px);
height: 4rem;
left: 50%;
width: 1.8px;
transform: translateX(-50%);
background:
repeating-linear-gradient(0deg, #333 0, #333 5px, transparent 5px, transparent 10px),
linear-gradient(white, white);
background-clip: padding-box, border-box;
background-origin: border-box;
}
&.active{
background-color: var(--treeItem-active-background);
}
}
.btn:nth-child(1){
background-color: #7A7A7A;
color: #FFF;
border: 2px solid #7A7A7A;
}
.btn:last-child{
margin-bottom: 0;
&::after{
display: none;
}
}
</style>

View File

@@ -1,24 +0,0 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue'
const props = defineProps<{
data: {
type: Object,
default: () => ({
id: '',
})
}
}>()
</script>
<template>
<div class="node start">
<Handle type="source" id="Bottom" :position="Position.Bottom" />
<div>index</div>
</div>
</template>
<style lang="less" scoped>
</style>

View File

@@ -1,244 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, nextTick, watch } from "vue";
import type { Node, Edge } from '@vue-flow/core'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import SpecialEdge from './speciaiEdge.vue'
import InputNode from './InputNode.vue'//主
import SecondaryNode from './secondaryNode.vue'//分支
import { useLayout } from '../../tools/tools'
import dialogVue from "../../components/dialog.vue";
const props = defineProps({
selectItem: {
type: Object,
default: () => ({})
} as any,
treeList: {
type: Array,
default: () => []
}
})
const emit = defineEmits([
'setSelectItem',
'versionRestore',
'versionDelete',
])
const dialogDeleteRef = ref()
const dialogRestoreRef = ref()
// 节点类型input、output、default、custom
// input:开始点output结尾点default普通节点custom自定义节点
const position = { x: 0, y: 0 }
const nodes = ref<Node[]>([
// { id: '1', type: 'input', label: 'Node 1', class: 'custom-node start', position },
// { id: '2', type: 'SecondaryNode', class: 'custom-node', data: { id: '主 1' }, position },
// { id: '2-1', type: 'SecondaryNode', class: 'custom-node', data: { id: '主 2' }, position },
// { id: '3', type: 'SecondaryNode', class: 'custom-node', data: { id: '主 3' }, position },
])
// 边类型custom、default
// custom自定义边default普通边step直角边smoothstep平滑边
const edges = ref<Edge[]>([
// { id: 'e1-3', source: '2', target: '2-1', type: 'smoothstep', },
// { id: 'e1-4', source: '2', target: '3', type: 'smoothstep', animated: true },
])
const { fitView } = useVueFlow()
const { layout } = useLayout()
async function layoutGraph(direction) {
setTimeout(() => {
nodes.value = layout(nodes.value, edges.value, direction)
console.log(nodes.value)
nextTick(() => {
fitView()
})
}, 0)
}
const push = (item)=>{
if(!item.id){
nodes.value.push({ id: '0', type: 'InputNode', class: 'custom-node', position })
}else{
let className = `custom-node item${item.id.replace(/-/g, "_")}`
let id = item.id
let source = edges.value.length == 0?'0':item.id.slice(0, -2)
nodes.value.push({id,type:'SecondaryNode',class:className,position,data:item})
edges.value.push({ id, target:id, source, type: 'smoothstep' })
}
}
const initialized = ()=>{
layoutGraph('TB')
}
//是否可拖动节点
const nodesDraggable = ref(false)
const toggleNodesDraggable = () => {
nodesDraggable.value = !nodesDraggable.value
}
const handleVueFlowNodeClick = ({node}) => {
if(node.data.id)emit('setSelectItem', node.data)
}
watch(()=>props.treeList.length, (newVal, oldVal) => {
nodes.value = []
edges.value = []
props.treeList.forEach(item=>{
push(item)
})
},{immediate:true})
watch(()=>props.selectItem.id, (newVal, oldVal) => {
})
const versionRestore = ()=>{
emit('versionRestore')
dialogRestoreRef.value?.open()
}
const versionDelete = ()=>{
dialogDeleteRef.value?.open()
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({push})
// const {} = toRefs(data);
</script>
<template>
<div class="view2">
<div class="vueFlowBox">
<div @click="toggleNodesDraggable">拖拽节点</div>
<VueFlow :nodes="nodes" @nodes-initialized="initialized" :edges="edges" @node-click="handleVueFlowNodeClick" :nodes-draggable="nodesDraggable">
<template #node-InputNode="nodeProps">
<InputNode v-bind="nodeProps" />
</template>
<template #node-SecondaryNode="nodeProps">
<SecondaryNode
v-bind="nodeProps"
:selectItem="props.selectItem"
/>
</template>
<!-- <template #edge-custom="edgeProps">
<SpecialEdge v-bind="edgeProps" />
</template> -->
</VueFlow>
<div class="btnBox">
<div class="item" @click="versionRestore">
<div class="icon">
<SvgIcon name="versionRestore" size="12" />
</div>
<span>{{ $t('VersionTree.restore') }}</span>
</div>
<div class="item" @click="versionDelete">
<div class="icon">
<SvgIcon name="versionDelete" size="12" />
</div>
<span>{{ $t('VersionTree.delete') }}</span>
</div>
</div>
</div>
<dialogVue
:textData="{
title: $t('VersionTree.deleteChat'),
text: $t('VersionTree.deleteHint'),
submitText: $t('VersionTree.delete'),
cancelText: $t('VersionTree.cancel'),
}"
:styleData="{
width: '40.6rem'
}"
:callBack="()=>emit('versionDelete')"
ref="dialogDeleteRef" />
<dialogVue
:textData="{
title: $t('VersionTree.restoreChat'),
text: $t('VersionTree.restoreHint'),
submitText: $t('VersionTree.confirm'),
cancelText: $t('VersionTree.cancel'),
}"
:styleData="{
width: '40.6rem'
}"
:callBack="()=>emit('versionRestore')"
ref="dialogRestoreRef" />
</div>
</template>
<style lang="less">
@import "@vue-flow/core/dist/style.css";
@import "@vue-flow/core/dist/theme-default.css";
</style>
<style lang="less" scoped>
.view2{
width: 100%;
height: 100%;
overflow: hidden;
>.vueFlowBox{
width: 100%;
height: 100%;
overflow: hidden;
}
:deep(.custom-node){
.node{
--vf-handle: #c1c1c1;
--vf-node-color: #000;
--vf-box-shadow: #000;
font-size: 1.2rem;
width: var(--treeItem-width);
height: var(--treeItem-height);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--treeItem-raduis);
border: var(--treeItem-border);
color: #000;
cursor: pointer;
background-color: var(--treeItem-background);
box-sizing: border-box;
&.active{
background-color: var(--treeItem-active-background);
}
&.start{
background-color: #7A7A7A;
color: #FFF;
border: 2px solid #7A7A7A;
}
}
}
.btnBox{
position: absolute;
z-index: 1;
bottom: 3rem;
right: 2rem;
> .item{
width: 14.5rem;
line-height: 3.3rem;
border-radius: .8rem;
border: 1px solid #d9d9d9;
display: flex;
justify-content: center;
margin-bottom: 2rem;
background-color: #ffffff;
cursor: pointer;
&:hover{
background-color: #f5f5f5;
}
&:last-child{
margin-bottom: 0;
}
> .icon{
margin-right: .4rem;
color: rgba(0, 0, 0, 0.5);
}
> span{
font-weight: 500;
font-size: 1.4rem;
color: rgba(0, 0, 0, 0.5);
}
}
}
}
</style>

View File

@@ -1,31 +0,0 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue'
const props = defineProps<{
data: {
type: Object,
default: () => ({
id: '',
})
},
selectItem: {
type: Object,
default: () => {},
},
}>()
</script>
<!-- source输入target输出 -->
<template>
<div class="node" :class="{active:props.selectItem.id == props.data.id}">
<Handle type="target" id="Top" :position="Position.Top" />
<Handle type="source" id="Bottom" :position="Position.Bottom" />
<!-- <Handle type="source" id="Right" :position="Position.Right" />
<Handle type="target" id="Left" :position="Position.Left" /> -->
<div>{{ props.data.id }}</div>
</div>
</template>
<style lang="less" scoped>
</style>

View File

@@ -1,45 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EdgeProps } from '@vue-flow/core'
import { BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow } from '@vue-flow/core'
const props = defineProps<EdgeProps>()
const { removeEdges } = useVueFlow()
const path = computed(() => getBezierPath(props))
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<BaseEdge :path="path[0]" />
<EdgeLabelRenderer>
<div
:style="{
pointerEvents: 'all',
position: 'absolute',
transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`,
}"
class="nodrag nopan"
>
<button class="edgebutton" @click="removeEdges(id)">×</button>
</div>
</EdgeLabelRenderer>
</template>
<style>
.edgebutton {
border-radius: 999px;
cursor: pointer;
}
.edgebutton:hover {
box-shadow: 0 0 0 2px pink, 0 0 0 4px #f05f75;
}
</style>