Forráskód Böngészése

feat: 修改左侧边栏/快速搜索/折叠/主页面tabs内容并跳转页面

Mickey Mike 3 hete
szülő
commit
b43733ee0b

+ 75 - 0
apps/web/auto-imports.d.ts

@@ -0,0 +1,75 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+  const EffectScope: typeof import('vue').EffectScope
+  const ElMessage: typeof import('element-plus/es').ElMessage
+  const ElMessageBox: typeof import('element-plus/es').ElMessageBox
+  const computed: typeof import('vue').computed
+  const createApp: typeof import('vue').createApp
+  const customRef: typeof import('vue').customRef
+  const defineAsyncComponent: typeof import('vue').defineAsyncComponent
+  const defineComponent: typeof import('vue').defineComponent
+  const effectScope: typeof import('vue').effectScope
+  const getCurrentInstance: typeof import('vue').getCurrentInstance
+  const getCurrentScope: typeof import('vue').getCurrentScope
+  const getCurrentWatcher: typeof import('vue').getCurrentWatcher
+  const h: typeof import('vue').h
+  const inject: typeof import('vue').inject
+  const isProxy: typeof import('vue').isProxy
+  const isReactive: typeof import('vue').isReactive
+  const isReadonly: typeof import('vue').isReadonly
+  const isRef: typeof import('vue').isRef
+  const isShallow: typeof import('vue').isShallow
+  const markRaw: typeof import('vue').markRaw
+  const nextTick: typeof import('vue').nextTick
+  const onActivated: typeof import('vue').onActivated
+  const onBeforeMount: typeof import('vue').onBeforeMount
+  const onBeforeUnmount: typeof import('vue').onBeforeUnmount
+  const onBeforeUpdate: typeof import('vue').onBeforeUpdate
+  const onDeactivated: typeof import('vue').onDeactivated
+  const onErrorCaptured: typeof import('vue').onErrorCaptured
+  const onMounted: typeof import('vue').onMounted
+  const onRenderTracked: typeof import('vue').onRenderTracked
+  const onRenderTriggered: typeof import('vue').onRenderTriggered
+  const onScopeDispose: typeof import('vue').onScopeDispose
+  const onServerPrefetch: typeof import('vue').onServerPrefetch
+  const onUnmounted: typeof import('vue').onUnmounted
+  const onUpdated: typeof import('vue').onUpdated
+  const onWatcherCleanup: typeof import('vue').onWatcherCleanup
+  const provide: typeof import('vue').provide
+  const reactive: typeof import('vue').reactive
+  const readonly: typeof import('vue').readonly
+  const ref: typeof import('vue').ref
+  const resolveComponent: typeof import('vue').resolveComponent
+  const shallowReactive: typeof import('vue').shallowReactive
+  const shallowReadonly: typeof import('vue').shallowReadonly
+  const shallowRef: typeof import('vue').shallowRef
+  const toRaw: typeof import('vue').toRaw
+  const toRef: typeof import('vue').toRef
+  const toRefs: typeof import('vue').toRefs
+  const toValue: typeof import('vue').toValue
+  const triggerRef: typeof import('vue').triggerRef
+  const unref: typeof import('vue').unref
+  const useAttrs: typeof import('vue').useAttrs
+  const useCssModule: typeof import('vue').useCssModule
+  const useCssVars: typeof import('vue').useCssVars
+  const useId: typeof import('vue').useId
+  const useModel: typeof import('vue').useModel
+  const useSlots: typeof import('vue').useSlots
+  const useTemplateRef: typeof import('vue').useTemplateRef
+  const watch: typeof import('vue').watch
+  const watchEffect: typeof import('vue').watchEffect
+  const watchPostEffect: typeof import('vue').watchPostEffect
+  const watchSyncEffect: typeof import('vue').watchSyncEffect
+}
+// for type re-export
+declare global {
+  // @ts-ignore
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  import('vue')
+}

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

@@ -0,0 +1,52 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore lint: disable
+// oxlint-disable
+// ------
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    CustomDropdown: typeof import('./src/components/CustomDropdown/index.vue')['default']
+    ElAside: typeof import('element-plus/es')['ElAside']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
+    ElCol: typeof import('element-plus/es')['ElCol']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElEmpty: typeof import('element-plus/es')['ElEmpty']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElMain: typeof import('element-plus/es')['ElMain']
+    ElMenu: typeof import('element-plus/es')['ElMenu']
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElRow: typeof import('element-plus/es')['ElRow']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    SearchDialog: typeof import('./src/components/SearchDialog/index.vue')['default']
+    Sidebar: typeof import('./src/components/Sidebar/index.vue')['default']
+    SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
+  }
+}

+ 5 - 0
apps/web/package.json

@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "@repo/nodes": "workspace:^",
+    "@element-plus/icons-vue": "^2.3.2",
     "element-plus": "^2.13.1",
     "normalize.css": "^8.0.1",
     "vue": "^3.5.24",
@@ -23,7 +24,11 @@
     "less": "^4.5.1",
     "typescript": "~5.9.3",
     "unocss": "^66.6.0",
+    "unplugin-auto-import": "^21.0.0",
+    "unplugin-icons": "^23.0.1",
+    "unplugin-vue-components": "^31.0.0",
     "vite": "npm:rolldown-vite@7.2.5",
