Quellcode durchsuchen

feat: 添加流程本地缓存

jiaxing.liao vor 3 Wochen
Ursprung
Commit
570148c1fb

+ 1 - 0
apps/web/components.d.ts

@@ -42,6 +42,7 @@ declare module 'vue' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElInputTag: typeof import('element-plus/es')['ElInputTag']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']

+ 3 - 1
apps/web/src/components/Sidebar/index.vue

@@ -145,6 +145,7 @@
 import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
 import SearchDialog from '../SearchDialog/index.vue'
+import { v4 } from 'uuid'
 
 const router = useRouter()
 const collapsed = ref(false)
@@ -158,7 +159,8 @@ const toggle = () => {
 }
 
 const createWorkflow = () => {
-	router.push('/workflow/0')
+	const id = v4()
+	router.push(`/workflow/${id}`)
 }
 
 const createCertificate = () => {}

+ 1 - 1
apps/web/src/components/setter/HttpSetter.vue

@@ -202,7 +202,7 @@ const handleDeleteBody = (index: number) => {
 						placeholder="请选择"
 					>
 					</el-select>
-					<el-input class="flex-1" v-model="formData.url" placeholder="请输入"></el-input>
+					<el-input class="flex-1" v-model="formData.url" placeholder="URL..."></el-input>
 				</div>
 			</el-form-item>
 

+ 14 - 1
apps/web/src/views/Dashboard.vue

@@ -493,6 +493,7 @@ import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Search } from '@element-plus/icons-vue'
 import { useDashboardStore } from '@/stores/dashboard'
+import { v4 } from 'uuid'
 
 const $router = useRouter()
 const dashboardStore = useDashboardStore()
