Преглед изворни кода

fix: 修改抽屉平铺展示

jiaxing.liao пре 1 недеља
родитељ
комит
cdc24cb31f

+ 76 - 5
apps/web/src/components/ResizableDrawer/index.vue

@@ -1,5 +1,8 @@
 <script lang="ts" setup>
 import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
+import { useDrawerStack } from './useDrawerStack'
+
+const { drawerStack, isAnyResizing, allocateId } = useDrawerStack()
 
 interface Props {
 	visible: boolean
@@ -11,6 +14,8 @@ interface Props {
 	bottom?: number
 	resizable?: boolean
 	zIndex?: number
+	stack?: boolean
+	stackGap?: number
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -22,9 +27,12 @@ const props = withDefaults(defineProps<Props>(), {
 	right: 5,
 	bottom: 10,
 	resizable: true,
-	zIndex: 1000
+	zIndex: 1000,
+	stack: true,
+	stackGap: 14
 })
 
+const drawerId = allocateId()
 const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
 const drawerWidth = ref(props.defaultWidth || props.minWidth)
 const resizeState = reactive({
@@ -41,14 +49,43 @@ const clampDrawerWidth = (width: number) => {
 	return Math.min(Math.max(width, props.minWidth), maxDrawerWidth.value)
 }
 
+const stackItem = computed(() => drawerStack.find((item) => item.id === drawerId))
+
+/**
+ * 计算当前抽屉的 right 偏移量(从右到左平铺)
+ * 排序规则:zIndex 高的在最右侧,相同 zIndex 时 order 大的(后打开的)在最右侧
+ * 每个抽屉的 right = 基础 right + 右侧所有可见抽屉的 (width + gap) 之和
+ */
+const stackedRight = computed(() => {
+	if (!props.stack) return props.right
+
+	const visibleDrawers = drawerStack
+		.filter((item) => item.visible)
+		.sort((a, b) => b.zIndex - a.zIndex || b.order - a.order)
+
+	const currentIndex = visibleDrawers.findIndex((item) => item.id === drawerId)
+	if (currentIndex <= 0) return props.right
+
+	// 累加右侧所有可见抽屉的宽度 + 间距
+	return visibleDrawers.slice(0, currentIndex).reduce((offset, item) => {
+		return offset + item.width + item.stackGap
+	}, props.right)
+})
+
+/** 关闭状态的 translateX,确保完全移出视口 */
+const closedTranslateX = computed(() => {
+	return `calc(100% + ${stackedRight.value}px + 20px)`
+})
+
 const rootStyle = computed(() => ({
 	top: `${props.top}px`,
-	right: `${props.right}px`,
+	right: `${stackedRight.value}px`,
 	bottom: `${props.bottom}px`,
 	width: `${drawerWidth.value}px`,
 	minWidth: `${props.minWidth}px`,
 	maxWidth: `${Math.floor(props.maxWidthRatio * 100)}vw`,
-	zIndex: `${props.zIndex}`
+	zIndex: `${props.zIndex}`,
+	'--drawer-closed-x': closedTranslateX.value
 }))
 
 const syncViewportWidth = () => {
@@ -59,6 +96,7 @@ const syncViewportWidth = () => {
 const stopResize = () => {
 	if (!resizeState.isDragging) return
 	resizeState.isDragging = false
+	isAnyResizing.value = false
 	document.body.style.userSelect = ''
 	document.body.style.cursor = ''
 	window.removeEventListener('mousemove', onResize)
@@ -76,12 +114,23 @@ const onResizeStart = (event: MouseEvent) => {
 	resizeState.startX = event.clientX
 	resizeState.startWidth = drawerWidth.value
 	resizeState.isDragging = true
+	isAnyResizing.value = true
 	document.body.style.userSelect = 'none'
 	document.body.style.cursor = 'ew-resize'
 	window.addEventListener('mousemove', onResize)
 	window.addEventListener('mouseup', stopResize)
 }
 
+const syncStackItem = () => {
+	const item = stackItem.value
+	if (!item) return
+	item.visible = props.visible
+	item.width = drawerWidth.value
+	item.zIndex = props.zIndex
+	item.right = props.right
+	item.stackGap = props.stackGap
+}
+
 watch(
 	() => [props.minWidth, props.maxWidthRatio, props.defaultWidth],
 	() => {
@@ -90,13 +139,35 @@ watch(
 	{ immediate: true }
 )
 
+watch(
+	() => [props.visible, props.zIndex, props.right, props.stackGap, drawerWidth.value],
+	() => {
+		syncStackItem()
+	},
+	{ immediate: true }
+)
+
 onMounted(() => {
+	drawerStack.push({
+		id: drawerId,
+		visible: props.visible,
+		width: drawerWidth.value,
+		zIndex: props.zIndex,
+		right: props.right,
+		order: drawerId,
+		stackGap: props.stackGap
+	})
+	syncStackItem()
 	window.addEventListener('resize', syncViewportWidth)
 	syncViewportWidth()
 })
 
 onBeforeUnmount(() => {
 	stopResize()
+	const index = drawerStack.findIndex((item) => item.id === drawerId)
+	if (index !== -1) {
+		drawerStack.splice(index, 1)
+	}
 	window.removeEventListener('resize', syncViewportWidth)
 })
 
@@ -113,7 +184,7 @@ defineExpose({
 		class="resizable-drawer shadow-2xl"
 		:class="{
 			'resizable-drawer--open': props.visible,
-			'resizable-drawer--resizing': resizeState.isDragging
+			'resizable-drawer--resizing': isAnyResizing
 		}"
 		:style="rootStyle"
 	>
@@ -134,7 +205,7 @@ defineExpose({
 	display: flex;
 	flex-direction: column;
 	border: 1px solid var(--border-light);
-	transform: translateX(110%);
+	transform: translateX(var(--drawer-closed-x, calc(100% + 20px)));
 	transition: transform 0.25s ease;
 
 	&.resizable-drawer--resizing {

+ 26 - 0
apps/web/src/components/ResizableDrawer/useDrawerStack.ts

@@ -0,0 +1,26 @@
+import { reactive, ref } from 'vue'
+
+export interface DrawerStackItem {
+	id: number
+	visible: boolean
+	width: number
+	zIndex: number
+	right: number
+	order: number
+	stackGap: number
+}
+
+// 模块级共享状态 —— 所有 useDrawerStack 调用方共享同一份数据
+let drawerIdSeed = 0
+const drawerStack = reactive<DrawerStackItem[]>([])
+const isAnyResizing = ref(false)
+
+export function useDrawerStack() {
+	const allocateId = () => drawerIdSeed++
+
+	return {
+		drawerStack,
+		isAnyResizing,
+		allocateId
+	}
+}

+ 17 - 2
apps/web/src/features/ChatDrawer/Chat.vue

@@ -439,8 +439,20 @@ const handleRetry = (message: BubbleMessage) => {
 	handleSend(previous.content)
 }
 
+const resetConversation = () => {
+	runnerStore.resetRunner()
+	messages.value = []
+	senderValue.value = ''
+	attachments.value = []
+	uploadDialogVisible.value = false
+	activeAiMessage.value = null
+	handledChatMessageCount.value = 0
+	startingRunner.value = false
+}
+
 defineExpose({
-	scrollToBottom
+	scrollToBottom,
+	resetConversation
 })
 
 watch(
@@ -488,7 +500,6 @@ watch(
 
 .setter-chat__footer {
 	flex-shrink: 0;
-	border-top: 1px solid var(--border-light);
 }
 
 :deep(.chat-content) {
@@ -516,4 +527,8 @@ watch(
 	width: 100%;
 	max-width: min(920px, calc(100vw - 220px));
 }
+
+:deep(.el-sender-footer) {
+	border: none;
+}
 </style>

+ 6 - 4
apps/web/src/features/ChatDrawer/WorkflowTraceBubble.vue

@@ -33,7 +33,9 @@ const toggleWorkflowTrace = () => {
 
 const workflowExpandedNodeIds = computed<string[]>(() =>
 	Array.isArray(props.message.workflowExpandedNodeIds)
-		? props.message.workflowExpandedNodeIds.filter((item): item is string => typeof item === 'string')
+		? props.message.workflowExpandedNodeIds.filter(
+				(item): item is string => typeof item === 'string'
+			)
 		: []
 )
 
@@ -160,7 +162,7 @@ const formatWorkflowValue = (value: unknown) => {
 							<span class="workflow-node-card__name">{{ nodeDisplayName(node) }}</span>
 							<span class="workflow-node-card__duration">{{ nodeUseTime(node) }}</span>
 							<span class="workflow-node-card__status" :class="nodeStatusClass(node.status)">
-								<Icon :icon="nodeStatusIcon(node.status)" :width="16" />
+								<Icon :icon="nodeStatusIcon(node.status)" :width="12" />
 							</span>
 						</div>
 					</template>
@@ -229,8 +231,8 @@ const formatWorkflowValue = (value: unknown) => {
 	display: inline-flex;
 	align-items: center;
 	justify-content: center;
-	width: 18px;
-	height: 18px;
+	width: 14px;
+	height: 14px;
 	border-radius: 999px;
 	color: #fff;
 	background: #11a260;

+ 52 - 12
apps/web/src/features/ChatDrawer/index.vue

@@ -1,11 +1,11 @@
 <script lang="ts" setup>
-import { nextTick, ref, watch } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
 
 import InputTab from '@/features/RunWorkflow/components/InputTab.vue'
 import Chat from './Chat.vue'
 import ResizableDrawer from '@/components/ResizableDrawer/index.vue'
 import { useI18n } from '@/composables/useI18n'
-import { Icon } from '@repo/ui'
+import { Icon, IconButton } from '@repo/ui'
 
 import type { IWorkflow, IWorkflowNode } from '@repo/workflow'
 import type { StartVariable } from '@/nodes/src/start'
@@ -37,8 +37,9 @@ const emit = defineEmits<{
 }>()
 
 const { t } = useI18n()
-const submitted = ref(false)
+const showForm = ref(false)
 const chatContainerRef = ref<InstanceType<typeof Chat>>()
+const hasVisibleForm = computed(() => props.visibleVariables.length > 0)
 
 const closeDrawer = () => {
 	handleDrawerUpdate(false)
@@ -57,7 +58,16 @@ const handleDrawerUpdate = (value: boolean) => {
 }
 
 const handleFirstSend = () => {
-	submitted.value = true
+	if (hasVisibleForm.value) {
+		showForm.value = false
+	}
+}
+
+const handleResetConversation = () => {
+	chatContainerRef.value?.resetConversation?.()
+	if (hasVisibleForm.value) {
+		showForm.value = true
+	}
 }
 
 const handleCancel = () => {
@@ -65,11 +75,10 @@ const handleCancel = () => {
 }
 
 watch(
-	() => props.visible,
-	(visible) => {
-		if (!visible) {
-			submitted.value = false
-		}
+	() => [props.visible, hasVisibleForm.value],
+	([visible, hasForm]) => {
+		if (!visible) return
+		showForm.value = !!hasForm
 	},
 	{ immediate: true }
 )
@@ -77,7 +86,7 @@ watch(
 defineExpose({
 	scrollToBottom: () => {
 		nextTick(() => {
-			chatContainerRef.value?.$el?.querySelector?.('.chat-content')?.scrollTo?.(0, 999999)
+			chatContainerRef.value?.scrollToBottom?.()
 		})
 	}
 })
@@ -95,11 +104,40 @@ defineExpose({
 		>
 			<header class="chat-drawer__header">
 				<h4>{{ t('pages.runWorkflow.chatDialogTitle') }}</h4>
-				<Icon icon="lucide:x" height="24" width="24" class="cursor-pointer" @click="closeDrawer" />
+				<span class="flex items-center gap-16px">
+					<!-- 重置按钮 -->
+					<Icon
+						icon="lucide:refresh-ccw"
+						height="18"
+						width="18"
+						class="cursor-pointer"
+						@click="handleResetConversation"
+					/>
+					<!-- 展示/隐藏表单 -->
+					<IconButton
+						v-if="hasVisibleForm"
+						icon="lucide:sliders-horizontal"
+						:type="showForm ? 'primary' : 'default'"
+						:icon-color="showForm ? '#fff' : undefined"
+						height="18"
+						width="18"
+						@click="showForm = !showForm"
+						size="small"
+						square
+					/>
+					<!-- 关闭 -->
+					<Icon
+						icon="lucide:x"
+						height="18"
+						width="18"
+						class="cursor-pointer"
+						@click="closeDrawer"
+					/>
+				</span>
 			</header>
 
 			<div class="chat-drawer__body">
-				<div v-if="!submitted" class="chat-drawer__form">
+				<div v-if="hasVisibleForm && showForm" class="chat-drawer__form">
 					<el-card>
 						<InputTab
 							:start-node="startNode"
@@ -158,6 +196,8 @@ defineExpose({
 	flex-direction: column;
 	gap: 12px;
 	overflow: hidden;
+	border-radius: 8px;
+	overflow-y: auto;
 }
 
 .chat-drawer__form {

+ 1 - 1
apps/web/src/i18n/locales/zh-cn.ts

@@ -1207,7 +1207,7 @@ export default {
 		toolbar: {
 			nodes: '节点',
 			note: '注释',
-			chat: '对话',
+			chat: '对话预览',
 			env: '环境变量',
 			runEntry: '选择运行入口',
 			envDialog: {