+    "vite-plugin-svg-icons": "^2.0.1",
     "vue-tsc": "^3.1.4"
   },
   "pnpm": {

+ 1 - 0
apps/web/src/assets/icons/Fold.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="16px" height="16px" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="panel-left"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2"></rect><path d="M9 3v18"></path></g></svg>

+ 1 - 0
apps/web/src/assets/icons/Plus.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="16px" height="16px" aria-hidden="true" focusable="false" role="img" data-icon="plus"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7-7v14"></path></svg>

+ 1 - 0
apps/web/src/assets/icons/Search.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="16px" height="16px" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="search"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m21 21l-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle></g></svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
apps/web/src/assets/icons/box.svg


+ 1 - 0
apps/web/src/assets/icons/chatMessage.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="message-circle"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"></path></svg>

+ 1 - 0
apps/web/src/assets/icons/help.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="circle-help"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3m.08 4h.01"></path></g></svg>

+ 1 - 0
apps/web/src/assets/icons/home.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="house"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"></path><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></g></svg>

+ 1 - 0
apps/web/src/assets/icons/line.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="chart-column-decreasing"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17V9m5 8v-3M3 3v16a2 2 0 0 0 2 2h16M8 17V5"></path></svg>

+ 1 - 0
apps/web/src/assets/icons/platForm.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="cloud"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9"></path></svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
apps/web/src/assets/icons/setting.svg


+ 1 - 0
apps/web/src/assets/icons/user.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" width="1em" height="1em" class="n8n-icon" aria-hidden="true" focusable="false" role="img" data-icon="user"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></g></svg>

+ 0 - 1
apps/web/src/assets/vue.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 326 - 0
apps/web/src/components/SearchDialog/index.vue

@@ -0,0 +1,326 @@
+<template>
+	<div v-if="isOpen" class="search-dialog-overlay" @click.self="closeDialog">
+		<div class="search-dialog-container">
+			<!-- 搜索框 -->
+			<div class="search-header">
+				<el-input
+					v-model="searchQuery"
+					placeholder="输入全部搜索........."
+					clearable
+					@keydown.escape="closeDialog"
+					autofocus
+				>
+					<template #prefix>
+						<el-icon><Search /></el-icon>
+					</template>
+				</el-input>
+			</div>
+
+			<!-- 项目 -->
+			<div class="search-section" v-if="!searchQuery">
+				<div class="section-title">项目</div>
+				<div class="search-items">
+					<div
+						class="search-item"
+						v-for="item in projectItems"
+						:key="item.id"
+						@click="selectItem(item)"
+					>
+						<span class="item-icon">{{ item.icon }}</span>
+						<span class="item-text">{{ item.text }}</span>
+					</div>
+				</div>
+			</div>
+
+			<!-- 工作流程 -->
+			<div class="search-section" v-if="!searchQuery">
+				<div class="section-title">工作流程</div>
+				<div class="search-items">
+					<div class="search-item action-item" @click="handleCreateWorkflow">
+						<span class="item-icon">+</span>
+						<span class="item-text">在个人环境中创建工作流程</span>
+					</div>
+				</div>
+			</div>
+
+			<!-- 搜索结果 -->
+			<div class="search-section" v-if="searchQuery">
+				<div class="section-title">搜索结果</div>
+				<div class="search-items">
+					<div
+						class="search-item"
+						v-for="item in filteredResults"
+						:key="item.id"
+						@click="selectItem(item)"
+					>
+						<span class="item-icon">{{ item.icon || '→' }}</span>
+						<span class="item-text">{{ item.text }}</span>
+					</div>
+					<div v-if="filteredResults.length === 0" class="no-results">没有找到相关结果</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Search } from '@element-plus/icons-vue'
+
+interface SearchItem {
+	id: string
+	text: string
+	icon?: string
+	action?: () => void
+}
+
+const router = useRouter()
+
+const props = defineProps({
+	isOpen: {
+		type: Boolean,
+		required: true
+	}
+})
+
+const emit = defineEmits(['close', 'select'])
+
+const searchQuery = ref('')
+
+const recentItems: SearchItem[] = [
+	{
+		id: '1',
+		text: '利用 Gemini AI, OCR 和 Google Sheets 编成,打开并解析发票的文档。'
+	},
+	{
+		id: '2',
+		text: '打开我的工作流程'
+	},
+	{
+		id: '3',
+		text: '并启与新闻的期间内'
+	}
+]
+
+const projectItems: SearchItem[] = [
+	{
+		id: '4',
+		text: '创建项目',
+		icon: '📁'
+	},
+	{
+		id: '5',
+		text: '开放项目',
+		icon: '📁'
+	}
+]
+
+const allItems = [...recentItems, ...projectItems]
+
+const filteredResults = computed(() => {
+	if (!searchQuery.value.trim()) {
+		return []
+	}
+	const query = searchQuery.value.toLowerCase()
+	return allItems.filter((item) => item.text.toLowerCase().includes(query))
+})
+
+const closeDialog = () => {
+	searchQuery.value = ''
+	emit('close')
+}
+
+const selectItem = (item: SearchItem) => {
+	emit('select', item)
+	closeDialog()
+}
+
+const handleCreateWorkflow = () => {
+	router.push('/workflow/0')
+	closeDialog()
+}
+
+const handleKeyDown = (e: KeyboardEvent) => {
+	if (e.key === 'Escape' && props.isOpen) {
+		closeDialog()
+	}
+}
+
+onMounted(() => {
+	window.addEventListener('keydown', handleKeyDown)
+})
+
+onUnmounted(() => {
+	window.removeEventListener('keydown', handleKeyDown)
+})
+</script>
+
+<style lang="less" scoped>
+.search-dialog-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.45);
+	display: flex;
+	justify-content: center;
+	align-items: flex-start;
+	padding-top: 60px;
+	z-index: 1000;
+	animation: fadeIn 0.2s ease-out;
+
+	@keyframes fadeIn {
+		from {
+			background: rgba(0, 0, 0, 0);
+		}
+		to {
+			background: rgba(0, 0, 0, 0.45);
+		}
+	}
+}
+
+.search-dialog-container {
+	width: 600px;
+	max-height: 70vh;
+	background: #fff;
+	border-radius: 12px;
+	box-shadow:
+		0 3px 12px rgba(0, 0, 0, 0.15),
+		0 6px 24px rgba(0, 0, 0, 0.1);
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+	animation: slideDown 0.3s ease-out;
+
+	@keyframes slideDown {
+		from {
+			opacity: 0;
+			transform: translateY(-20px);
+		}
+		to {
+			opacity: 1;
+			transform: translateY(0);
+		}
+	}
+}
+
+.search-header {
+	padding: 16px 20px;
+	border-bottom: 1px solid #f0f0f0;
+	display: flex;
+	gap: 12px;
+	align-items: center;
+	background: #fafafa;
+
+	:deep(.el-input) {
+		input {
+			border-radius: 8px;
+			font-size: 14px;
+
+			&::placeholder {
+				color: #d0d0d0;
+			}
+		}
+	}
+}
+
+.search-content {
+	flex: 1;
+	overflow-y: auto;
+	padding: 12px 0;
+
+	&::-webkit-scrollbar {
+		width: 6px;
+	}
+
+	&::-webkit-scrollbar-track {
+		background: transparent;
+	}
+
+	&::-webkit-scrollbar-thumb {
+		background: #e0e0e0;
+		border-radius: 3px;
+
+		&:hover {
+			background: #ccc;
+		}
+	}
+}
+
+.search-section {
+	padding: 8px 0;
+}
+
+.section-title {
+	font-size: 12px;
+	color: #999;
+	padding: 8px 20px;
+	font-weight: 500;
+	text-transform: uppercase;
+	letter-spacing: 0.5px;
+}
+
+.search-items {
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+	padding: 0 8px;
+}
+
+.search-item {
+	padding: 10px 16px;
+	margin: 0 8px;
+	background: transparent;
+	border-radius: 8px;
+	cursor: pointer;
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	transition: all 0.2s ease;
+	color: #333;
+	font-size: 13px;
+
+	&:hover {
+		background: #f5f5f5;
+		color: #ff6b6b;
+
+		.item-icon {
+			color: #ff6b6b;
+		}
+	}
+
+	&.action-item {
+		color: #999;
+
+		&:hover {
+			color: #ff6b6b;
+			background: #fff5f5;
+		}
+	}
+}
+
+.item-icon {
+	font-size: 14px;
+	width: 20px;
+	text-align: center;
+	color: #ccc;
+	transition: color 0.2s ease;
+	flex-shrink: 0;
+}
+
+.item-text {
+	flex: 1;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.no-results {
+	padding: 40px 20px;
+	text-align: center;
+	color: #999;
+	font-size: 14px;
+}
+</style>

+ 0 - 48
apps/web/src/components/Sidebar.vue

@@ -1,48 +0,0 @@
-<template>
-	<div style="height: 100%; display: flex; flex-direction: column">
-		<div
-			style="
-				padding: 18px 16px;
-				display: flex;
-				align-items: center;
-				gap: 8px;
-				border-bottom: 1px solid #f2f2f2;
-			"
-		>
-			<div style="font-weight: 800; font-size: 18px; color: #ff6b6b">n8n</div>
-			<div style="color: #999; font-size: 12px">管理平台</div>
-		</div>
-
-		<el-menu
-			default-active="/"
-			router
-			class="el-menu-vertical-demo"
-			style="flex: 1; padding-top: 8px"
-		>
-			<el-menu-item index="/">
-				<i class="el-icon-data-analysis"></i>
-				<span>概览</span>
-			</el-menu-item>
-			<el-menu-item index="/templates">
-				<i class="el-icon-s-operation"></i>
-				<span>模板</span>
-			</el-menu-item>
-			<el-menu-item index="/executions">
-				<i class="el-icon-refresh-right"></i>
-				<span>执行</span>
-			</el-menu-item>
-		</el-menu>
-
-		<div style="padding: 12px; border-top: 1px solid #eee">
-			<el-button type="text" size="small">个人</el-button>
-		</div>
-	</div>
-</template>
-
-<script setup lang="ts"></script>
-
-<style lang="less" scoped>
-.el-menu-vertical-demo {
-	border-right: none;
-}
-</style>

+ 328 - 0
apps/web/src/components/Sidebar/index.vue

@@ -0,0 +1,328 @@
+<template>
+	<div :class="['sidebar', { collapsed }]">
+		<div class="top-bar">
+			<div class="brand" v-if="!collapsed">
+				<span class="brand-logo">AI Agent</span>
+			</div>
+			<div
+				class="top-icons"
+				:style="{
+					flexDirection: collapsed ? 'column' : 'row',
+					width: collapsed ? '100%' : 'auto'
+				}"
+			>
+				<el-dropdown placement="bottom-start" trigger="click">
+					<SvgIcon name="Plus" style="cursor: pointer" />
+					<template #dropdown>
+						<el-dropdown-menu>
+							<el-dropdown-item @click="createWorkflow">工作流程</el-dropdown-item>
+							<el-dropdown-item @click="createCertificate">凭证</el-dropdown-item>
+							<el-dropdown-item @click="createTable">数据表</el-dropdown-item>
+						</el-dropdown-menu>
+					</template>
+				</el-dropdown>
+				<el-tooltip placement="bottom">
+					<template #content>
+						<div class="tooltip-keys">
+							<kbd>Ctrl</kbd>
+							<span class="plus">+</span>
+							<kbd>K</kbd>
+							<span class="desc">快速搜索</span>
+						</div>
+					</template>
+					<SvgIcon name="Search" @click="showSearchDialog = true" style="cursor: pointer" />
+				</el-tooltip>
+				<el-tooltip placement="bottom">
+					<template #content>
+						<div class="tooltip-keys">
+							<kbd>[</kbd>
+							<span class="desc">折叠侧边栏</span>
+						</div>
+					</template>
+					<SvgIcon name="Fold" @click="toggle" />
+				</el-tooltip>
+			</div>
+		</div>
+
+		<el-menu default-active="/" router class="el-menu-vertical-demo main-menu">
+			<el-menu-item index="/">
+				<el-tooltip v-if="collapsed" content="概览" placement="right">
+					<SvgIcon name="home" />
+				</el-tooltip>
+				<SvgIcon v-else name="home" />
+				<span v-if="!collapsed" class="label">概览</span>
+			</el-menu-item>
+
+			<el-menu-item index="/personal">
+				<el-tooltip v-if="collapsed" content="个人" placement="right">
+					<SvgIcon name="user" />
+				</el-tooltip>
+				<SvgIcon v-else name="user" />
+
+				<span v-if="!collapsed" class="label">个人</span>
+			</el-menu-item>
+
+			<el-menu-item index="/chat">
+				<el-tooltip v-if="collapsed" content="聊天" placement="right">
+					<SvgIcon name="chatMessage" />
+				</el-tooltip>
+				<SvgIcon v-else name="chatMessage" />
+				<span v-if="!collapsed" class="label">聊天 <small class="beta">beta</small></span>
+			</el-menu-item>
+		</el-menu>
+
+		<div class="spacer"></div>
+
+		<div class="bottom-menu">
+			<div class="bottom-item">
+				<el-tooltip v-if="collapsed" content="管理面板" placement="right">
+					<SvgIcon name="platForm" />
+				</el-tooltip>
+				<SvgIcon v-else name="platForm" />
+				<span v-if="!collapsed" class="label">管理面板</span>
+			</div>
+
+			<div class="bottom-item">
+				<el-tooltip v-if="collapsed" content="模板" placement="right">
+					<SvgIcon name="box" />
+				</el-tooltip>
+				<SvgIcon v-else name="box" />
+				<span v-if="!collapsed" class="label">模板</span>
+			</div>
+
+			<div class="bottom-item">
+				<el-tooltip v-if="collapsed" content="统计" placement="right">
+					<SvgIcon name="line" />
+				</el-tooltip>
+				<SvgIcon v-else name="line" />
+				<span v-if="!collapsed" class="label">统计</span>
+			</div>
+
+			<div class="bottom-item">
+				<el-tooltip v-if="collapsed" content="帮助" placement="right">
+					<SvgIcon name="help" />
+				</el-tooltip>
+				<SvgIcon v-else name="help" />
+				<span v-if="!collapsed" class="label">帮助</span>
+			</div>
+
+			<div class="bottom-item">
+				<el-tooltip v-if="collapsed" content="设置" placement="right">
+					<SvgIcon name="setting" />
+				</el-tooltip>
+				<SvgIcon v-else name="setting" />
+				<span v-if="!collapsed" class="label">设置</span>
+			</div>
+		</div>
+	</div>
+
+	<!-- 搜索对话框 -->
+	<SearchDialog
+		:is-open="showSearchDialog"
+		@close="showSearchDialog = false"
+		@select="handleSearchSelect"
+	/>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import SearchDialog from '../SearchDialog/index.vue'
+
+const router = useRouter()
+const collapsed = ref(false)
+const showSearchDialog = ref(false)
+
+const toggle = () => {
+	collapsed.value = !collapsed.value
+}
+
+const createWorkflow = () => {
+	router.push('/workflow/0')
+}
+
+const createCertificate = () => {}
+
+const createVariable = () => {}
+
+const createTable = () => {}
+
+const handleKeyDown = (e: KeyboardEvent) => {
+	if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+		e.preventDefault()
+		showSearchDialog.value = true
+	}
+	if (e.key === '[') {
+		e.preventDefault()
+		toggle()
+	}
+}
+
+onMounted(() => {
+	window.addEventListener('keydown', handleKeyDown)
+})
+
+onUnmounted(() => {
+	window.removeEventListener('keydown', handleKeyDown)
+})
+
+const dropdownItems = ref([
+	{ id: '1', label: '工作流程', action: createWorkflow },
+	{ id: '2', label: '凭证', action: createCertificate },
+	{ id: '3', label: '变量', action: createVariable },
+	{ id: '4', label: '数据表', action: createTable }
+])
+
+const handleSearchSelect = (item: any) => {
+	console.log('Selected:', item)
+}
+
+const handleDropdownSelect = (item: any) => {
+	console.log('Dropdown selected:', item)
+}
+</script>
+
+<style lang="less" scoped>
+* {
+	color: hsl(0, 0%, 46%);
+}
+.sidebar {
+	width: 200px;
+	height: 100%;
+	background: #fff;
+	border-right: 1px solid #f0f0f0;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	transition: width 0.18s ease;
+	overflow: hidden;
+}
+.sidebar.collapsed {
+	width: 41px;
+}
+.top-bar {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 8px 10px;
+	border-bottom: 1px solid #f2f2f2;
+}
+.brand-logo {
+	font-weight: 700;
+	color: #ff6b6b;
+	font-size: 14px;
+}
+.top-icons {
+	display: flex;
+	gap: 10px;
+	align-items: center;
+}
+.top-icons svg {
+	cursor: pointer;
+	padding: 6px 8px;
+	margin: -6px -8px;
+	border-radius: 4px;
+	transition: all 0.2s ease;
+}
+.top-icons svg:hover {
+	color: #ff6b6b;
+	background-color: #f0f0f0;
+}
+
+.main-menu {
+	padding-top: 8px;
+	border-right: none;
+}
+.el-menu-vertical-demo .el-menu-item {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	padding: 0 16px;
+	height: 32px;
+}
+.el-menu-vertical-demo .el-menu-item:hover {
+	background: #fafafa;
+}
+.el-menu-vertical-demo .el-menu-item.is-active {
+	background: #f0f0f0 !important;
+}
+.label {
+	font-size: 13px;
+}
+.beta {
+	font-size: 10px;
+	color: #999;
+	margin-left: 6px;
+}
+.spacer {
+	flex: 1 1 auto;
+}
+.bottom-menu {
+	padding: 8px 6px;
+	border-top: 1px solid #f2f2f2;
+	display: flex;
+	flex-direction: column;
+	gap: 6px;
+}
+.bottom-item {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	padding: 8px 12px;
+	color: #333;
+}
+.bottom-item:hover {
+	background: #fafafa;
+}
+.sidebar.collapsed .label {
+	display: none;
+}
+.sidebar.collapsed .el-menu-vertical-demo .el-menu-item,
+.sidebar.collapsed .bottom-item {
+	justify-content: center;
+	padding-left: 0;
+	padding-right: 0;
+}
+.sidebar.collapsed .top-icons {
+	gap: 10px;
+}
+:deep(.el-button + .el-button) {
+	margin-left: 0;
+}
+
+.tooltip-keys {
+	display: flex;
+	align-items: center;
+	gap: 4px;
+	font-size: 11px;
+
+	kbd {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		padding: 1px 4px;
+		min-width: 18px;
+		height: 18px;
+		background: #f0f0f0;
+		color: #333;
+		border: 1px solid #d9d9d9;
+		border-radius: 2px;
+		font-family: 'Monaco', 'Courier New', monospace;
+		font-weight: 500;
+		font-size: 10px;
+		box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+	}
+
+	.plus {
+		color: #999;
+		margin: 0 1px;
+		font-size: 10px;
+	}
+
+	.desc {
+		color: #ccc;
+		margin-left: 2px;
+		font-size: 11px;
+	}
+}
+</style>

