diff --git a/AGENT.md b/AGENT.md index 2273b28..e44f1f7 100644 --- a/AGENT.md +++ b/AGENT.md @@ -82,7 +82,25 @@ ### 3.4 管理控制台模块 -后续需要一个前端控制台,至少覆盖: +当前已经建立基于 Vue 3、Vite、Element Plus 的前端控制台基础骨架。 + +已具备的页面与布局: + +- 左侧管理菜单与品牌区 +- 工作台入口 +- 系统枚举管理页 +- 附件管理入口 +- 知识库入口 +- 知识文档入口 + +当前样式约定: + +- 管理控制台定位为后台工具界面,优先保证信息密度、可扫描性和稳定布局。 +- 主内容区域只保留页面自身标题,避免外层布局和页面面板重复展示标题。 +- 页面统一使用 `page-panel` 作为内容容器,侧边栏、页面面板、工具栏和表格使用统一边框、背景和主色变量。 +- 系统枚举页工具栏采用响应式布局,桌面端保持查询项和操作按钮分区,窄屏时纵向排列。 + +后续控制台至少继续覆盖: - 枚举管理 - 附件管理 @@ -159,6 +177,8 @@ 6. 补前端控制台 +当前前端控制台已经完成基础骨架和系统枚举页面样式优化,后续重点应转向附件、知识库、知识文档页面的业务闭环。 + ## 7. 下一步建议 结合当前代码状态,接下来建议重点做: @@ -168,6 +188,7 @@ - 完善 `rag_store` / `rag_document` 的增删改查 - 增加知识库文档上传并自动关联附件 - 为后续切片与向量化预留任务入口 +- 补齐前端附件、知识库、知识文档页面的表单、列表和接口联调 ## 8. 文档用途说明 diff --git a/README.md b/README.md index c356640..755766b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。 项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。 -当前项目处于基础工程阶段。后端骨架、PostgreSQL 配置、MyBatis-Plus、Lombok 和多环境配置文件已经完成; -Agent 运行时、RAG 索引、前端页面和管理功能会在后续阶段逐步实现。 +当前项目处于基础工程阶段。后端骨架、PostgreSQL 配置、MyBatis-Plus、Lombok、多环境配置文件和前端控制台基础页面已经完成; +Agent 运行时、RAG 索引和更多管理功能会在后续阶段逐步实现。 ## 项目愿景 @@ -31,6 +31,11 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台: ```text common_agent +├── frontend +│ ├── src/layouts +│ ├── src/pages +│ ├── src/router +│ └── src/styles ├── src/main/java/com/bruce │ └── CommonAgentApplication.java ├── src/main/resources @@ -99,10 +104,30 @@ npm run dev ```powershell cd frontend +npm run test:unit npm run type-check npm run build ``` +## 前端控制台 + +前端控制台位于 `frontend` 目录,当前使用 Vue 3、Vite、Pinia、Vue Router 和 Element Plus。 + +当前已有页面: + +- 工作台 +- 系统枚举 +- 附件管理 +- 知识库 +- 知识文档 + +当前 UI 规范: + +- 左侧为固定管理导航,右侧为主内容区。 +- 主内容区不再额外渲染外层页面标题,标题由各业务页面面板自行展示。 +- 全局样式集中在 `frontend/src/styles/global.css`,页面专属样式优先放在对应 `.vue` 文件的 scoped style 中。 +- 后台页面以清晰、克制、便于扫描为优先目标,避免营销式大面积装饰。 + ## 规划模块 - `agent-core`:Agent 执行模型、工具注册、记忆和编排能力。 diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 4529852..2f2af95 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -7,15 +7,6 @@ import { Files, Grid, } from '@element-plus/icons-vue'; -import { computed } from 'vue'; -import { useRoute } from 'vue-router'; - -import { useAppStore } from '@/stores/app'; - -const route = useRoute(); -const appStore = useAppStore(); - -const pageTitle = computed(() => String(route.meta.title ?? 'Common Agent')); const menuItems = [ { path: '/dashboard', label: '工作台', icon: DataBoard }, @@ -47,13 +38,6 @@ const menuItems = [ - -
-

{{ pageTitle }}

-

{{ appStore.environmentName }}

