index.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <script lang="ts" setup>
  2. import { computed, onBeforeUnmount, onMounted, provide, reactive, ref, watch } from 'vue'
  3. import { Icon, Input } from '@repo/ui'
  4. import { useDebounceFn } from '@vueuse/core'
  5. import { agent } from '@repo/api-service'
  6. import NodeLog from './NodeLog.vue'
  7. import { nodeMap } from '@/nodes'
  8. import { useI18n } from '@/composables/useI18n'
  9. import type { IWorkflow, IWorkflowNode } from '@repo/workflow'
  10. import type { NodeVar } from '@/types/var'
  11. interface Props {
  12. id: string
  13. workflow: IWorkflow
  14. visible: boolean
  15. activeTab?: 'setting' | 'last-run'
  16. }
  17. const props = withDefaults(defineProps<Props>(), {
  18. visible: false,
  19. id: '',
  20. activeTab: 'setting'
  21. })
  22. const emit = defineEmits<{
  23. 'update:visible': [value: boolean]
  24. 'update:node:data': [data: IWorkflowNode]
  25. 'run-node': [id: string]
  26. }>()
  27. const { t } = useI18n()
  28. const node = computed<IWorkflowNode>(
  29. () => props.workflow.nodes.find((item) => item.id === props.id)!
  30. )
  31. const setter = computed(() => {
  32. return node.value?.data?.nodeType
  33. ? nodeMap[node.value.data.nodeType as keyof typeof nodeMap]?.Setter
  34. : undefined
  35. })
  36. const nodeInfo = computed(() => {
  37. return node.value?.data?.nodeType
  38. ? nodeMap[node.value.data.nodeType as keyof typeof nodeMap]
  39. : undefined
  40. })
  41. const isImageIcon = computed(() => {
  42. return !!nodeInfo.value?.icon && nodeInfo.value.icon.startsWith('data:image/')
  43. })
  44. const closeDrawer = () => {
  45. emit('update:visible', false)
  46. }
  47. const onUpdateData = useDebounceFn((data: Record<string, unknown>) => {
  48. emit('update:node:data', {
  49. ...node.value,
  50. data: {
  51. ...node.value?.data,
  52. ...data
  53. }
  54. })
  55. }, 1000)
  56. const name = ref(node.value?.name || '')
  57. const remark = ref(node.value?.remark || '')
  58. const nodeVars = ref<NodeVar[]>([])
  59. const currentTab = ref<'setting' | 'last-run'>(props.activeTab || 'setting')
  60. const MIN_DRAWER_WIDTH = 420
  61. const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1440)
  62. const drawerWidth = ref(MIN_DRAWER_WIDTH)
  63. const resizeState = reactive({
  64. startX: 0,
  65. startWidth: MIN_DRAWER_WIDTH,
  66. isDragging: false
  67. })
  68. const maxDrawerWidth = computed(() => {
  69. return Math.max(MIN_DRAWER_WIDTH, Math.floor(viewportWidth.value * 0.6))
  70. })
  71. const clampDrawerWidth = (width: number) => {
  72. return Math.min(Math.max(width, MIN_DRAWER_WIDTH), maxDrawerWidth.value)
  73. }
  74. const syncViewportWidth = () => {
  75. viewportWidth.value = window.innerWidth
  76. drawerWidth.value = clampDrawerWidth(drawerWidth.value)
  77. }
  78. const stopResize = () => {
  79. if (!resizeState.isDragging) return
  80. resizeState.isDragging = false
  81. document.body.style.userSelect = ''
  82. document.body.style.cursor = ''
  83. window.removeEventListener('mousemove', onResize)
  84. window.removeEventListener('mouseup', stopResize)
  85. }
  86. const onResize = (event: MouseEvent) => {
  87. if (!resizeState.isDragging) return
  88. const nextWidth = resizeState.startWidth + (resizeState.startX - event.clientX)
  89. drawerWidth.value = clampDrawerWidth(nextWidth)
  90. }
  91. const onResizeStart = (event: MouseEvent) => {
  92. resizeState.startX = event.clientX
  93. resizeState.startWidth = drawerWidth.value
  94. resizeState.isDragging = true
  95. document.body.style.userSelect = 'none'
  96. document.body.style.cursor = 'ew-resize'
  97. window.addEventListener('mousemove', onResize)
  98. window.addEventListener('mouseup', stopResize)
  99. }
  100. const onUpdateName = () => {
  101. if (name.value !== node.value?.name && name.value.trim() !== '') {
  102. emit('update:node:data', { ...node.value, name: name.value })
  103. }
  104. }
  105. const onUpdateRemark = () => {
  106. emit('update:node:data', { ...node.value, remark: remark.value })
  107. }
  108. watch(
  109. () => [props.id, props.visible],
  110. async () => {
  111. name.value = node.value?.name || ''
  112. remark.value = node.value?.remark || ''
  113. currentTab.value = props.activeTab || 'setting'
  114. if (props.id && props.visible) {
  115. const response = await agent.postAgentGetPrevNodeOutVariableList({
  116. node_id: props.id,
  117. varTypeList: []
  118. })
  119. nodeVars.value = (response.result as NodeVar[]) || []
  120. }
  121. }
  122. )
  123. watch(
  124. () => props.activeTab,
  125. (value) => {
  126. currentTab.value = value || 'setting'
  127. }
  128. )
  129. onMounted(() => {
  130. window.addEventListener('resize', syncViewportWidth)
  131. syncViewportWidth()
  132. })
  133. onBeforeUnmount(() => {
  134. stopResize()
  135. window.removeEventListener('resize', syncViewportWidth)
  136. })
  137. provide('nodeVars', nodeVars)
  138. </script>
  139. <template>
  140. <div class="setter">
  141. <div
  142. class="drawer shadow-2xl"
  143. :class="{
  144. 'drawer--open': props.visible && setter,
  145. 'drawer--resizing': resizeState.isDragging
  146. }"
  147. :style="{ width: `${drawerWidth}px`, maxWidth: '60vw' }"
  148. >
  149. <!-- Resize handle -->
  150. <div class="resize-handle" @mousedown.prevent="onResizeStart"></div>
  151. <header class="text-gray-800">
  152. <div class="w-full flex items-center justify-between">
  153. <h4 class="flex items-center">
  154. <span
  155. v-if="nodeInfo?.icon"
  156. class="h-22px w-22px flex items-center justify-center rounded-lg shrink-0"
  157. :style="{ background: nodeInfo?.iconColor }"
  158. >
  159. <img
  160. v-if="isImageIcon"
  161. :src="nodeInfo?.icon"
  162. alt="node icon"
  163. class="w-14px h-14px object-contain"
  164. />
  165. <Icon v-else :icon="nodeInfo?.icon" color="#fff" :size="14" />
  166. </span>
  167. <Input
  168. v-model="name"
  169. :placeholder="t('pages.setter.titlePlaceholder')"
  170. variant="borderless"
  171. @blur="onUpdateName"
  172. />
  173. </h4>
  174. <div class="flex items-center">
  175. <el-tooltip :content="t('pages.setter.runNode')" placement="top">
  176. <Icon
  177. icon="lucide:play"
  178. width="20"
  179. height="20"
  180. class="text-gray-400 p-2 hover:cursor-pointer hover:bg-gray-200"
  181. @click="emit('run-node', id)"
  182. />
  183. </el-tooltip>
  184. <el-divider direction="vertical" />
  185. <Icon
  186. icon="lucide:x"
  187. height="24"
  188. width="24"
  189. @click="closeDrawer"
  190. class="cursor-pointer"
  191. />
  192. </div>
  193. </div>
  194. <Input
  195. v-model="remark"
  196. :placeholder="t('pages.setter.descriptionPlaceholder')"
  197. variant="borderless"
  198. @blur="onUpdateRemark"
  199. />
  200. </header>
  201. <div class="content">
  202. <el-tabs v-model="currentTab">
  203. <el-tab-pane :label="t('pages.setter.setting')" name="setting">
  204. <div class="tab-pane tab-pane--fill">
  205. <component
  206. :is="setter"
  207. :key="node?.id"
  208. :id="node?.id"
  209. :data="node?.data"
  210. @update="onUpdateData"
  211. />
  212. </div>
  213. </el-tab-pane>
  214. <el-tab-pane :label="t('pages.setter.lastRun')" name="last-run">
  215. <div class="tab-pane tab-pane--scroll">
  216. <NodeLog :node="node" :active="props.visible && currentTab === 'last-run'" />
  217. </div>
  218. </el-tab-pane>
  219. </el-tabs>
  220. </div>
  221. </div>
  222. </div>
  223. </template>
  224. <style lang="less" scoped>
  225. .setter {
  226. z-index: 998;
  227. .drawer {
  228. position: fixed;
  229. top: 60px;
  230. right: 5px;
  231. bottom: 10px;
  232. min-width: 420px;
  233. background: #fff;
  234. z-index: 1000;
  235. border-radius: 8px;
  236. display: flex;
  237. flex-direction: column;
  238. border: 1px solid #e4e4e4;
  239. transform: translateX(110%);
  240. transition: transform 0.25s ease;
  241. &.drawer--resizing {
  242. transition: none;
  243. }
  244. .resize-handle {
  245. position: absolute;
  246. left: -4px;
  247. top: 50%;
  248. transform: translateY(-50%);
  249. width: 3px;
  250. height: 32px;
  251. background-color: #c1c4cb;
  252. border-radius: 8px;
  253. cursor: ew-resize;
  254. }
  255. }
  256. .drawer--open {
  257. transform: translateX(0);
  258. }
  259. .drawer header {
  260. height: 66px;
  261. padding: 16px;
  262. padding-bottom: 0;
  263. h4 {
  264. margin: 0;
  265. }
  266. }
  267. .drawer .content {
  268. flex: 1;
  269. min-height: 0;
  270. overflow: hidden;
  271. }
  272. :deep(.el-collapse-item__header) {
  273. box-sizing: border-box;
  274. padding: 0 8px;
  275. }
  276. :deep(.el-collapse-item__content) {
  277. box-sizing: border-box;
  278. padding: 0 8px 12px;
  279. }
  280. :deep(.el-tabs__nav-scroll) {
  281. padding-left: 20px;
  282. }
  283. :deep(.el-tabs) {
  284. height: 100%;
  285. display: flex;
  286. flex-direction: column;
  287. }
  288. :deep(.el-tabs__header) {
  289. flex-shrink: 0;
  290. margin-bottom: 0;
  291. }
  292. :deep(.el-tabs__content) {
  293. flex: 1;
  294. min-height: 0;
  295. overflow: hidden;
  296. }
  297. :deep(.el-tab-pane) {
  298. height: 100%;
  299. }
  300. .tab-pane {
  301. height: 100%;
  302. min-height: 0;
  303. margin-top: 16px;
  304. }
  305. .tab-pane--fill {
  306. display: flex;
  307. flex-direction: column;
  308. overflow-x: hidden;
  309. overflow-y: auto;
  310. }
  311. :deep(.tab-pane--fill > .el-scrollbar) {
  312. height: 100%;
  313. }
  314. .tab-pane--scroll {
  315. overflow-y: auto;
  316. }
  317. }
  318. </style>