+ 56 - 0
apps/web/src/components/SvgIcon/index.vue

@@ -0,0 +1,56 @@
+<template>
+	<svg v-on="$attrs" class="svg-icon" :class="svgClass" :style="getStyle" aria-hidden="true">
+		<use :xlink:href="symbolId" :fill="props.color" />
+	</svg>
+</template>
+
+<script lang="ts" setup>
+import { type CSSProperties, computed } from 'vue'
+
+const props = defineProps({
+	prefix: {
+		type: String,
+		default: 'svg-icon'
+	},
+	name: {
+		type: String,
+		required: true
+	},
+	color: {
+		type: String,
+		default: ''
+	},
+	size: {
+		type: [Number, String],
+		default: 16
+	},
+	className: {
+		type: String,
+		default: ''
+	}
+})
+const symbolId = computed(() => `#${props.prefix}-${props.name}`)
+const svgClass = computed(() => {
+	if (props.className) {
+		return `svg-icon ${props.className}`
+	} else {
+		return 'svg-icon'
+	}
+})
+const getStyle = computed((): CSSProperties => {
+	const { size } = props
+	const s = `${size}`.replace('px', '').concat('px')
+	return {
+		width: s,
+		height: s
+	}
+})
+</script>
+
+<style>
+.svg-icon {
+	overflow: hidden;
+	vertical-align: -0.15em;
+	fill: currentColor;
+}
+</style>

