فهرست منبع

feat: 新增统计页面/聊天页面/并修改样式和路由高亮

Mickey Mike 3 هفته پیش
والد
کامیت
40a547d44c

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

@@ -22,6 +22,7 @@ declare module 'vue' {
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -51,6 +52,7 @@ declare module 'vue' {
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ExecutionChart: typeof import('./src/components/Chart/ExecutionChart.vue')['default']
     HttpSetter: typeof import('./src/components/setter/HttpSetter.vue')['default']
     HttpSetter: typeof import('./src/components/setter/HttpSetter.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     RouterView: typeof import('vue-router')['RouterView']

+ 3 - 1
apps/web/package.json

@@ -11,11 +11,13 @@
   "dependencies": {
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.2",
     "@element-plus/icons-vue": "^2.3.2",
     "@repo/nodes": "workspace:^",
     "@repo/nodes": "workspace:^",
+    "echarts": "^6.0.0",
     "element-plus": "^2.13.1",
     "element-plus": "^2.13.1",
     "normalize.css": "^8.0.1",
     "normalize.css": "^8.0.1",
-    "uuid": "^13.0.0",
     "pinia": "^3.0.4",
     "pinia": "^3.0.4",
+    "uuid": "^13.0.0",
     "vue": "^3.5.24",
     "vue": "^3.5.24",
+    "vue-element-plus-x": "^1.3.98",
     "vue-router": "4"
     "vue-router": "4"
   },
   },
   "devDependencies": {
   "devDependencies": {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
apps/web/src/assets/icons/sparkle.svg


+ 126 - 0
apps/web/src/components/Chart/ExecutionChart.vue

@@ -0,0 +1,126 @@
+<template>
+	<div ref="chartContainer" style="width: 100%; height: 300px"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import * as echarts from 'echarts'
+
+interface ChartData {
+	categories: string[]
+	successful: number[]
+	failed: number[]
+}
+
+interface Props {
+	data: ChartData
+	title?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+	title: '执行统计'
+})
+
+const chartContainer = ref<HTMLElement>()
+let chart: echarts.ECharts | null = null
+
+const initChart = () => {
+	if (!chartContainer.value) return
+
+	chart = echarts.init(chartContainer.value)
+
+	const option: echarts.EChartsOption = {
+		tooltip: {
+			trigger: 'axis',
+			axisPointer: {
+				type: 'shadow'
+			}
+		},
+		legend: {
+			data: ['Successful', 'Failed'],
+			top: 0
+		},
+		xAxis: {
+			type: 'category',
+			data: props.data.categories,
+			boundaryGap: true
+		},
+		yAxis: {
+			type: 'value',
+			splitLine: {
+				lineStyle: {
+					color: '#f0f0f0'
+				}
+			}
+		},
+		series: [
+			{
+				name: 'Successful',
+				data: props.data.successful,
+				type: 'bar',
+				itemStyle: {
+					color: '#13c2c2'
+				},
+				barWidth: '30%',
+				barGap: '30%'
+			},
+			{
+				name: 'Failed',
+				data: props.data.failed,
+				type: 'bar',
+				itemStyle: {
+					color: '#ff6b6b'
+				},
+				barWidth: '30%',
+				barGap: '30%'
+			}
+		],
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			top: '50px',
+			containLabel: true
+		}
+	}
+
+	chart.setOption(option)
+}
+
+const handleResize = () => {
+	chart?.resize()
+}
+
+watch(
+	() => props.data,
+	() => {
+		if (chart) {
+			const option: echarts.EChartsOption = {
+				xAxis: {
+					data: props.data.categories
+				},
+				series: [
+					{
+						data: props.data.successful
+					},
+					{
+						data: props.data.failed
+					}
+				]
+			}
+			chart.setOption(option)
+		}
+	},
+	{ deep: true }
+)
+
+onMounted(() => {
+	initChart()
+	window.addEventListener('resize', handleResize)
+})
+
+onBeforeUnmount(() => {
+	window.removeEventListener('resize', handleResize)
+	chart?.dispose()
+})
+</script>

+ 38 - 7
apps/web/src/components/Sidebar/index.vue

@@ -44,7 +44,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<el-menu default-active="/" router class="el-menu-vertical-demo main-menu">
+		<el-menu :default-active="activeMenu" router class="el-menu-vertical-demo main-menu">
 			<el-menu-item index="/">
 			<el-menu-item index="/">
 				<el-tooltip v-if="collapsed" content="概览" placement="right">
 				<el-tooltip v-if="collapsed" content="概览" placement="right">
 					<SvgIcon name="home" />
 					<SvgIcon name="home" />
@@ -65,7 +65,11 @@
 		<div class="spacer"></div>
 		<div class="spacer"></div>
 
 
 		<div class="bottom-menu">
 		<div class="bottom-menu">
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/management' }"
+				@click="$router.push('/management')"
+			>
 				<el-tooltip v-if="collapsed" content="管理面板" placement="right">
 				<el-tooltip v-if="collapsed" content="管理面板" placement="right">
 					<SvgIcon name="platForm" />
 					<SvgIcon name="platForm" />
 				</el-tooltip>
 				</el-tooltip>
@@ -73,7 +77,11 @@
 				<span v-if="!collapsed" class="label">管理面板</span>
 				<span v-if="!collapsed" class="label">管理面板</span>
 			</div>
 			</div>
 
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/templates' }"
+				@click="$router.push('/templates')"
+			>
 				<el-tooltip v-if="collapsed" content="模板" placement="right">
 				<el-tooltip v-if="collapsed" content="模板" placement="right">
 					<SvgIcon name="box" />
 					<SvgIcon name="box" />
 				</el-tooltip>
 				</el-tooltip>
@@ -81,7 +89,11 @@
 				<span v-if="!collapsed" class="label">模板</span>
 				<span v-if="!collapsed" class="label">模板</span>
 			</div>
 			</div>
 
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/statistics' }"
+				@click="$router.push('/statistics')"
+			>
 				<el-tooltip v-if="collapsed" content="统计" placement="right">
 				<el-tooltip v-if="collapsed" content="统计" placement="right">
 					<SvgIcon name="line" />
 					<SvgIcon name="line" />
 				</el-tooltip>
 				</el-tooltip>
@@ -89,7 +101,11 @@
 				<span v-if="!collapsed" class="label">统计</span>
 				<span v-if="!collapsed" class="label">统计</span>
 			</div>
 			</div>
 
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/help' }"
+				@click="$router.push('/help')"
+			>
 				<el-tooltip v-if="collapsed" content="帮助" placement="right">
 				<el-tooltip v-if="collapsed" content="帮助" placement="right">
 					<SvgIcon name="help" />
 					<SvgIcon name="help" />
 				</el-tooltip>
 				</el-tooltip>
@@ -97,7 +113,11 @@
 				<span v-if="!collapsed" class="label">帮助</span>
 				<span v-if="!collapsed" class="label">帮助</span>
 			</div>
 			</div>
 
 
-			<div class="bottom-item">
+			<div
+				class="bottom-item"
+				:class="{ active: router.currentRoute.value.path === '/settings' }"
+				@click="$router.push('/settings')"
+			>
 				<el-tooltip v-if="collapsed" content="设置" placement="right">
 				<el-tooltip v-if="collapsed" content="设置" placement="right">
 					<SvgIcon name="setting" />
 					<SvgIcon name="setting" />
 				</el-tooltip>
 				</el-tooltip>
@@ -116,7 +136,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
 import SearchDialog from '../SearchDialog/index.vue'
 import SearchDialog from '../SearchDialog/index.vue'
 
 
@@ -124,6 +144,9 @@ const router = useRouter()
 const collapsed = ref(false)
 const collapsed = ref(false)
 const showSearchDialog = ref(false)
 const showSearchDialog = ref(false)
 
 
+// 计算当前活跃的菜单项
+const activeMenu = computed(() => router.currentRoute.value.path)
+
 const toggle = () => {
 const toggle = () => {
 	collapsed.value = !collapsed.value
 	collapsed.value = !collapsed.value
 }
 }
@@ -261,9 +284,17 @@ const handleDropdownSelect = (item: any) => {
 	gap: 10px;
 	gap: 10px;
 	padding: 8px 12px;
 	padding: 8px 12px;
 	color: #333;
 	color: #333;
+	cursor: pointer;
+	border-radius: 4px;
+	transition: all 0.2s ease;
 }
 }
 .bottom-item:hover {
 .bottom-item:hover {
 	background: #fafafa;
 	background: #fafafa;
+	color: #ff6b6b;
+}
+.bottom-item.active {
+	background: #fafafa;
+	color: #ff6b6b;
 }
 }
 .sidebar.collapsed .label {
 .sidebar.collapsed .label {
 	display: none;
 	display: none;

+ 12 - 0
apps/web/src/composables/useI18n.ts

@@ -0,0 +1,12 @@
+import { inject } from 'vue'
+import i18n from '@/i18n'
+
+export function useI18n() {
+  const $i18n = inject('i18n', i18n)
+
+  return {
+    t: (key: string) => $i18n.t(key),
+    setLocale: (locale: 'zh-cn' | 'en-us') => $i18n.setLocale(locale),
+    getLocale: () => $i18n.getLocale()
+  }
+}

+ 44 - 0
apps/web/src/i18n/index.ts

@@ -0,0 +1,44 @@
+import zhCn from './locales/zh-cn'
+
+export type LocaleType = 'zh-cn' | 'en-us'
+
+const messages = {
+  'zh-cn': zhCn,
+  'en-us': {}
+}
+
+class I18n {
+  private currentLocale: LocaleType = 'zh-cn'
+
+  constructor() {
+    // 从本地存储获取语言设置
+    const savedLocale = localStorage.getItem('locale') as LocaleType
+    if (savedLocale && messages[savedLocale]) {
+      this.currentLocale = savedLocale
+    }
+  }
+
+  setLocale(locale: LocaleType) {
+    if (messages[locale]) {
+      this.currentLocale = locale
+      localStorage.setItem('locale', locale)
+    }
+  }
+
+  getLocale() {
+    return this.currentLocale
+  }
+
+  t(key: string): string {
+    const keys = key.split('.')
+    let value: any = messages[this.currentLocale]
+
+    for (const k of keys) {
+      value = value?.[k]
+    }
+
+    return typeof value === 'string' ? value : key
+  }
+}
+
+export default new I18n()

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

@@ -0,0 +1,35 @@
+export default {
+  // 公共
+  common: {
+    search: '搜索',
+    delete: '删除',
+    edit: '编辑',
+    add: '添加',
+    save: '保存',
+    cancel: '取消',
+    confirm: '确认',
+    close: '关闭',
+    back: '返回'
+  },
+  // 仪表板
+  dashboard: {
+    title: 'AI Agent',
+    subtitle: '概述',
+    workflows: '工作流程',
+    certificates: '证书',
+    executions: '执行',
+    variables: '变量',
+    dataTables: '数据表'
+  },
+  // 统计
+  statistics: {
+    title: '统计',
+    subtitle: '所有项目',
+    productionExecutions: '生产执行',
+    failedExecutions: '生产环境执行失败',
+    failureRate: '故障率',
+    timeSaved: '节省时间',
+    avgRuntime: '运行时间(平均)',
+    last7Days: '过去7天'
+  }
+}

+ 1 - 1
apps/web/src/layouts/MainLayout.vue

@@ -5,7 +5,7 @@
 		</el-aside>
 		</el-aside>
 
 
 		<el-container>
 		<el-container>
-			<el-main style="padding: 16px; overflow: auto">
+			<el-main style="padding: 0; overflow: auto">
 				<router-view />
 				<router-view />
 			</el-main>
 			</el-main>
 		</el-container>
 		</el-container>

+ 55 - 55
apps/web/src/main.ts

@@ -5,71 +5,71 @@ import store from './store'
 import router from './router'
 import router from './router'
 import ElementPlus from 'element-plus'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
 import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import i18n from './i18n'
 import 'virtual:svg-icons-register'
 import 'virtual:svg-icons-register'
 
 
 import 'normalize.css'
 import 'normalize.css'
 import 'virtual:uno.css'
 import 'virtual:uno.css'
 
 
+// Theme colors configuration
+const themeColors = {
+	primary: '#ff6b6b',
+	'primary-light-3': '#ff8a8a',
+	'primary-light-5': '#ffa3a3',
+	'primary-light-7': '#ffbcbc',
+	'primary-light-8': '#ffd0d0',
+	'primary-light-9': '#ffe3e3',
+	'primary-dark-2': '#e55555',
+	success: '#67c23a',
+	'success-light-3': '#85ce61',
+	'success-light-5': '#a6e4a1',
+	'success-light-7': '#c6f6d5',
+	'success-light-8': '#d4edda',
+	'success-light-9': '#e1f5e3',
+	'success-dark-2': '#55b82d',
+	warning: '#e6a23c',
+	'warning-light-3': '#edb563',
+	'warning-light-5': '#f3d19e',
+	'warning-light-7': '#f9e4ba',
+	'warning-light-8': '#fce9cc',
+	'warning-light-9': '#fef0d9',
+	'warning-dark-2': '#d68830',
+	danger: '#f56c6c',
+	'danger-light-3': '#f78989',
+	'danger-light-5': '#f9a8a8',
+	'danger-light-7': '#fcc7c7',
+	'danger-light-8': '#fdd9d9',
+	'danger-light-9': '#feebeb',
+	'danger-dark-2': '#dd5960',
+	error: '#f56c6c',
+	'error-light-3': '#f78989',
+	'error-light-5': '#f9a8a8',
+	'error-light-7': '#fcc7c7',
+	'error-light-8': '#fdd9d9',
+	'error-light-9': '#feebeb',
+	info: '#909399',
+	'info-light-3': '#a6a9ad',
+	'info-light-5': '#b1b3b9',
+	'info-light-7': '#d3d4d6',
+	'info-light-8': '#e4e4e7',
+	'info-light-9': '#f2f2f5',
+	'info-dark-2': '#7a7d82'
+}
+
+const root = document.documentElement
+Object.entries(themeColors).forEach(([key, value]) => {
+	root.style.setProperty(`--el-color-${key}`, value)
+})
 
 
-// Set Element Plus theme colors
 const app = createApp(App)
 const app = createApp(App)
 app.use(store)
 app.use(store)
 app.use(router)
 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')
+const currentLocale = zhCn
+app.use(ElementPlus, { locale: currentLocale })
 
 
-// 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.provide('i18n', i18n)
+app.config.globalProperties.$t = (key: string) => i18n.t(key)
 
 
 app.mount('#app')
 app.mount('#app')

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

@@ -3,6 +3,8 @@ import { createRouter, createWebHistory } from 'vue-router'
 const MainLayout = () => import('@/layouts/MainLayout.vue')
 const MainLayout = () => import('@/layouts/MainLayout.vue')
 const Dashboard = () => import('@/views/Dashboard.vue')
 const Dashboard = () => import('@/views/Dashboard.vue')
 const Editor = () => import('@/views/Editor.vue')
 const Editor = () => import('@/views/Editor.vue')
+const Statistics = () => import('@/views/Statistics.vue')
+const Chat = () => import('@/views/Chat.vue')
 
 
 const routes = [
 const routes = [
 	{
 	{
@@ -14,6 +16,16 @@ const routes = [
 				name: 'Dashboard',
 				name: 'Dashboard',
 				component: Dashboard
 				component: Dashboard
 			},
 			},
+			{
+				path: 'statistics',
+				name: 'Statistics',
+				component: Statistics
+			},
+			{
+				path: 'chat',
+				name: 'Chat',
+				component: Chat
+			},
 			{
 			{
 				path: 'workflow/:id',
 				path: 'workflow/:id',
 				name: 'Editor',
 				name: 'Editor',

+ 96 - 0
apps/web/src/store/modules/chat.store.ts

@@ -0,0 +1,96 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export interface Message {
+  id: string
+  content: string
+  role: 'user' | 'assistant'
+  timestamp: number
+}
+
+export interface Conversation {
+  id: string
+  title: string
+  createdAt: number
+  updatedAt: number
+  messages: Message[]
+}
+
+export const useChatStore = defineStore('chat', () => {
+  const conversations = ref<Conversation[]>([])
+  const activeConversationId = ref<string>('')
+
+  // 创建新对话
+  const createConversation = () => {
+    const id = `conv_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
+    const newConversation: Conversation = {
+      id,
+      title: `对话 ${conversations.value.length + 1}`,
+      createdAt: Date.now(),
+      updatedAt: Date.now(),
+      messages: []
+    }
+    conversations.value.unshift(newConversation)
+    activeConversationId.value = id
+    return id
+  }
+
+  // 获取活跃对话
+  const getActiveConversation = () => {
+    return conversations.value.find(c => c.id === activeConversationId.value)
+  }
+
+  // 添加消息
+  const addMessage = (conversationId: string, content: string, role: 'user' | 'assistant') => {
+    const conversation = conversations.value.find(c => c.id === conversationId)
+    if (conversation) {
+      const message: Message = {
+        id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
+        content,
+        role,
+        timestamp: Date.now()
+      }
+      conversation.messages.push(message)
+      conversation.updatedAt = Date.now()
+
+      // 更新标题(第一条消息)
+      if (conversation.messages.length === 1 && role === 'user') {
+        conversation.title = content.slice(0, 30) || '新对话'
+      }
+    }
+  }
+
+  // 删除对话
+  const deleteConversation = (id: string) => {
+    const index = conversations.value.findIndex(c => c.id === id)
+    if (index > -1) {
+      conversations.value.splice(index, 1)
+      if (activeConversationId.value === id) {
+        activeConversationId.value = conversations.value[0]?.id || ''
+      }
+    }
+  }
+
+  // 设置活跃对话
+  const setActiveConversation = (id: string) => {
+    activeConversationId.value = id
+  }
+
+  // 初始化:创建默认对话
+  const initializeChat = () => {
+    if (conversations.value.length === 0) {
+      createConversation()
+    }
+  }
+
+  return {
+    conversations,
+    activeConversationId,
+    createConversation,
+    getActiveConversation,
+    addMessage,
+    deleteConversation,
+    setActiveConversation,
+    initializeChat
+  }
+})

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 576 - 0
apps/web/src/views/Chat.vue


+ 79 - 70
apps/web/src/views/Dashboard.vue

@@ -1,55 +1,63 @@
 <template>
 <template>
-	<div style="margin: -16px -16px 0 -16px">
-		<!-- 顶部栏 -->
-		<div
-			style="
-				height: 64px;
-				display: flex;
-				align-items: center;
-				padding: 0 16px;
-				justify-content: space-between;
-				background: #fff;
-				border-bottom: 1px solid #f0f0f0;
-			"
-		>
-			<div style="display: flex; align-items: center; gap: 12px">
-				<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-dropdown style="background: #ff6b6b" split-button type="primary" @click="handleMenuClick(buttonConfig.text)">
-					{{ buttonConfig.text }}
-					<template #dropdown>
-						<el-dropdown-menu>
-							<el-dropdown-item v-for="item in buttonConfig.items" :key="item" @click="handleMenuClick(item)">
-								{{ item }}
-							</el-dropdown-item>
-						</el-dropdown-menu>
-					</template>
-				</el-dropdown>
-			</div>
+	<!-- 顶部栏 -->
+	<div
+		style="
+			height: 64px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			justify-content: space-between;
+			background: #fff;
+			border-bottom: 1px solid #f0f0f0;
+		"
+	>
+		<div style="display: flex; align-items: center; gap: 12px">
+			<div style="font-weight: 700; font-size: 18px">AI Agent</div>
+			<div style="color: #888; font-size: 13px">概述</div>
 		</div>
 		</div>
 
 
-		<div style="padding: 16px">
-			<el-card shadow="never" style="padding: 18px">
-				<el-row :gutter="16" justify="start">
-					<el-col :span="4" v-for="(card, idx) in cards" :key="idx">
-						<div
-							style="
-								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="display: flex; align-items: center; gap: 12px">
+			<el-dropdown
+				style="background: #ff6b6b"
+				split-button
+				type="primary"
+				@click="handleMenuClick(buttonConfig.text)"
+			>
+				{{ buttonConfig.text }}
+				<template #dropdown>
+					<el-dropdown-menu>
+						<el-dropdown-item
+							v-for="item in buttonConfig.items"
+							:key="item"
+							@click="handleMenuClick(item)"
 						>
 						>
-							<div style="font-size: 13px; color: #888">{{ card.title }}</div>
-							<div style="font-size: 20px; margin-top: 6px; font-weight: 600">{{ card.value }}</div>
-						</div>
-					</el-col>
-				</el-row>
-			</el-card>
+							{{ item }}
+						</el-dropdown-item>
+					</el-dropdown-menu>
+				</template>
+			</el-dropdown>
+		</div>
+	</div>
+
+	<div style="padding: 16px">
+		<el-card shadow="never" style="padding: 18px">
+			<el-row :gutter="16" justify="start">
+				<el-col :span="4" v-for="(card, idx) in cards" :key="idx">
+					<div
+						style="
+							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; font-weight: 600">{{ card.value }}</div>
+					</div>
+				</el-col>
+			</el-row>
+		</el-card>
 
 
 		<el-card style="margin-top: 16px">
 		<el-card style="margin-top: 16px">
 			<el-tabs v-model="dashboardStore.activeTab">
 			<el-tabs v-model="dashboardStore.activeTab">
@@ -410,25 +418,26 @@
 					gap: 12px;
 					gap: 12px;
 				"
 				"
 			>
 			>
-				<span style="color: #666; font-size: 13px">总计 {{ getTabData.length }}</span>
 				<el-pagination
 				<el-pagination
 					background
 					background
 					:page-size="pageSize"
 					:page-size="pageSize"
 					:current-page="currentPage"
 					:current-page="currentPage"
+					:page-sizes="[10, 20, 50]"
 					@update:current-page="currentPage = $event"
 					@update:current-page="currentPage = $event"
+					@update:page-size="pageSize = $event"
 					:total="getTabData.length"
 					:total="getTabData.length"
-					layout="prev, pager, next"
+					layout="total, prev, pager, next, sizes"
 				/>
 				/>
-				<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>
 			</div>
 		</el-card>
 		</el-card>
 
 
 		<!-- 新变量对话框 -->
 		<!-- 新变量对话框 -->
-	<el-dialog v-model="dashboardStore.showVarDialog" :title="varDialogTitle" width="500px" @close="resetVarForm">
+		<el-dialog
+			v-model="dashboardStore.showVarDialog"
+			:title="varDialogTitle"
+			width="500px"
+			@close="resetVarForm"
+		>
 			<el-form ref="varFormRef" :model="varForm" :rules="varFormRules" label-position="top">
 			<el-form ref="varFormRef" :model="varForm" :rules="varFormRules" label-position="top">
 				<el-form-item label="Key" prop="key">
 				<el-form-item label="Key" prop="key">
 					<el-input v-model="varForm.key" placeholder="请输入key" />
 					<el-input v-model="varForm.key" placeholder="请输入key" />
@@ -444,14 +453,19 @@
 			</el-form>
 			</el-form>
 			<template #footer>
 			<template #footer>
 				<div style="text-align: right">
 				<div style="text-align: right">
-				<el-button @click="dashboardStore.closeVarDialog">取消</el-button>
+					<el-button @click="dashboardStore.closeVarDialog">取消</el-button>
 					<el-button type="primary" @click="submitVariable">提交</el-button>
 					<el-button type="primary" @click="submitVariable">提交</el-button>
 				</div>
 				</div>
 			</template>
 			</template>
 		</el-dialog>
 		</el-dialog>
 
 
 		<!-- 创建新数据表对话框 -->
 		<!-- 创建新数据表对话框 -->
-	<el-dialog v-model="dashboardStore.showTableDialog" title="创建新数据表" width="500px" @close="resetTableForm">
+		<el-dialog
+			v-model="dashboardStore.showTableDialog"
+			title="创建新数据表"
+			width="500px"
+			@close="resetTableForm"
+		>
 			<el-form ref="tableFormRef" :model="tableForm" :rules="tableFormRules" label-position="top">
 			<el-form ref="tableFormRef" :model="tableForm" :rules="tableFormRules" label-position="top">
 				<el-form-item label="数据表名称" prop="name">
 				<el-form-item label="数据表名称" prop="name">
 					<el-input v-model="tableForm.name" placeholder="输入数据表名称" />
 					<el-input v-model="tableForm.name" placeholder="输入数据表名称" />
@@ -465,12 +479,11 @@
 			</el-form>
 			</el-form>
 			<template #footer>
 			<template #footer>
 				<div style="text-align: right">
 				<div style="text-align: right">
-				<el-button @click="dashboardStore.closeTableDialog">取消</el-button>
+					<el-button @click="dashboardStore.closeTableDialog">取消</el-button>
 					<el-button type="primary" @click="submitTable">创建</el-button>
 					<el-button type="primary" @click="submitTable">创建</el-button>
 				</div>
 				</div>
 			</template>
 			</template>
 		</el-dialog>
 		</el-dialog>
-		</div>
 	</div>
 	</div>
 </template>
 </template>
 
 
@@ -499,13 +512,13 @@ const buttonConfig = computed(() => {
 
 
 const handleMenuClick = (text: string) => {
 const handleMenuClick = (text: string) => {
 	const actionMap: Record<string, () => void> = {
 	const actionMap: Record<string, () => void> = {
-		'创建工作流程': () => $router.push('/workflow/0'),
-		'创建凭证': () => console.log('创建凭证'),
-		'创建变量': () => {
+		创建工作流程: () => $router.push('/workflow/0'),
+		创建凭证: () => console.log('创建凭证'),
+		创建变量: () => {
 			dashboardStore.setActiveTab('vars')
 			dashboardStore.setActiveTab('vars')
 			dashboardStore.openVarDialog()
 			dashboardStore.openVarDialog()
 		},
 		},
-		'创建数据表': () => {
+		创建数据表: () => {
 			dashboardStore.setActiveTab('tables')
 			dashboardStore.setActiveTab('tables')
 			dashboardStore.openTableDialog()
 			dashboardStore.openTableDialog()
 		}
 		}
@@ -606,9 +619,7 @@ const tableFormRules = {
 		{ required: true, message: '请输入数据表名称', trigger: 'blur' },
 		{ required: true, message: '请输入数据表名称', trigger: 'blur' },
 		{ min: 1, max: 100, message: '数据表名称长度为 1-100 个字符', trigger: 'blur' }
 		{ min: 1, max: 100, message: '数据表名称长度为 1-100 个字符', trigger: 'blur' }
 	],
 	],
-	method: [
-		{ required: true, message: '请选择创建方式', trigger: 'change' }
-	]
+	method: [{ required: true, message: '请选择创建方式', trigger: 'change' }]
 }
 }
 
 
 const submitTable = async () => {
 const submitTable = async () => {
@@ -666,9 +677,7 @@ const varFormRules = {
 		{ min: 1, max: 100, message: 'Key 长度为 1-100 个字符', trigger: 'blur' }
 		{ min: 1, max: 100, message: 'Key 长度为 1-100 个字符', trigger: 'blur' }
 	],
 	],
 	value: [],
 	value: [],
-	scope: [
-		{ required: true, message: '请选择范围', trigger: 'change' }
-	]
+	scope: [{ required: true, message: '请选择范围', trigger: 'change' }]
 }
 }
 
 
 const submitVariable = async () => {
 const submitVariable = async () => {

+ 373 - 0
apps/web/src/views/Statistics.vue

@@ -0,0 +1,373 @@
+<template>
+	<!-- 顶部栏 -->
+	<div
+		style="
+			height: 64px;
+			display: flex;
+			align-items: center;
+			padding: 0 16px;
+			justify-content: space-between;
+			background: #fff;
+			border-bottom: 1px solid #f0f0f0;
+		"
+	>
+		<div style="display: flex; align-items: center; gap: 12px">
+			<div style="font-weight: 700; font-size: 18px">统计</div>
+			<div style="color: #888; font-size: 13px">所有项目</div>
+		</div>
+
+		<div style="display: flex; align-items: center; gap: 12px">
+			<el-date-picker
+				v-model="dateRange"
+				type="daterange"
+				range-separator="至"
+				start-placeholder="开始日期"
+				end-placeholder="结束日期"
+				format="YYYY年MM月DD日"
+			/>
+		</div>
+	</div>
+
+	<div style="padding: 16px">
+		<!-- 统计卡片 -->
+		<el-card shadow="never" style="padding: 18px; background-color: #fff">
+			<el-row :gutter="24" justify="start">
+				<el-col :span="4" v-for="(card, idx) in statsData" :key="idx">
+					<div
+						@click="selectCard(idx)"
+						:style="{
+							background:
+								selectedCardIdx === idx
+									? 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)'
+									: 'linear-gradient(135deg, #fff 0%, #fafafa 100%)',
+							padding: '20px 16px',
+							borderRadius: '8px',
+							boxShadow:
+								selectedCardIdx === idx
+									? '0 4px 12px rgba(0, 118, 255, 0.15)'
+									: '0 2px 8px rgba(0, 0, 0, 0.08)',
+							border: selectedCardIdx === idx ? '1px solid #0076ff' : '1px solid #f0f0f0',
+							textAlign: 'center',
+							cursor: 'pointer',
+							transition: 'all 0.3s ease'
+						}"
+					>
+						<div style="font-size: 12px; color: #999; margin-bottom: 8px">{{ card.title }}</div>
+						<div style="font-size: 32px; font-weight: 600; color: #262626">{{ card.value }}</div>
+						<div style="font-size: 12px; color: #bbb; margin-top: 8px">{{ card.subtitle }}</div>
+					</div>
+				</el-col>
+			</el-row>
+		</el-card>
+
+		<!-- 执行统计图表 -->
+		<el-card style="margin-top: 16px; padding: 18px">
+			<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px">
+				<div style="font-weight: 600">{{ currentChartTitle }}</div>
+				<div style="display: flex; gap: 8px">
+					<div style="display: flex; align-items: center; gap: 4px">
+						<div style="width: 12px; height: 12px; background: #13c2c2; border-radius: 2px"></div>
+						<span style="font-size: 12px; color: #666">Successful</span>
+					</div>
+					<div style="display: flex; align-items: center; gap: 4px">
+						<div style="width: 12px; height: 12px; background: #ff6b6b; border-radius: 2px"></div>
+						<span style="font-size: 12px; color: #666">Failed</span>
+					</div>
+				</div>
+			</div>
+			<ExecutionChart :data="currentChartData" />
+		</el-card>
+
+		<!-- 执行统计表格 -->
+		<el-card style="margin-top: 16px; padding: 18px">
+			<div style="font-weight: 600; margin-bottom: 16px; font-size: 14px">
+				{{ currentTableTitle }}
+			</div>
+			<el-table :data="filteredTableData" style="width: 100%" stripe border>
+				<el-table-column type="selection" width="50" />
+				<el-table-column prop="name" label="姓名" width="120" />
+				<el-table-column prop="totalExecutions" label="生产执行次数↓" width="140" />
+				<el-table-column prop="failedExecutions" label="生产环境执行失败数" width="160" />
+				<el-table-column prop="failureRate" label="故障率" width="100" />
+				<el-table-column prop="timeSaved" label="节省时间" width="100" />
+				<el-table-column prop="avgRuntime" label="运行时间(平均)" width="140" />
+				<el-table-column prop="projectName" label="项目名称" min-width="180" />
+			</el-table>
+
+			<!-- 分页器 -->
+			<div
+				style="
+					display: flex;
+					justify-content: flex-end;
+					align-items: center;
+					margin-top: 16px;
+					gap: 12px;
+				"
+			>
+				<el-pagination
+					background
+					:page-size="pageSize"
+					:current-page="currentPage"
+					:page-sizes="[10, 20, 50]"
+					@update:current-page="currentPage = $event"
+					@update:page-size="pageSize = $event"
+					:total="filteredTableData.length"
+					layout="total, prev, pager, next, sizes"
+				/>
+			</div>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import ExecutionChart from '@/components/Chart/ExecutionChart.vue'
+
+// 日期范围
+const dateRange = ref<[Date, Date] | null>([new Date('2026-01-20'), new Date('2026-01-27')])
+
+// 当前选中的卡片索引
+const selectedCardIdx = ref(0)
+
+// 统计数据
+const statsData = [
+	{ title: '生产执行', value: 12, subtitle: '过去7天' },
+	{ title: '生产环境执行失败', value: 3, subtitle: '过去7天' },
+	{ title: '故障率', value: '25%', subtitle: '过去7天' },
+	{ title: '节省时间', value: '8.2h', subtitle: '过去7天' },
+	{ title: '运行时间(平均)', value: '5.1s', subtitle: '过去7天' }
+]
+
+// 不同卡片对应的图表和表格数据
+const cardDataMap = [
+	{
+		// 生产执行
+		chartTitle: '生产执行 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [8, 12, 15, 24, 18, 10, 5],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '生产执行明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 生产环境执行失败
+		chartTitle: '生产环境执行失败 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [2, 3, 1, 2, 1, 0, 0],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '生产环境失败执行明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '2.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '1.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 1,
+				failedExecutions: 1,
+				failureRate: '100%',
+				timeSaved: '0h',
+				avgRuntime: '3.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 故障率
+		chartTitle: '故障率 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [5, 8, 12, 18, 15, 8, 4],
+			failed: [1, 2, 1, 3, 2, 1, 0]
+		},
+		tableTitle: '故障率统计明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			}
+		]
+	},
+	{
+		// 节省时间
+		chartTitle: '节省时间 - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [12, 18, 20, 28, 22, 15, 8],
+			failed: [0, 1, 0, 2, 1, 0, 0]
+		},
+		tableTitle: '节省时间统计明细',
+		tableData: [
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			}
+		]
+	},
+	{
+		// 运行时间(平均)
+		chartTitle: '运行时间(平均) - 按时间统计',
+		chartData: {
+			categories: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+			successful: [6, 10, 14, 22, 18, 9, 4],
+			failed: [1, 1, 0, 2, 1, 1, 0]
+		},
+		tableTitle: '运行时间统计明细',
+		tableData: [
+			{
+				name: '项目2',
+				totalExecutions: 18,
+				failedExecutions: 1,
+				failureRate: '5.6%',
+				timeSaved: '4.2h',
+				avgRuntime: '4.8s',
+				projectName: 'AI 分析项目'
+			},
+			{
+				name: '项目3',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.3s',
+				projectName: '图像识别项目'
+			},
+			{
+				name: '项目1',
+				totalExecutions: 12,
+				failedExecutions: 1,
+				failureRate: '8.3%',
+				timeSaved: '2.5h',
+				avgRuntime: '5.1s',
+				projectName: '数据处理项目'
+			}
+		]
+	}
+]
+
+// 当前选中卡片的图表标题
+const currentChartTitle = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.chartTitle || '统计图表'
+})
+
+// 当前选中卡片的图表数据
+const currentChartData = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.chartData || cardDataMap[0].chartData
+})
+
+// 当前选中卡片的表格标题
+const currentTableTitle = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.tableTitle || '统计明细'
+})
+
+// 当前选中卡片的表格数据
+const filteredTableData = computed(() => {
+	return cardDataMap[selectedCardIdx.value]?.tableData || []
+})
+
+// 点击卡片
+const selectCard = (idx: number) => {
+	selectedCardIdx.value = idx
+	currentPage.value = 1
+}
+
+// 分页
+const pageSize = ref(10)
+const currentPage = ref(1)
+</script>
+
+<style scoped>
+:deep(.el-date-picker) {
+	width: 280px;
+}
+
+:deep(.el-card) {
+	border: 1px solid #f0f0f0;
+}
+
+:deep(.el-table) {
+	font-size: 13px;
+}
+
+:deep(.el-table__header-wrapper) {
+	background-color: #fafafa;
+}
+
+:deep(.el-pagination) {
+	margin-top: 16px;
+}
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1241 - 40
pnpm-lock.yaml