feat: 建立Vue3前端框架

This commit is contained in:
2026-05-20 00:08:33 +08:00
parent bc225d3557
commit e68150ad02
25 changed files with 3895 additions and 1 deletions

3
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<RouterView />
</template>

View File

@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest';
import { request } from '../request';
describe('request client', () => {
it('uses the backend api prefix', () => {
expect(request.defaults.baseURL).toBe('/api');
});
});

View File

@@ -0,0 +1,21 @@
import axios from 'axios';
import type { AxiosRequestConfig } from 'axios';
export interface RequestResult<T> {
code: number;
message: string;
data: T;
}
export const request = axios.create({
baseURL: '/api',
timeout: 15000,
});
export function get<T>(url: string, config?: AxiosRequestConfig) {
return request.get<RequestResult<T>>(url, config).then((response) => response.data);
}
export function post<T, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig) {
return request.post<RequestResult<T>>(url, data, config).then((response) => response.data);
}

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import {
Box,
Collection,
DataBoard,
Document,
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 },
{ path: '/system/enums', label: '系统枚举', icon: Grid },
{ path: '/system/attachments', label: '附件管理', icon: Files },
{ path: '/rag/stores', label: '知识库', icon: Collection },
{ path: '/rag/documents', label: '知识文档', icon: Document },
];
</script>
<template>
<el-container class="admin-layout">
<el-aside class="admin-sidebar" width="232px">
<div class="brand">
<el-icon :size="24">
<Box />
</el-icon>
<span>Common Agent</span>
</div>
<el-menu class="side-menu" :default-active="$route.path" router>
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="admin-header">
<div>
<h1>{{ pageTitle }}</h1>
<p>{{ appStore.environmentName }}</p>
</div>
</el-header>
<el-main class="admin-main">
<RouterView />
</el-main>
</el-container>
</el-container>
</template>

17
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import 'element-plus/dist/index.css';
import './styles/global.css';
import ElementPlus from 'element-plus';
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(ElementPlus);
app.mount('#app');

View File

@@ -0,0 +1,8 @@
<template>
<section class="page-panel">
<div class="page-panel__header">
<h2>工作台</h2>
<span>Console</span>
</div>
</section>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<main class="not-found">
<h1>404</h1>
<RouterLink to="/dashboard">返回工作台</RouterLink>
</main>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<section class="page-panel">
<div class="page-panel__header">
<h2>知识文档</h2>
<span>Documents</span>
</div>
</section>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<section class="page-panel">
<div class="page-panel__header">
<h2>知识库</h2>
<span>RAG</span>
</div>
</section>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<section class="page-panel">
<div class="page-panel__header">
<h2>附件管理</h2>
<span>Files</span>
</div>
</section>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<section class="page-panel">
<div class="page-panel__header">
<h2>系统枚举</h2>
<span>System</span>
</div>
</section>
</template>

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { routes } from '../index';
describe('router', () => {
it('defines the admin shell routes', () => {
const paths = routes.map((route) => route.path);
expect(paths).toContain('/');
expect(paths).toContain('/dashboard');
expect(paths).toContain('/system/enums');
expect(paths).toContain('/system/attachments');
expect(paths).toContain('/rag/stores');
expect(paths).toContain('/rag/documents');
expect(paths).toContain('/:pathMatch(.*)*');
});
});

View File

@@ -0,0 +1,109 @@
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import DashboardPage from '@/pages/DashboardPage.vue';
import NotFoundPage from '@/pages/NotFoundPage.vue';
import RagDocumentsPage from '@/pages/RagDocumentsPage.vue';
import RagStoresPage from '@/pages/RagStoresPage.vue';
import SystemAttachmentsPage from '@/pages/SystemAttachmentsPage.vue';
import SystemEnumsPage from '@/pages/SystemEnumsPage.vue';
import AdminLayout from '@/layouts/AdminLayout.vue';
export const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/dashboard',
name: 'dashboard',
component: DashboardPage,
meta: { title: '工作台' },
},
{
path: '/system/enums',
name: 'system-enums',
component: SystemEnumsPage,
meta: { title: '系统枚举' },
},
{
path: '/system/attachments',
name: 'system-attachments',
component: SystemAttachmentsPage,
meta: { title: '附件管理' },
},
{
path: '/rag/stores',
name: 'rag-stores',
component: RagStoresPage,
meta: { title: '知识库' },
},
{
path: '/rag/documents',
name: 'rag-documents',
component: RagDocumentsPage,
meta: { title: '知识文档' },
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundPage,
meta: { title: '页面不存在' },
},
];
const routerRoutes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/',
component: AdminLayout,
children: [
{
path: 'dashboard',
name: 'dashboard',
component: DashboardPage,
meta: { title: '工作台' },
},
{
path: 'system/enums',
name: 'system-enums',
component: SystemEnumsPage,
meta: { title: '系统枚举' },
},
{
path: 'system/attachments',
name: 'system-attachments',
component: SystemAttachmentsPage,
meta: { title: '附件管理' },
},
{
path: 'rag/stores',
name: 'rag-stores',
component: RagStoresPage,
meta: { title: '知识库' },
},
{
path: 'rag/documents',
name: 'rag-documents',
component: RagDocumentsPage,
meta: { title: '知识文档' },
},
],
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundPage,
meta: { title: '页面不存在' },
},
];
const router = createRouter({
history: createWebHistory(),
routes: routerRoutes,
});
export default router;

View File

@@ -0,0 +1,7 @@
import { defineStore } from 'pinia';
export const useAppStore = defineStore('app', {
state: () => ({
environmentName: '本地开发',
}),
});

View File

@@ -0,0 +1,128 @@
:root {
color: #1f2937;
background: #f4f6f8;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
margin: 0;
}
a {
color: inherit;
}
#app {
min-height: 100vh;
}
.admin-layout {
min-height: 100vh;
}
.admin-sidebar {
border-right: 1px solid #d9dee7;
background: #ffffff;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
height: 64px;
padding: 0 18px;
border-bottom: 1px solid #e5e9f0;
color: #182433;
font-size: 17px;
font-weight: 700;
}
.side-menu {
border-right: 0;
padding: 10px 8px;
}
.side-menu .el-menu-item {
height: 42px;
border-radius: 6px;
margin-bottom: 4px;
}
.admin-header {
display: flex;
align-items: center;
height: 64px;
border-bottom: 1px solid #d9dee7;
background: #ffffff;
}
.admin-header h1 {
margin: 0;
color: #172033;
font-size: 18px;
font-weight: 700;
letter-spacing: 0;
}
.admin-header p {
margin: 4px 0 0;
color: #667085;
font-size: 12px;
}
.admin-main {
padding: 20px;
background: #f4f6f8;
}
.page-panel {
min-height: calc(100vh - 104px);
border: 1px solid #d9dee7;
border-radius: 6px;
background: #ffffff;
}
.page-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 56px;
padding: 0 18px;
border-bottom: 1px solid #edf0f4;
}
.page-panel__header h2 {
margin: 0;
color: #1f2937;
font-size: 16px;
font-weight: 700;
letter-spacing: 0;
}
.page-panel__header span {
color: #768195;
font-size: 12px;
}
.not-found {
display: grid;
min-height: 100vh;
place-content: center;
gap: 16px;
text-align: center;
}
.not-found h1 {
margin: 0;
font-size: 48px;
letter-spacing: 0;
}