+ 27 - 4
apps/web/src/layouts/MainLayout.vue

@@ -1,11 +1,12 @@
 <template>
 	<el-container style="height: 100vh">
-		<el-aside width="220px">
+		<el-aside width="200px">
 			<Sidebar />
 		</el-aside>
 
 		<el-container>
 			<el-header
+				v-if="!isWorkflowPage"
 				style="
 					height: 64px;
 					display: flex;
@@ -15,12 +16,21 @@
 				"
 			>
 				<div style="display: flex; align-items: center; gap: 12px">
-					<div style="font-weight: 700; font-size: 18px">n8n</div>
+					<div style="font-weight: 700; font-size: 18px">AI Agent</div>
 					<div style="color: #888; font-size: 13px">概述</div>
 				</div>
 
 				<div style="display: flex; align-items: center; gap: 12px">
-					<el-button type="primary" size="small">创建工作流程</el-button>
+					<el-dropdown style="background: #ff6b6b" split-button type="primary" @click="handleClick">
+						创建工作流程
+						<template #dropdown>
+							<el-dropdown-menu>
+								<el-dropdown-item>创建凭证</el-dropdown-item>
+								<el-dropdown-item>创建变量</el-dropdown-item>
+								<el-dropdown-item>创建数据表</el-dropdown-item>
+							</el-dropdown-menu>
+						</template>
+					</el-dropdown>
 				</div>
 			</el-header>
 
@@ -31,7 +41,20 @@
 	</el-container>
 </template>
 <script setup lang="ts">
-import Sidebar from '@/components/Sidebar.vue'
+import { useRouter, useRoute } from 'vue-router'
+import { computed } from 'vue'
+import Sidebar from '@/components/Sidebar/index.vue'
+
+const router = useRouter()
+const route = useRoute()
+
+const isWorkflowPage = computed(() => {
+	return route.path.includes('/workflow/')
+})
+
+const handleClick = () => {
+	router.push('/workflow/0')
+}
 </script>
 
 <style lang="less" scoped></style>

+ 63 - 1
apps/web/src/main.ts

@@ -4,8 +4,70 @@ import App from './App.vue'
 import router from './router'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
+import 'virtual:svg-icons-register'
 
 import 'normalize.css'
 import 'virtual:uno.css'
 