-
-
- diff --git a/frontend/src/layouts/__tests__/AdminLayout.spec.ts b/frontend/src/layouts/__tests__/AdminLayout.spec.ts new file mode 100644 index 0000000..63787cb --- /dev/null +++ b/frontend/src/layouts/__tests__/AdminLayout.spec.ts @@ -0,0 +1,29 @@ +import { mount } from '@vue/test-utils'; +import ElementPlus from 'element-plus'; +import { createPinia } from 'pinia'; +import { describe, expect, it, vi } from 'vitest'; + +import AdminLayout from '../AdminLayout.vue'; + +vi.mock('vue-router', () => ({ + useRoute: () => ({ meta: { title: '系统枚举' } }), +})); + +describe('AdminLayout', () => { + it('does not render a duplicate page header above the main page content', () => { + const wrapper = mount(AdminLayout, { + global: { + plugins: [createPinia(), ElementPlus], + mocks: { + $route: { path: '/system/enums' }, + }, + stubs: { + RouterView: { template: '
' }, + }, + }, + }); + + expect(wrapper.find('.admin-header').exists()).toBe(false); + expect(wrapper.find('[data-test="router-view"]').exists()).toBe(true); + }); +}); diff --git a/frontend/src/pages/SystemEnumsPage.vue b/frontend/src/pages/SystemEnumsPage.vue index 06e3d27..232ed4f 100644 --- a/frontend/src/pages/SystemEnumsPage.vue +++ b/frontend/src/pages/SystemEnumsPage.vue @@ -358,28 +358,58 @@ onMounted(loadEnums); } .enum-toolbar { - display: flex; - align-items: flex-start; - justify-content: space-between; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; gap: 16px; - padding: 16px 18px 4px; + padding: 18px 22px 8px; + border-bottom: 1px solid #eef2f7; + background: #ffffff; +} + +.enum-toolbar :deep(.el-form) { + display: flex; + flex-wrap: wrap; + gap: 0 14px; } .enum-toolbar :deep(.el-form-item) { - margin-bottom: 12px; + margin-right: 0; + margin-bottom: 10px; +} + +.enum-toolbar :deep(.el-form-item__label) { + color: #475467; + font-weight: 600; } .enum-toolbar :deep(.el-input) { - width: 180px; + width: 196px; } .enum-table { width: 100%; } +.enum-table :deep(.el-table__header-wrapper th) { + background: #f8fafc; + color: #667085; + font-weight: 700; +} + +.enum-table :deep(.el-table__row td) { + color: #344054; +} + +.enum-table :deep(.el-table__row:hover td) { + background: #f8fbff; +} + .enum-actions { display: flex; + justify-content: flex-end; gap: 8px; + padding-top: 1px; } .batch-base { @@ -401,4 +431,37 @@ onMounted(loadEnums); .batch-table { width: 100%; } + +@media (max-width: 1080px) { + .enum-toolbar { + grid-template-columns: 1fr; + } + + .enum-actions { + justify-content: flex-start; + } +} + +@media (max-width: 768px) { + .enum-toolbar { + padding: 14px 16px 8px; + } + + .enum-toolbar :deep(.el-form) { + display: grid; + grid-template-columns: 1fr; + } + + .enum-toolbar :deep(.el-input) { + width: 100%; + } + + .enum-actions { + flex-wrap: wrap; + } + + .batch-base { + grid-template-columns: 1fr; + } +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 46d8a7b..aeb9beb 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1,6 +1,13 @@ :root { + --app-bg: #f5f7fb; + --app-border: #e3e8f0; + --app-border-soft: #eef2f7; + --app-primary: #1677ff; + --app-primary-soft: #eaf3ff; + --app-text: #172033; + --app-text-muted: #667085; color: #1f2937; - background: #f4f6f8; + background: var(--app-bg); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; @@ -27,90 +34,96 @@ a { .admin-layout { min-height: 100vh; + background: var(--app-bg); } .admin-sidebar { - border-right: 1px solid #d9dee7; + border-right: 1px solid var(--app-border); background: #ffffff; + box-shadow: 1px 0 0 rgba(15, 23, 42, 0.02); } .brand { display: flex; align-items: center; gap: 10px; - height: 64px; - padding: 0 18px; - border-bottom: 1px solid #e5e9f0; - color: #182433; + height: 72px; + padding: 0 22px; + border-bottom: 1px solid var(--app-border-soft); + color: var(--app-text); font-size: 17px; font-weight: 700; } +.brand .el-icon { + color: var(--app-primary); +} + .side-menu { border-right: 0; - padding: 10px 8px; + padding: 14px 12px; } .side-menu .el-menu-item { - height: 42px; - border-radius: 6px; + height: 44px; + border-radius: 8px; margin-bottom: 4px; + color: #344054; + font-size: 15px; } -.admin-header { - display: flex; - align-items: center; - height: 64px; - border-bottom: 1px solid #d9dee7; - background: #ffffff; +.side-menu .el-menu-item:hover { + background: #f3f7fd; } -.admin-header h1 { - margin: 0; - color: #172033; - font-size: 18px; +.side-menu .el-menu-item.is-active { + background: var(--app-primary-soft); + color: var(--app-primary); font-weight: 700; - letter-spacing: 0; } -.admin-header p { - margin: 4px 0 0; - color: #667085; - font-size: 12px; +.side-menu .el-menu-item .el-icon { + font-size: 18px; } .admin-main { - padding: 20px; - background: #f4f6f8; + min-width: 0; + padding: 24px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0) 180px), + var(--app-bg); } .page-panel { - min-height: calc(100vh - 104px); - border: 1px solid #d9dee7; - border-radius: 6px; + min-height: calc(100vh - 48px); + border: 1px solid var(--app-border); + border-radius: 8px; background: #ffffff; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04); } .page-panel__header { display: flex; align-items: center; justify-content: space-between; - min-height: 56px; - padding: 0 18px; - border-bottom: 1px solid #edf0f4; + min-height: 64px; + padding: 0 22px; + border-bottom: 1px solid var(--app-border-soft); + background: linear-gradient(180deg, #ffffff, #fbfcff); } .page-panel__header h2 { margin: 0; - color: #1f2937; - font-size: 16px; + color: var(--app-text); + font-size: 18px; font-weight: 700; letter-spacing: 0; } .page-panel__header span { - color: #768195; + color: var(--app-text-muted); font-size: 12px; + font-weight: 600; } .not-found { @@ -126,3 +139,43 @@ a { font-size: 48px; letter-spacing: 0; } + +@media (max-width: 768px) { + .admin-layout { + display: block; + } + + .admin-sidebar { + width: 100% !important; + border-right: 0; + border-bottom: 1px solid var(--app-border); + } + + .brand { + height: 60px; + } + + .side-menu { + display: flex; + overflow-x: auto; + padding: 10px 12px; + } + + .side-menu .el-menu-item { + flex: 0 0 auto; + margin: 0 6px 0 0; + } + + .admin-main { + padding: 14px; + } + + .page-panel { + min-height: calc(100vh - 148px); + } + + .page-panel__header { + min-height: 58px; + padding: 0 16px; + } +}