| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- <script lang="ts" setup>
- import { computed, onBeforeUnmount, onMounted, provide, reactive, ref, watch } from 'vue'
- import { Icon, Input } from '@repo/ui'
- import { useDebounceFn } from '@vueuse/core'
- import { agent } from '@repo/api-service'
- import NodeLog from './NodeLog.vue'
- import { nodeMap } from '@/nodes'
- import { useI18n } from '@/composables/useI18n'
- import type { IWorkflow, IWorkflowNode } from '@repo/workflow'
- import type { NodeVar } from '@/types/var'
- interface Props {
- id: string
- workflow: IWorkflow
- visible: boolean
- activeTab?: 'setting' | 'last-run'
- }
- const props = withDefaults(defineProps<Props>(), {
- visible: false,
- id: '',
- activeTab: 'setting'
- })
- const emit = defineEmits<{
- 'update:visible': [value: boolean]
- 'update:node:data': [data: IWorkflowNode]
- 'run-node': [id: string]
- }>()
- const { t } = useI18n()
- const node = computed<IWorkflowNode>(
- () => props.workflow.nodes.find((item) => item.id === props.id)!
- )
- const setter = computed(() => {
- return node.value?.data?.nodeType
- ? nodeMap[node.value.data.nodeType as keyof typeof nodeMap]?.Setter
- : undefined
- })
- const nodeInfo = computed(() => {
- return node.value?.data?.nodeType
- ? nodeMap[node.value.data.nodeType as keyof typeof nodeMap]
- : undefined
- })
- const isImageIcon = computed(() => {
- return !!nodeInfo.value?.icon && nodeInfo.value.icon.startsWith('data:image/')
- })
- const closeDrawer = () => {
- emit('update:visible', false)
- }
- const onUpdateData = useDebounceFn((data: Record<string, unknown>) => {
- emit('update:node:data', {
- ...node.value,
- data: {
- ...node.value?.data,
- ...data
- }
- })
- }, 1000)
- const name = ref(node.value?.name || '')
- const remark = ref(node.value?.remark || '')
- const nodeVars = ref<NodeVar[]>([])
- const currentTab = ref<'setting' | 'last-run'>(props.activeTab || 'setting')
- const MIN_DRAWER_WIDTH = 420
- const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
- const drawerWidth = ref(MIN_DRAWER_WIDTH)
- const resizeState = reactive({
- startX: 0,
- startWidth: MIN_DRAWER_WIDTH,
- isDragging: false
- })
- const maxDrawerWidth = computed(() => {
- return Math.max(MIN_DRAWER_WIDTH, Math.floor(viewportWidth.value * 0.6))
- })
- const clampDrawerWidth = (width: number) => {
- return Math.min(Math.max(width, MIN_DRAWER_WIDTH), maxDrawerWidth.value)
- }
- const syncViewportWidth = () => {
- viewportWidth.value = window.innerWidth
- drawerWidth.value = clampDrawerWidth(drawerWidth.value)
- }
- const stopResize = () => {
- if (!resizeState.isDragging) return
- resizeState.isDragging = false
- document.body.style.userSelect = ''
- document.body.style.cursor = ''
- window.removeEventListener('mousemove', onResize)
- window.removeEventListener('mouseup', stopResize)
- }
- const onResize = (event: MouseEvent) => {
- if (!resizeState.isDragging) return
- const nextWidth = resizeState.startWidth + (resizeState.startX - event.clientX)
- drawerWidth.value = clampDrawerWidth(nextWidth)
- }
- const onResizeStart = (event: MouseEvent) => {
- resizeState.startX = event.clientX
- resizeState.startWidth = drawerWidth.value
- resizeState.isDragging = true
- document.body.style.userSelect = 'none'
- document.body.style.cursor = 'ew-resize'
- window.addEventListener('mousemove', onResize)
- window.addEventListener('mouseup', stopResize)
- }
- const onUpdateName = () => {
- if (name.value !== node.value?.name && name.value.trim() !== '') {
- emit('update:node:data', { ...node.value, name: name.value })
- }
- }
- const onUpdateRemark = () => {
- emit('update:node:data', { ...node.value, remark: remark.value })
- }
- watch(
- () => [props.id, props.visible],
- async () => {
- name.value = node.value?.name || ''
- remark.value = node.value?.remark || ''
- currentTab.value = props.activeTab || 'setting'
- if (props.id && props.visible) {
- const response = await agent.postAgentGetPrevNodeOutVariableList({
- node_id: props.id,
- varTypeList: []
- })
- nodeVars.value = (response.result as NodeVar[]) || []
- }
- }
- )
- watch(
- () => props.activeTab,
- (value) => {
- currentTab.value = value || 'setting'
- }
- )
- onMounted(() => {
- window.addEventListener('resize', syncViewportWidth)
- syncViewportWidth()
- })
- onBeforeUnmount(() => {
- stopResize()
- window.removeEventListener('resize', syncViewportWidth)
- })
- provide('nodeVars', nodeVars)
- </script>
- <template>
- <div class="setter">
- <div
- class="drawer shadow-2xl"
- :class="{
- 'drawer--open': props.visible && setter,
- 'drawer--resizing': resizeState.isDragging
- }"
- :style="{ width: `${drawerWidth}px`, maxWidth: '60vw' }"
- >
- <!-- Resize handle -->
- <div class="resize-handle" @mousedown.prevent="onResizeStart"></div>
- <header class="text-gray-800">
- <div class="w-full flex items-center justify-between">
- <h4 class="flex items-center">
- <span
- v-if="nodeInfo?.icon"
- class="h-22px w-22px flex items-center justify-center rounded-lg shrink-0"
- :style="{ background: nodeInfo?.iconColor }"
- >
- <img
- v-if="isImageIcon"
- :src="nodeInfo?.icon"
- alt="node icon"
- class="w-14px h-14px object-contain"
- />
- <Icon v-else :icon="nodeInfo?.icon" color="#fff" :size="14" />
- </span>
- <Input
- v-model="name"
- :placeholder="t('pages.setter.titlePlaceholder')"
- variant="borderless"
- @blur="onUpdateName"
- />
- </h4>
- <div class="flex items-center">
- <el-tooltip :content="t('pages.setter.runNode')" placement="top">
- <Icon
- icon="lucide:play"
- width="20"
- height="20"
- class="text-gray-400 p-2 hover:cursor-pointer hover:bg-gray-200"
- @click="emit('run-node', id)"
- />
- </el-tooltip>
- <el-divider direction="vertical" />
- <Icon
- icon="lucide:x"
- height="24"
- width="24"
- @click="closeDrawer"
- class="cursor-pointer"
- />
- </div>
- </div>
- <Input
- v-model="remark"
- :placeholder="t('pages.setter.descriptionPlaceholder')"
- variant="borderless"
- @blur="onUpdateRemark"
- />
- </header>
- <div class="content">
- <el-tabs v-model="currentTab">
- <el-tab-pane :label="t('pages.setter.setting')" name="setting">
- <div class="tab-pane tab-pane--fill">
- <component
- :is="setter"
- :key="node?.id"
- :id="node?.id"
- :data="node?.data"
- @update="onUpdateData"
- />
- </div>
- </el-tab-pane>
- <el-tab-pane :label="t('pages.setter.lastRun')" name="last-run">
- <div class="tab-pane tab-pane--scroll">
- <NodeLog :node="node" :active="props.visible && currentTab === 'last-run'" />
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </div>
- </div>
- </template>
- <style lang="less" scoped>
- .setter {
- z-index: 998;
- .drawer {
- position: fixed;
- top: 60px;
- right: 5px;
- bottom: 10px;
- min-width: 420px;
- background: #fff;
- z-index: 1000;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- border: 1px solid #e4e4e4;
- transform: translateX(110%);
- transition: transform 0.25s ease;
- &.drawer--resizing {
- transition: none;
- }
- .resize-handle {
- position: absolute;
- left: -4px;
- top: 50%;
- transform: translateY(-50%);
- width: 3px;
- height: 32px;
- background-color: #c1c4cb;
- border-radius: 8px;
- cursor: ew-resize;
- }
- }
- .drawer--open {
- transform: translateX(0);
- }
- .drawer header {
- height: 66px;
- padding: 16px;
- padding-bottom: 0;
- h4 {
- margin: 0;
- }
- }
- .drawer .content {
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- :deep(.el-collapse-item__header) {
- box-sizing: border-box;
- padding: 0 8px;
- }
- :deep(.el-collapse-item__content) {
- box-sizing: border-box;
- padding: 0 8px 12px;
- }
- :deep(.el-tabs__nav-scroll) {
- padding-left: 20px;
- }
- :deep(.el-tabs) {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- :deep(.el-tabs__header) {
- flex-shrink: 0;
- margin-bottom: 0;
- }
- :deep(.el-tabs__content) {
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- :deep(.el-tab-pane) {
- height: 100%;
- }
- .tab-pane {
- height: 100%;
- min-height: 0;
- margin-top: 16px;
- }
- .tab-pane--fill {
- display: flex;
- flex-direction: column;
- overflow-x: hidden;
- overflow-y: auto;
- }
- :deep(.tab-pane--fill > .el-scrollbar) {
- height: 100%;
- }
- .tab-pane--scroll {
- overflow-y: auto;
- }
- }
- </style>
|