@@ -512,7 +513,10 @@ const buttonConfig = computed(() => {
 
 const handleMenuClick = (text: string) => {
 	const actionMap: Record<string, () => void> = {
-		创建工作流程: () => $router.push('/workflow/0'),
+		创建工作流程: () => {
+			const id = v4()
+			$router.push(`/workflow/${id}`)
+		},
 		创建凭证: () => console.log('创建凭证'),
 		创建变量: () => {
 			dashboardStore.setActiveTab('vars')
@@ -535,7 +539,16 @@ const cards = [
 	{ title: '运行时间(平均)', value: '0s' }
 ]
 
+const projectMap = JSON.parse(localStorage.getItem(`workflow-map`) || '{}')
+
 const workflows = ref([
+	...Object.entries(projectMap)
+		.map(([_id, workflow]) => workflow)
+		.map((item: any) => ({
+			...item,
+			id: item.id,
+			title: item.name
+		})),
 	{ id: 1, title: '与新同事人才交流', created: '1 月 23 日', description: '', icon: '' },
 	{
 		id: 2,

+ 91 - 22
apps/web/src/views/Editor.vue

@@ -4,11 +4,33 @@
 			class="h-60px shrink-0 border-b border-b-solid border-gray-200 flex items-center justify-between px-12px"
 		>
 			<div class="left flex items-center gap-4">
-				<el-breadcrumb separator="/">
+				<el-breadcrumb separator="/" class="flex items-center">
 					<el-breadcrumb-item>Workspace</el-breadcrumb-item>
-					<el-breadcrumb-item>workflow_1</el-breadcrumb-item>
+					<el-breadcrumb-item>
+						<Input ref="inputRef" v-model="workflow.name" variant="borderless" />
+					</el-breadcrumb-item>
 				</el-breadcrumb>
-				<IconButton icon="iconoir:plus" type="primary" link>标签</IconButton>
+				<div class="flex gap-2" v-show="!showTagInput" @click="showTagInput = true">
+					<el-tag type="info" v-for="tag in workflow.tags" :key="tag" :disable-transitions="false">
+						{{ tag }}
+					</el-tag>
+				</div>
+				<el-input-tag
+					v-show="showTagInput"
+					v-model="workflow.tags"
+					placeholder="按回车键添加标签"
+					aria-label="按回车键添加标签"
+					:max-tags="5"
+					@blur="showTagInput = false"
+				/>
+				<IconButton
+					v-if="!workflow.tags?.length && !showTagInput"
+					icon="iconoir:plus"
+					type="primary"
+					link
+					@click="showTagInput = true"
+					>标签</IconButton
+				>
 			</div>
 			<div class="right flex items-center gap-2">
 				<el-button type="default" size="small">发布</el-button>
@@ -18,8 +40,8 @@
 					<template #dropdown>
 						<el-dropdown-item>描述</el-dropdown-item>
 						<el-dropdown-item>复用</el-dropdown-item>
-						<el-dropdown-item>重命名</el-dropdown-item>
-						<el-dropdown-item divided>删除</el-dropdown-item>
+						<el-dropdown-item @click="handleRename">重命名</el-dropdown-item>
+						<el-dropdown-item divided @click="handleDelete">删除</el-dropdown-item>
 					</template>
 				</el-dropdown>
 			</div>
@@ -40,7 +62,7 @@
 				<RunWorkflow v-model:visible="runVisible" />
 				<Setter
 					:id="nodeID"
-					:workflow="workflow"
+					:workflow="workflow!"
 					@update:node:data="hangleUpdateNodeData"
 					v-model:visible="setterVisible"
 				/>
@@ -54,18 +76,20 @@
 </template>
 
 <script setup lang="ts">
-import { ref, inject, type CSSProperties, onBeforeUnmount } from 'vue'
+import { ref, inject, type CSSProperties, onBeforeUnmount, watch } from 'vue'
 import { startNode, endNode, httpNode, conditionNode, databaseNode, codeNode } from '@repo/nodes'
 import { Workflow, type IWorkflow, type XYPosition, type Connection } from '@repo/workflow'
 import { v4 as uuid } from 'uuid'
+import { useRoute, useRouter } from 'vue-router'
 
 import Setter from '@/components/setter/index.vue'
 import RunWorkflow from '@/components/RunWorkflow/index.vue'
 import EditorFooter from '@/features/editorFooter/index.vue'
 
-import { IconButton } from '@repo/ui'
+import { IconButton, Input } from '@repo/ui'
 
 import type { SourceType } from '@repo/nodes'
+import { dayjs, ElMessageBox } from 'element-plus'
 
 const layout = inject<{ setMainStyle: (style: CSSProperties) => void }>('layout')
 
@@ -74,13 +98,36 @@ layout?.setMainStyle({
 })
 
 const footerHeight = ref(32)
+const route = useRoute()
+const router = useRouter()
+const id = (route.params?.id as string) || uuid()
+const projectMap = JSON.parse(localStorage.getItem(`workflow-map`) || '{}') as Record<
+	string,
+	IWorkflow
+>
+const showTagInput = ref(false)
 
-const workflow = ref<IWorkflow>({
-	id: uuid(),
-	nodes: [startNode, endNode],
-	edges: []
-})
+const workflow = ref<IWorkflow>(
+	projectMap?.[id]
+		? projectMap[id]
+		: {
+				id,
+				name: 'workflow_1',
+				created: dayjs().format('MM 月 DD 日'),
+				nodes: [startNode, endNode],
+				edges: []
+			}
+)
+const inputRef = ref<InstanceType<typeof Input>>()
 
+watch(
+	() => workflow.value,
+	(workflow) => {
+		projectMap[id] = workflow
+		localStorage.setItem(`workflow-map`, JSON.stringify(projectMap))
+	},
+	{ deep: true }
+)
 /**
  * Editor
  */
@@ -96,13 +143,12 @@ const setterVisible = ref(false)
 const runVisible = ref(false)
 const handleRunWorkflow = () => {
 	runVisible.value = true
-	console.log('run workflow')
 }
 const handleNodeCreate = (value: SourceType | string) => {
 	const id = uuid()
 	if (typeof value === 'string') {
 		if (value === 'stickyNote') {
-			workflow.value.nodes.push({
+			workflow.value?.nodes.push({
 				id,
 				type: 'canvas-node',
 				zIndex: -1,
@@ -135,7 +181,7 @@ const handleNodeCreate = (value: SourceType | string) => {
 
 	// 如果存在对应节点则添加
 	if (nodeToAdd) {
-		workflow.value.nodes.push({
+		workflow.value?.nodes.push({
 			...nodeToAdd,
 			data: {
 				...nodeToAdd.data,
@@ -145,7 +191,7 @@ const handleNodeCreate = (value: SourceType | string) => {
 			id: uuid()
 		})
 	}
-	console.log(workflow.value.nodes, 'workflow.nodes')
+	console.log(workflow.value?.nodes, 'workflow.nodes')
 }
 const handleNodeClick = (id: string, position: XYPosition) => {
 	nodeID.value = id
@@ -156,14 +202,37 @@ const handleDrop = (position: XYPosition, event: DragEvent) => {
 	console.log('drag and drop at', position, event)
 }
 
+/**
+ * 修改工作流名称
+ */
+const handleRename = () => {
+	inputRef.value?.focus()
+	inputRef.value?.select()
+}
+
+/**
+ * 删除工作流
+ */
+const handleDelete = () => {
+	ElMessageBox.confirm('确定要删除吗?', '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	}).then(() => {
+		console.log('删除成功')
+		localStorage.removeItem(`project_${id}`)
+		router.push('/')
+	})
+}
+
 /**
  * 创建连线
  */
 const onCreateConnection = (connection: Connection) => {
 	const { source, target } = connection
 
-	if (!workflow.value.edges.some((edge) => edge.source === source && edge.target === target)) {
-		workflow.value.edges.push({
+	if (!workflow.value?.edges.some((edge) => edge.source === source && edge.target === target)) {
+		workflow.value?.edges.push({
 			id: `edge-${source}-${target}`,
 			source,
 			target,
@@ -178,7 +247,7 @@ const onCreateConnection = (connection: Connection) => {
  */
 const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
 	events?.forEach(({ id, position }) => {
-		const node = workflow.value.nodes.find((node) => node.id === id)
+		const node = workflow.value?.nodes.find((node) => node.id === id)
 		if (node) {
 			node.position = position
 		}
@@ -189,7 +258,7 @@ const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[
  * 修改节点数据
  */
 const hangleUpdateNodeData = (id: string, data: any) => {
-	const node = workflow.value.nodes.find((node) => node.id === id)
+	const node = workflow.value?.nodes.find((node) => node.id === id)
 	if (node) {
 		node.data = {
 			...node.data,
@@ -203,7 +272,7 @@ const hangleUpdateNodeData = (id: string, data: any) => {
  * 修改节点属性
  */
 const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
-	const node = workflow.value.nodes.find((node) => node.id === id)
+	const node = workflow.value?.nodes.find((node) => node.id === id)
 	if (node) {
 		if (node.data?.renderType === 'stickyNote') {
 			Object.assign(node.data, attrs)

+ 19 - 1
packages/ui/components/input/Input.vue

@@ -1,14 +1,32 @@
 <template>
-	<el-input v-bind="$attrs" :class="[variant]" />
+	<el-input ref="input" v-bind="$attrs" :class="[variant]" />
 </template>
 
 <script setup lang="ts">
+import { ref } from 'vue'
+import type { InputInstance } from 'element-plus'
+
+const input = ref<InputInstance>()
+
 withDefaults(
 	defineProps<{
 		variant?: 'outlined' | 'borderless' | 'filled' | 'underline'
 	}>(),
 	{ variant: 'outlined' }
 )
+
+defineExpose({
+	...(input.value || {}),
+	focus() {
+		input.value?.focus()
+	},
+	blur() {
+		input.value?.blur()
+	},
+	select() {
+		input.value?.select()
+	}
+})
 </script>
 
 <style lang="less" scoped>

+ 4 - 0
packages/workflow/src/Interface.ts

@@ -38,6 +38,10 @@ export type IWorkflowEdge = DefaultEdge<{
 
 export interface IWorkflow {
 	id: string
+	name: string
+	tags?: string[]
+	description?: string
+	status?: 'published' | 'draft' | 'deleted'
 	nodes: IWorkflowNode[]
 	edges: IWorkflowEdge[]
 	[key: string]: any

+ 11 - 1
packages/workflow/src/components/Canvas.vue

@@ -48,6 +48,7 @@ const emit = defineEmits<{
 	'update:has-range-selection': [isActive: boolean]
 	'click:node': [id: string, position: XYPosition]
 	'click:node:add': [id: string, handle: string]
+	'initialized:nodes': []
 	'run:node': [id: string]
 	'copy:production:url': [id: string]
 	'copy:test:url': [id: string]
@@ -215,6 +216,15 @@ function onClickConnectionAdd(connection: Connection) {
 	emit('click:connection:add', connection)
 }
 
+let loaded = false
+function onNodesInitialized() {
+	if (!loaded) {
+		onZoomToFit()
+		loaded = true
+		emit('initialized:nodes')
+	}
+}
+
 /**
  * Handle
  */
@@ -242,7 +252,7 @@ onMounted(() => {
 		@connect="onConnect"
 		@connect-start="onConnectStart"
 		@connect-end="onConnectEnd"
-		@nodes-initialized="onZoomToFit"
+		@nodes-initialized="onNodesInitialized"
 		v-bind="$attrs"
 	>
 		<template #node-canvas-node="nodeProps">