-createApp(App).use(router).use(ElementPlus).mount('#app')
+// Set Element Plus theme colors
+const app = createApp(App)
+app.use(router)
+app.use(ElementPlus)
+
+const root = document.documentElement
+const primaryColor = '#ff6b6b'
+
+// Primary color and variants
+root.style.setProperty('--el-color-primary', primaryColor)
+root.style.setProperty('--el-color-primary-light-3', '#ff8a8a')
+root.style.setProperty('--el-color-primary-light-5', '#ffa3a3')
+root.style.setProperty('--el-color-primary-light-7', '#ffbcbc')
+root.style.setProperty('--el-color-primary-light-8', '#ffd0d0')
+root.style.setProperty('--el-color-primary-light-9', '#ffe3e3')
+root.style.setProperty('--el-color-primary-dark-2', '#e55555')
+
+// Success color (green-based)
+root.style.setProperty('--el-color-success', '#67c23a')
+root.style.setProperty('--el-color-success-light-3', '#85ce61')
+root.style.setProperty('--el-color-success-light-5', '#a6e4a1')
+root.style.setProperty('--el-color-success-light-7', '#c6f6d5')
+root.style.setProperty('--el-color-success-light-8', '#d4edda')
+root.style.setProperty('--el-color-success-light-9', '#e1f5e3')
+root.style.setProperty('--el-color-success-dark-2', '#55b82d')
+
+// Warning color (orange-based)
+root.style.setProperty('--el-color-warning', '#e6a23c')
+root.style.setProperty('--el-color-warning-light-3', '#edb563')
+root.style.setProperty('--el-color-warning-light-5', '#f3d19e')
+root.style.setProperty('--el-color-warning-light-7', '#f9e4ba')
+root.style.setProperty('--el-color-warning-light-8', '#fce9cc')
+root.style.setProperty('--el-color-warning-light-9', '#fef0d9')
+root.style.setProperty('--el-color-warning-dark-2', '#d68830')
+
+// Danger/Error color (red-based, complement primary)
+root.style.setProperty('--el-color-danger', '#f56c6c')
+root.style.setProperty('--el-color-danger-light-3', '#f78989')
+root.style.setProperty('--el-color-danger-light-5', '#f9a8a8')
+root.style.setProperty('--el-color-danger-light-7', '#fcc7c7')
+root.style.setProperty('--el-color-danger-light-8', '#fdd9d9')
+root.style.setProperty('--el-color-danger-light-9', '#feebeb')
+root.style.setProperty('--el-color-danger-dark-2', '#dd5960')
+
+// Error (same as danger)
+root.style.setProperty('--el-color-error', '#f56c6c')
+root.style.setProperty('--el-color-error-light-3', '#f78989')
+root.style.setProperty('--el-color-error-light-5', '#f9a8a8')
+root.style.setProperty('--el-color-error-light-7', '#fcc7c7')
+root.style.setProperty('--el-color-error-light-8', '#fdd9d9')
+root.style.setProperty('--el-color-error-light-9', '#feebeb')
+
+// Info color (blue-based)
+root.style.setProperty('--el-color-info', '#909399')
+root.style.setProperty('--el-color-info-light-3', '#a6a9ad')
+root.style.setProperty('--el-color-info-light-5', '#b1b3b9')
+root.style.setProperty('--el-color-info-light-7', '#d3d4d6')
+root.style.setProperty('--el-color-info-light-8', '#e4e4e7')
+root.style.setProperty('--el-color-info-light-9', '#f2f2f5')
+root.style.setProperty('--el-color-info-dark-2', '#7a7d82')
+
+app.mount('#app')

+ 0 - 7
apps/web/src/router/index.ts

@@ -2,10 +2,6 @@ import { createRouter, createWebHistory } from 'vue-router'
 
 const MainLayout = () => import('@/layouts/MainLayout.vue')
 const Dashboard = () => import('@/views/Dashboard.vue')
-const About = () => import('@/views/About.vue')
-const Templates = () => import('@/views/Templates.vue')
-const Executions = () => import('@/views/Executions.vue')
-
 const Editor = () => import('@/views/Editor.vue')
 
 const routes = [
@@ -14,9 +10,6 @@ const routes = [
 		component: MainLayout,
 		children: [
 			{ path: '', name: 'Dashboard', component: Dashboard },
-			{ path: 'about', name: 'About', component: About },
-			{ path: 'templates', name: 'Templates', component: Templates },
-			{ path: 'executions', name: 'Executions', component: Executions },
 			{ path: 'workflow/:id', name: 'Editor', component: Editor }
 		]
 	}

+ 3 - 0
apps/web/src/style.css

@@ -11,6 +11,9 @@
   text-rendering: optimizeLegibility;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+
+  /* Element Plus theme color */
+  --el-color-primary: #ff6b6b;
 }
 
 a {

+ 0 - 12
apps/web/src/views/About.vue

@@ -1,12 +0,0 @@
-<template>
-	<div>
-		<el-card>
-			<h2>关于</h2>
-			<p>这是一个基于 Vue 3 与 Element Plus 的示例管理界面。</p>
-		</el-card>
-	</div>
-</template>
-
-<script setup lang="ts"></script>
-
-<style lang="less" scoped></style>

+ 586 - 39
apps/web/src/views/Dashboard.vue

@@ -5,21 +5,22 @@
 				<el-col :span="4" v-for="(card, idx) in cards" :key="idx">
 					<div
 						style="
-							background: #fff;
-							padding: 12px;
-							border-radius: 6px;
-							box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+							background: linear-gradient(135deg, #fff 0%, #fafafa 100%);
+							padding: 16px;
+							border-radius: 8px;
+							box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+							border: 1px solid #f0f0f0;
 						"
 					>
 						<div style="font-size: 13px; color: #888">{{ card.title }}</div>
-						<div style="font-size: 20px; margin-top: 6px">{{ card.value }}</div>
+						<div style="font-size: 20px; margin-top: 6px; font-weight: 600">{{ card.value }}</div>
 					</div>
 				</el-col>
 			</el-row>
 		</el-card>
 
 		<el-card style="margin-top: 16px">
-			<el-tabs v-model:active-name="activeTab">
+			<el-tabs v-model="activeTab">
 				<el-tab-pane label="工作流程" name="flows"></el-tab-pane>
 				<el-tab-pane label="证书" name="certs"></el-tab-pane>
 				<el-tab-pane label="执行" name="execs"></el-tab-pane>
@@ -27,98 +28,644 @@
 				<el-tab-pane label="数据表" name="tables"></el-tab-pane>
 			</el-tabs>
 
+			<!-- 工作流程和证书 Tab 的搜索和排序 -->
+			<div
+				v-if="activeTab === 'flows' || activeTab === 'certs'"
+				style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px"
+			>
+				<div style="display: flex; gap: 8px; align-items: center">
+					<el-input
+						placeholder="搜索"
+						:prefix-icon="Search"
+						v-model="filter"
+						clearable
+						style="width: 260px"
+					/>
+					<el-select v-model="sort" placeholder="排序" style="width: 180px">
+						<el-option label="按更新时间排序" value="updated" />
+						<el-option label="按创建时间排序" value="created" />
+						<el-option label="按名称升序" value="name" />
+						<el-option label="按名称降序" value="name-desc" />
+					</el-select>
+				</div>
+
+				<div>
+					<el-button type="text">筛选</el-button>
+				</div>
+			</div>
+
+			<!-- 执行 Tab 的搜索和筛选 -->
 			<div
+				v-else-if="activeTab === 'execs'"
 				style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px"
 			>
 				<div style="display: flex; gap: 8px; align-items: center">
-					<el-input placeholder="搜索" v-model="filter" clearable style="width: 260px" />
-					<el-select v-model="sort" placeholder="Sort" size="small" style="width: 160px">
-						<el-option label="Sort by last updated" value="updated" />
-						<el-option label="Sort by name" value="name" />
+					<el-input
+						placeholder="搜索"
+						:prefix-icon="Search"
+						v-model="filter"
+						clearable
+						style="width: 260px"
+					/>
+					<el-select v-model="sort" placeholder="排序" style="width: 180px">
+						<el-option label="按名称升序" value="name" />
+						<el-option label="按名称降序" value="name-desc" />
 					</el-select>
 				</div>
 
 				<div>
-					<el-button type="text" size="small">筛选</el-button>
+					<el-button type="text">筛选</el-button>
+				</div>
+			</div>
+
+			<!-- 变量 Tab 的搜索和添加按钮 -->
+			<div
+				v-else-if="activeTab === 'vars'"
+				style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px"
+			>
+				<div style="display: flex; gap: 8px; align-items: center">
+					<el-input
+						placeholder="搜索变量......"
+						:prefix-icon="Search"
+						v-model="varFilter"
+						clearable
+						style="width: 260px"
+					/>
+					<el-select v-model="varSort" placeholder="按名称排序" style="width: 200px">
+						<el-option label="按名称排序" value="name" />
+					</el-select>
+				</div>
+
+				<div style="display: flex; gap: 8px">
+					<el-button type="text">筛选</el-button>
 				</div>
 			</div>
 
 			<div style="margin-top: 12px">
-				<el-empty v-if="filtered.length === 0" description="无工作流" />
+				<!-- 工作流程 Tab -->
+				<template v-if="activeTab === 'flows'">
+					<el-empty v-if="getTabData.length === 0" description="无工作流程" />
+					<div v-else>
+						<div v-for="item in pagedData" :key="item.id" style="margin-bottom: 12px">
+							<el-card>
+								<div style="display: flex; justify-content: space-between; align-items: center">
+									<div>
+										<div style="font-weight: 700">{{ item.title }}</div>
+										<div style="color: #999; font-size: 12px">上次更新时间: {{ item.created }}</div>
+									</div>
 
-				<div v-else>
-					<div v-for="item in paged" :key="item.id" style="margin-bottom: 12px">
-						<el-card>
+									<div style="display: flex; gap: 8px; align-items: center">
+										<el-tag size="small" type="info">个人的</el-tag>
+										<el-dropdown>
+											<span class="el-dropdown-link">•••</span>
+											<template #dropdown>
+												<el-dropdown-menu>
+													<el-dropdown-item>打开</el-dropdown-item>
+													<el-dropdown-item>分享</el-dropdown-item>
+													<el-dropdown-item>复制</el-dropdown-item>
+													<el-dropdown-item>移动</el-dropdown-item>
+													<el-dropdown-item>档案</el-dropdown-item>
+												</el-dropdown-menu>
+											</template>
+										</el-dropdown>
+									</div>
+								</div>
+							</el-card>
+						</div>
+					</div>
+				</template>
+
+				<!-- 执行 Tab - 表格形式 -->
+				<template v-else-if="activeTab === 'execs'">
+					<div
+						v-if="getTabData.length === 0"
+						style="text-align: center; padding: 40px; color: #999"
+					>
+						没有正在进行的执行。
+					</div>
+					<el-table v-else :data="getTabData" style="width: 100%; margin-top: 12px" stripe border>
+						<el-table-column type="selection" width="50" />
+						<el-table-column prop="workflow" label="工作流程">
+							<template #default="scope">
+								<el-button type="text" @click="$router.push(`/workflow/${scope.row.id}`)">{{
+									scope.row.workflow
+								}}</el-button>
+							</template>
+						</el-table-column>
+						<el-table-column prop="status" label="地位" width="100">
+							<template #default="scope">
+								<div style="display: flex; align-items: center; gap: 6px">
+									<el-icon style="color: #67c23a" v-if="scope.row.status === '成功'">
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 24 24"
+											fill="currentColor"
+											width="1em"
+											height="1em"
+										>
+											<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
+										</svg>
+									</el-icon>
+									{{ scope.row.status }}
+								</div>
+							</template>
+						</el-table-column>
+						<el-table-column prop="startTime" label="开始" />
+						<el-table-column prop="duration" label="运行时间" width="100" />
+						<el-table-column prop="executionId" label="执行 ID" width="80" />
+						<el-table-column label="手动执行" width="100" align="center">
+							<template #default>
+								<el-button style="background: #ccc; color: #666; border: none" size="small">手动执行</el-button>
+							</template>
+						</el-table-column>
+						<el-table-column label="操作" width="80" align="center">
+							<template #default>
+								<el-dropdown>
+									<span class="el-dropdown-link">⋯</span>
+									<template #dropdown>
+										<el-dropdown-menu>
+											<el-dropdown-item>删除</el-dropdown-item>
+										</el-dropdown-menu>
+									</template>
+								</el-dropdown>
+							</template>
+						</el-table-column>
+					</el-table>
+				</template>
+
+				<!-- 变量 Tab - 表格或空状态 -->
+				<template v-else-if="activeTab === 'vars'">
+					<div
+						v-if="filteredVariables.length === 0"
+						style="
+							text-align: center;
+							padding: 60px 20px;
+							border: 2px dashed #ddd;
+							border-radius: 6px;
+							background: #fafafa;
+							color: #666;
+						"
+					>
+						<div style="font-size: 48px; margin-bottom: 12px">👋</div>
+						<div style="font-size: 16px; margin-bottom: 8px">迄今,我还没设置一个变量</div>
+						<div style="font-size: 13px; color: #999; margin-bottom: 16px">
+							变量可用于存储可在多个工作流程中轻松引用的数据。
+						</div>
+						<el-button @click="showVarDialog = true">添加第一个变量</el-button>
+					</div>
+					<el-table v-else :data="filteredVariables" style="width: 100%; margin-top: 12px">
+						<el-table-column prop="key" label="钥匙" />
+						<el-table-column prop="value" label="价值" />
+						<el-table-column prop="usage" label="使用语法" width="150">
+							<template #default="scope">
+								<el-tag type="info">{{ scope.row.usage }}</el-tag>
+							</template>
+						</el-table-column>
+						<el-table-column prop="scope" label="范围" width="100">
+							<template #default="scope">
+								<el-tag size="small">{{ scope.row.scope }}</el-tag>
+							</template>
+						</el-table-column>
+					<el-table-column label="操作" width="120" align="right">
+							<template #default="scope">
+								<el-button text size="small" @click="editVariable(scope.row)">编辑</el-button>
+								<el-button text size="small" type="danger" @click="deleteVariable(scope.row.id)"
+									>删除</el-button
+								>
+							</template>
+						</el-table-column>
+					</el-table>
+					<div v-if="filteredVariables.length > 0" style="margin-top: 12px; text-align: right">
+						<span style="color: #999; font-size: 12px">页面大小: </span>
+						<el-select v-model="varPageSize" size="small" style="width: 60px">
+							<el-option label="25" value="25" />
+							<el-option label="50" value="50" />
+							<el-option label="100" value="100" />
+						</el-select>
+					</div>
+				</template>
+
+				<!-- 其他 Tab -->
+				<template v-else-if="activeTab === 'tables'">
+					<div
+						v-if="tables.length === 0"
+						style="
+							text-align: center;
+							padding: 60px 20px;
+							border: 2px dashed #ddd;
+							border-radius: 6px;
+							background: #fafafa;
+						"
+					>
+						<div style="font-size: 16px; margin-bottom: 8px; color: #333">
+							您目前还没有任何数据表。
+						</div>
+						<div style="font-size: 13px; color: #999; margin-bottom: 20px">
+							使用数据表来保存执行结果、在工作流之间共享数据以及跨进程体估指标。
+						</div>
+						<el-button @click="showTableDialog = true">创建数据表</el-button>
+					</div>
+					<div v-else>
+						<div
+							v-for="item in tables"
+							:key="item.id"
+							style="
+								margin-bottom: 12px;
+								padding: 12px;
+								border: 1px solid #f0f0f0;
+								border-radius: 6px;
+								background: #fff;
+							"
+						>
 							<div style="display: flex; justify-content: space-between; align-items: center">
-								<div>
-									<div style="font-weight: 700">{{ item.title }}</div>
-									<div style="color: #999; font-size: 12px">上次更新时间: {{ item.created }}</div>
+								<div style="display: flex; gap: 12px; align-items: center; flex: 1">
+									<div style="font-size: 20px">📋</div>
+									<div style="flex: 1">
+										<div style="font-weight: 600; margin-bottom: 4px; font-size: 14px">
+											{{ item.name }}
+										</div>
+										<div style="color: #999; font-size: 12px; display: flex; gap: 12px">
+											<span>{{ item.size || '0MB' }}</span>
+											<span>{{ item.rows || '0' }}行</span>
+											<span style="color: #ff6b6b">操作更新</span>
+											<span style="color: #ff6b6b">操作创建</span>
+										</div>
+									</div>
 								</div>
 
-								<div style="display: flex; gap: 8px; align-items: center">
-									<el-tag size="small">个人的</el-tag>
-									<el-button type="primary" size="small">运行</el-button>
+								<div>
 									<el-dropdown>
 										<span class="el-dropdown-link">•••</span>
 										<template #dropdown>
 											<el-dropdown-menu>
-												<el-dropdown-item>编辑</el-dropdown-item>
+											<el-dropdown-item>下载 CSV 文件</el-dropdown-item>
 												<el-dropdown-item>删除</el-dropdown-item>
 											</el-dropdown-menu>
 										</template>
 									</el-dropdown>
 								</div>
 							</div>
-						</el-card>
+						</div>
+
+						<!-- 数据表分页器 -->
+						<div
+							style="
+								display: flex;
+								justify-content: flex-end;
+								align-items: center;
+								margin-top: 16px;
+								gap: 12px;
+							"
+						>
+							<span style="color: #666; font-size: 13px">总计 {{ tables.length }}</span>
+							<el-input v-model="tablePageInput" style="width: 50px" placeholder="1" />
+							<el-select v-model="tablePageSize" size="small" style="width: 100px">
+								<el-option label="50/页" value="50" />
+								<el-option label="100/页" value="100" />
+								<el-option label="200/页" value="200" />
+							</el-select>
+						</div>
 					</div>
-				</div>
+				</template>
+
+				<!-- 其他 Tab -->
+				<template v-else>
+					<el-empty v-if="getTabData.length === 0" :description="`无${getTabLabel}`" />
+					<div v-else>
+						<div v-for="item in getTabData" :key="item.id" style="margin-bottom: 12px">
+							<el-card>
+								<div style="display: flex; justify-content: space-between; align-items: center">
+									<div style="display: flex; gap: 12px; align-items: flex-start; flex: 1">
+										<div style="font-size: 24px">{{ item.icon || '📋' }}</div>
+										<div style="flex: 1">
+											<div style="font-weight: 700; margin-bottom: 4px">{{ item.title }}</div>
+											<div style="color: #999; font-size: 12px">{{ item.description }}</div>
+										</div>
+									</div>
+
+									<div style="display: flex; gap: 8px; align-items: center">
+										<el-tag size="small" type="info">个人的</el-tag>
+										<el-dropdown>
+											<span class="el-dropdown-link">•••</span>
+											<template #dropdown>
+												<el-dropdown-menu>
+													<el-dropdown-item>打开</el-dropdown-item>
+													<el-dropdown-item>删除</el-dropdown-item>
+													<el-dropdown-item>变更所有者</el-dropdown-item>
+												</el-dropdown-menu>
+											</template>
+										</el-dropdown>
+									</div>
+								</div>
+							</el-card>
+						</div>
+					</div>
+				</template>
 			</div>
 
-			<div style="display: flex; justify-content: flex-end; margin-top: 12px">
+			<!-- 分页器仅在工作流程 Tab 显示 -->
+			<div
+				v-if="activeTab === 'flows'"
+				style="
+					display: flex;
+					justify-content: flex-end;
+					align-items: center;
+					margin-top: 16px;
+					gap: 12px;
+				"
+			>
+				<span style="color: #666; font-size: 13px">总计 {{ getTabData.length }}</span>
 				<el-pagination
 					background
 					:page-size="pageSize"
-					:current-page.sync="page"
-					:total="filtered.length"
+					:current-page="currentPage"
+					@update:current-page="currentPage = $event"
+					:total="getTabData.length"
 					layout="prev, pager, next"
 				/>
+				<el-select v-model.number="pageSize" size="small" style="width: 100px">
+					<el-option label="10/页" :value="10" />
+					<el-option label="20/页" :value="20" />
+					<el-option label="50/页" :value="50" />
+				</el-select>
 			</div>
 		</el-card>
+
+		<!-- 新变量对话框 -->
+		<el-dialog v-model="showVarDialog" title="新变量" width="500px" @close="resetVarForm">
+			<el-form :model="varForm">
+				<el-form-item label="钥匙" required>
+					<el-input v-model="varForm.key" placeholder="请输入姓名" />
+				</el-form-item>
+				<el-form-item label="价值">
+					<el-input v-model="varForm.value" type="textarea" placeholder="请输入一个值" rows="4" />
+				</el-form-item>
+				<el-form-item label="范围" required>
+					<el-select v-model="varForm.scope" placeholder="选择">
+						<el-option label="全局的" value="全局的" />
+					</el-select>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<div style="text-align: right">
+					<el-button @click="showVarDialog = false">取消</el-button>
+					<el-button type="primary" @click="submitVariable">提交</el-button>
+				</div>
+			</template>
+		</el-dialog>
+
+		<!-- 创建新数据表对话框 -->
+		<el-dialog v-model="showTableDialog" title="创建新数据表" width="500px" @close="resetTableForm">
+			<el-form :model="tableForm">
+				<el-form-item label="数据表名称" required>
+					<el-input v-model="tableForm.name" placeholder="输入数据表名称" />
+				</el-form-item>
+				<el-form-item label="创建方式">
+					<el-radio-group v-model="tableForm.method">
+						<el-radio value="from-scratch">从零开始</el-radio>
+						<el-radio value="import-csv">导入 CSV 文件</el-radio>
+					</el-radio-group>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<div style="text-align: right">
+					<el-button @click="showTableDialog = false">取消</el-button>
+					<el-button type="primary" @click="submitTable">创建</el-button>
+				</div>
+			</template>
+		</el-dialog>
 	</div>
 </template>
 
 <script setup lang="ts">
 import { ref, computed } from 'vue'
+import { useRouter } from 'vue-router'
+import { Search } from '@element-plus/icons-vue'
+
+const $router = useRouter()
 
 const cards = [
 	{ title: '生产执行', value: 0 },
 	{ title: '执行失败', value: 0 },
 	{ title: '故障率', value: '0%' },
+	{ title: '节省时间', value: '0%' },
 	{ title: '运行时间(平均)', value: '0s' }
 ]
 
 const workflows = ref([
-	{ id: 1, title: '与新同事人才交流', created: '1 月 23 日' },
-	{ id: 2, title: 'RAG 同步机器人,基于 Supabase', created: '1 月 23 日' },
-	{ id: 3, title: '利用 Gemini AI, OCR 和 Google Sheets', created: '1 月 23 日' }
+	{ id: 1, title: '与新同事人才交流', created: '1 月 23 日', description: '', icon: '' },
+	{
+		id: 2,
+		title: 'RAG 同步机器人,基于 Supabase',
+		created: '1 月 23 日',
+		description: '',
+		icon: ''
+	},
+	{
+		id: 3,
+		title: '利用 Gemini AI, OCR 和 Google Sheets',
+		created: '1 月 23 日',
+		description: '',
+		icon: ''
+	}
+])
+
+const certificates = ref([
+	{
+		id: 1,
+		title: 'n8n 免费 OpenAI API 额度',
+		description: 'OpenAI 创建更新了 3 天前 | 创建时间:1 月 23 日',
+		created: '1 月 23 日',
+		icon: '🤖'
+	},
+	{
+		id: 2,
+		title: 'Google Sheets 帐户',
+		description: 'Google Sheets OAuth2 API | 上次更新时间:3 天前 | 创建时间:1 月 23 日',
+		created: '1 月 23 日',
+		icon: '📊'
+	}
+])
+
+const executions = ref([
+	{
+		id: 1,
+		workflow: '我的工作流程',
+		status: '成功',
+		startTime: '1 月 26 10:39:03',
+		duration: '35 秒',
+		executionId: '1'
+	},
+	{
+		id: 2,
+		workflow: '工作流程 2',
+		status: '成功',
+		startTime: '1 月 26 10:30:00',
+		duration: '2 分钟',
+		executionId: '2'
+	}
 ])
 
+const variables = ref([])
+
+const tables = ref([])
+
+// 数据表相关
+const showTableDialog = ref(false)
+const tablePageInput = ref('')
+const tablePageSize = ref('50')
+const tableForm = ref({
+	name: '',
+	method: 'from-scratch'
+})
+
+const resetTableForm = () => {
+	tableForm.value = {
+		name: '',
+		method: 'from-scratch'
+	}
+}
+
+const submitTable = () => {
+	if (!tableForm.value.name.trim()) {
+		ElMessage.error('请输入数据表名称')
+		return
+	}
+	const newTable = {
+		id: Math.max(...tables.value.map((t: any) => t.id), 0) + 1,
+		name: tableForm.value.name,
+		description: tableForm.value.method === 'from-scratch' ? '从零开始创建' : '从 CSV 导入',
+		method: tableForm.value.method
+	}
+	tables.value.push(newTable)
+	showTableDialog.value = false
+	resetTableForm()
+	ElMessage.success('数据表已创建')
+}
+
 const filter = ref('')
-const sort = ref('updated')
+const sort = ref('name')
 const activeTab = ref('flows')
-const page = ref(1)
-const pageSize = 10
+const pageSize = ref(10)
+
+// 变量表单相关
+const showVarDialog = ref(false)
+const varFilter = ref('')
+const varSort = ref('name')
+const varPageSize = ref('25')
+const varForm = ref({
+	key: '',
+	value: '',
+	scope: '全局的'
+})
+
+const resetVarForm = () => {
+	varForm.value = {
+		key: '',
+		value: '',
+		scope: '全局的'
+	}
+}
+
+const submitVariable = () => {
+	if (!varForm.value.key.trim()) {
+		ElMessage.error('请输入钥匙')
+		return
+	}
+	const newVar = {
+		id: Math.max(...variables.value.map((v: any) => v.id), 0) + 1,
+		key: varForm.value.key,
+		value: varForm.value.value,
+		usage: `$vars.${varForm.value.key}`,
+		scope: varForm.value.scope
+	}
+	variables.value.push(newVar)
+	showVarDialog.value = false
+	resetVarForm()
+	ElMessage.success('变量已添加')
+}
+
+const editVariable = (variable: any) => {
+	varForm.value = { ...variable, scope: variable.scope }
+	showVarDialog.value = true
+}
 
-const filtered = computed(() => {
+const deleteVariable = (id: number) => {
+	ElMessageBox.confirm('确定删除该变量吗?', '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning'
+	})
+		.then(() => {
+			variables.value = variables.value.filter((v: any) => v.id !== id)
+			ElMessage.success('变量已删除')
+		})
+		.catch(() => {})
+}
+
+const pageMap = ref({
+	flows: 1,
+	certs: 1,
+	execs: 1,
+	vars: 1,
+	tables: 1
+})
+
+const currentPage = computed({
+	get: () => pageMap.value[activeTab.value as keyof typeof pageMap.value] || 1,
+	set: (val: number) => {
+		pageMap.value[activeTab.value as keyof typeof pageMap.value] = val
+	}
+})
+
+const getTabLabel = computed(() => {
+	const labels: Record<string, string> = {
+		flows: '工作流程',
+		certs: '证书',
+		execs: '执行',
+		vars: '变量',
+		tables: '数据表'
+	}
+	return labels[activeTab.value] || '项目'
+})
+
+const getTabData = computed(() => {
+	const tabs: Record<string, any> = {
+		flows: workflows.value,
+		certs: certificates.value,
+		execs: executions.value,
+		vars: variables.value,
+		tables: tables.value
+	}
+	const data = tabs[activeTab.value] || []
 	const q = filter.value.trim().toLowerCase()
-	if (!q) return workflows.value
-	return workflows.value.filter((w) => w.title.toLowerCase().includes(q))
+	if (!q) return data
+	return data.filter((item: any) => item.title.toLowerCase().includes(q))
 })
 
-const paged = computed(() => {
-	const start = (page.value - 1) * pageSize
-	return filtered.value.slice(start, start + pageSize)
+const pagedData = computed(() => {
+	const start = (currentPage.value - 1) * pageSize.value
+	return getTabData.value.slice(start, start + pageSize.value)
 })
+
+const filteredVariables = computed(() => {
+	let result = variables.value
+	const q = varFilter.value.trim().toLowerCase()
+	if (q) {
+		result = result.filter(
+			(v: any) => v.key.toLowerCase().includes(q) || v.value.toLowerCase().includes(q)
+		)
+	}
+	// 排序
+	if (varSort.value === 'name') {
+		result.sort((a: any, b: any) => a.key.localeCompare(b.key))
+	} else if (varSort.value === 'name-desc') {
+		result.sort((a: any, b: any) => b.key.localeCompare(a.key))
+	}
+	return result
+})
+
+import { ElMessage, ElMessageBox } from 'element-plus'
 </script>
 
 <style lang="less" scoped></style>

+ 0 - 10
apps/web/src/views/Executions.vue

@@ -1,10 +0,0 @@
-<template>
-	<el-card>
-		<h2>执行</h2>
-		<p>这里是执行记录(占位)。</p>
-	</el-card>
-</template>
-
-<script setup lang="ts"></script>
-
-<style lang="less" scoped></style>

+ 0 - 10
apps/web/src/views/Templates.vue

@@ -1,10 +0,0 @@
-<template>
-	<el-card>
-		<h2>模板</h2>
-		<p>这里是模板列表(占位)。</p>
-	</el-card>
-</template>
-
-<script setup lang="ts"></script>
-
-<style lang="less" scoped></style>

+ 40 - 9
apps/web/vite.config.ts

@@ -2,16 +2,47 @@ import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import path from 'path'
 import UnoCss from 'unocss/vite'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import IconsResolver from 'unplugin-icons/resolver'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 
 // https://vite.dev/config/
 export default defineConfig({
-  plugins: [
-    vue(),
-    UnoCss()
-  ],
-  resolve: {
-    alias: {
-      '@': path.resolve(__dirname, 'src')
-    }
-  }
+	plugins: [
+		vue(),
+		UnoCss(),
+		createSvgIconsPlugin({
+			// 指定存放 SVG 的文件夹
+			iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
+			symbolId: 'svg-icon-[dir]-[name]'
+		}),
+		// 按需求加载(模板)
+		AutoImport({
+			imports: ['vue'],
+			resolvers: [
+				IconsResolver({
+					prefix: 'Icon'
+				}),
+				ElementPlusResolver()
+			],
+			dts: 'auto-imports.d.ts'
+		}),
+		Components({
+			resolvers: [
+				// 自动注册图标组件
+				IconsResolver({
+					enabledCollections: ['ep']
+				}),
+				ElementPlusResolver()
+			],
+			dts: 'components.d.ts'
+		})
+	],
+	resolve: {
+		alias: {
+			'@': path.resolve(__dirname, 'src')
+		}
+	}
 })

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1468 - 77
pnpm-lock.yaml