补充dashboard

This commit is contained in:
yumaojun03 2025-08-10 13:54:58 +08:00
parent 783972ecab
commit fb832df8e1
17 changed files with 1324 additions and 93 deletions

View File

@ -11,6 +11,7 @@
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
@ -2388,6 +2389,24 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD",
"peer": true
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.194", "version": "1.5.194",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
@ -4647,6 +4666,51 @@
} }
} }
}, },
"node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-echarts": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-7.0.3.tgz",
"integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==",
"license": "MIT",
"dependencies": {
"vue-demi": "^0.13.11"
},
"peerDependencies": {
"@vue/runtime-core": "^3.0.0",
"echarts": "^5.5.1",
"vue": "^2.7.0 || ^3.1.1"
},
"peerDependenciesMeta": {
"@vue/runtime-core": {
"optional": true
}
}
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz",
@ -4771,6 +4835,23 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD",
"peer": true
} }
} }
} }

View File

@ -17,6 +17,7 @@
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,154 @@
<template>
<div class="layout-container">
<!-- 背景装饰元素 -->
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-wave"></div>
<!-- 使用顶部导航组件 -->
<HeaderNav :active-key="activeMenuKey" @menu-change="handleMenuChange" @user-option-click="handleUserOption" />
<!-- 主内容区 -->
<div class="main-content-wrapper">
<!-- 内容滚动区域 -->
<div class="scrollable-content">
<div class="content-scroll-wrapper">
<!-- 主内容区 -->
<a-watermark :content="token.user_name" :font="{ color: 'rgba(0, 0, 0, 0.06)' }">
<main class="router-view-wrapper">
<router-view />
</main>
</a-watermark>
<!-- 页脚 -->
<footer class="layout-footer">
<p>© 2025 研发交付平台 · 让软件交付更高效</p>
</footer>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import HeaderNav from './components/HeaderNav.vue';
import { useRouter } from 'vue-router';
import token from '@/storage/token'
const router = useRouter()
const activeMenuKey = ref('DashBoard');
const handleMenuChange = (key) => {
activeMenuKey.value = key;
router.push({ name: key })
//
console.log('菜单切换:', key);
};
const handleUserOption = (option) => {
console.log('用户操作:', option);
// option
switch (option) {
case 'profile':
//
break;
case 'settings':
//
break;
case 'logout':
// 退
break;
}
};
</script>
<style lang="less" scoped>
/* 基础布局 */
.layout-container {
min-height: 100vh;
background-color: #f5f7fa;
position: relative;
overflow: hidden;
.decoration-circle {
position: fixed;
border-radius: 50%;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
z-index: 0;
&.circle-1 {
width: 600px;
height: 600px;
top: -200px;
left: -200px;
}
&.circle-2 {
width: 400px;
height: 400px;
bottom: -100px;
right: -100px;
}
}
.decoration-wave {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 70px;
background: url('@/assets/svg/wave.svg') repeat-x;
background-size: 1000px 100px;
opacity: 0.1;
}
}
/* 主内容区 */
.main-content-wrapper {
padding-top: 60px;
height: calc(100vh - 60px);
}
/* 内容滚动区域 */
.scrollable-content {
height: 100%;
overflow-y: scroll;
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE/Edge */
&::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
.content-scroll-wrapper {
min-height: 100%;
display: flex;
flex-direction: column;
}
}
/* 路由视图容器 */
.router-view-wrapper {
flex: 1;
padding: 20px;
min-height: calc(100vh - 180px);
/* 调整最小高度 */
}
/* 页脚 */
.layout-footer {
padding: 16px 24px;
margin-top: auto;
/* 关键:使页脚始终在底部 */
text-align: center;
color: var(--color-text-3);
font-size: 12px;
border-top: 1px solid var(--color-border-2);
background: var(--color-bg-2);
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<div class="layout-container">
<!-- 背景装饰元素 -->
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-wave"></div>
<!-- 使用顶部导航组件 -->
<div class="fixed-header">
<div class="header-content">
<div class="logo-section">
<h1 class="platform-name">研发交付平台</h1>
</div>
<div class="main-nav-section">
<a-menu mode="horizontal" :default-selected-keys="[activeKey]">
<a-menu-item v-for="item in menuItems" :key="item.key" @click="handleMenuClick(item)">
{{ item.label }}
</a-menu-item>
</a-menu>
</div>
<div class="user-section">
<a-button type="text" @click="() => $router.push({ name: 'LoginPage' })">登录</a-button>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content-wrapper">
<!-- 内容滚动区域 -->
<div class="scrollable-content">
<div class="content-scroll-wrapper">
<!-- 主内容区 -->
<main class="router-view-wrapper">
<router-view />
</main>
<!-- 页脚 -->
<footer class="layout-footer">
<p>© 2025 研发交付平台 · 让软件交付更高效</p>
</footer>
</div>
</div>
</div>
</div>
</template>
<script setup>
</script>
<style lang="less" scoped>
/* 基础布局 */
.layout-container {
min-height: 100vh;
background-color: #f5f7fa;
position: relative;
overflow: hidden;
.decoration-circle {
position: fixed;
border-radius: 50%;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
z-index: 0;
&.circle-1 {
width: 600px;
height: 600px;
top: -200px;
left: -200px;
}
&.circle-2 {
width: 400px;
height: 400px;
bottom: -100px;
right: -100px;
}
}
.decoration-wave {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 70px;
background: url('@/assets/svg/wave.svg') repeat-x;
background-size: 1000px 100px;
opacity: 0.1;
}
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
z-index: 100;
.header-content {
max-width: 100%;
height: 100%;
padding: 0 24px;
margin: 0 auto;
display: flex;
align-items: center;
.logo-section {
.platform-name {
color: var(--color-text-1);
font-size: 18px;
font-weight: 600;
margin: 0;
white-space: nowrap;
}
}
.main-nav-section {
flex: 1;
margin: 0 40px;
overflow-x: auto;
overflow-y: hidden;
:deep(.arco-menu-horizontal) {
height: 60px;
background: transparent;
border-bottom: none;
}
}
.user-avatar {
background-color: var(--color-primary-light-3);
color: var(--color-primary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--color-primary-light-2);
}
}
}
}
/* 主内容区 */
.main-content-wrapper {
padding-top: 60px;
height: calc(100vh - 60px);
}
/* 内容滚动区域 */
.scrollable-content {
height: 100%;
overflow-y: scroll;
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE/Edge */
&::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
.content-scroll-wrapper {
min-height: 100%;
display: flex;
flex-direction: column;
}
}
/* 路由视图容器 */
.router-view-wrapper {
flex: 1;
padding: 20px;
min-height: calc(100vh - 180px);
/* 调整最小高度 */
}
/* 页脚 */
.layout-footer {
padding: 16px 24px;
margin-top: auto;
/* 关键:使页脚始终在底部 */
text-align: center;
color: var(--color-text-3);
font-size: 12px;
border-top: 1px solid var(--color-border-2);
background: var(--color-bg-2);
}
</style>

View File

@ -5,63 +5,21 @@
<div class="decoration-circle circle-2"></div> <div class="decoration-circle circle-2"></div>
<div class="decoration-wave"></div> <div class="decoration-wave"></div>
<!-- 固定顶部导航栏 --> <!-- 使用新的顶部导航组件 -->
<div class="fixed-header"> <HeaderNav :active-key="activeMenuKey" @menu-change="handleMenuChange" @user-option-click="handleUserOption" />
<div class="header-content">
<div class="logo-section">
<h1 class="platform-name">研发交付平台</h1>
</div>
<div class="main-nav-section">
<a-menu mode="horizontal" :default-selected-keys="['1']">
<a-menu-item key="1">工作台</a-menu-item>
<a-menu-item key="2">项目管理</a-menu-item>
<a-menu-item key="3">研发交付</a-menu-item>
<a-menu-item key="4">制品库</a-menu-item>
<a-menu-item key="5">测试中心</a-menu-item>
<a-menu-item key="6">运维中心</a-menu-item>
</a-menu>
</div>
<div class="user-section">
<a-dropdown position="bottom">
<a-avatar :size="32" class="user-avatar">
<icon-user />
</a-avatar>
<template #content>
<a-doption>个人中心</a-doption>
<a-doption>系统设置</a-doption>
<a-doption>退出登录</a-doption>
</template>
</a-dropdown>
</div>
</div>
</div>
<!-- 主内容区 --> <!-- 主内容区 -->
<div class="main-content-wrapper"> <div class="main-content-wrapper">
<!-- 可收缩侧边栏 --> <!-- 可收缩侧边栏 -->
<div class="fixed-sidebar" :class="{ 'collapsed': isSidebarCollapsed }"> <div class="fixed-sidebar" :class="{ 'collapsed': isSidebarCollapsed }">
<!-- 使用原生a-menu确保折叠效果 --> <!-- 使用原生a-menu确保折叠效果 -->
<a-menu :theme="light" :default-selected-keys="['1']" :collapsed="isSidebarCollapsed" :collapsed-width="64" <a-menu theme="light" :default-selected-keys="['1']" :collapsed="isSidebarCollapsed" :collapsed-width="64"
:style="{ width: '100%', height: '100%' }"> :style="{ width: '100%', height: '100%' }">
<a-menu-item key="1"> <a-menu-item v-for="menu in currentMenus" :key="menu.key">
<template #icon><icon-dashboard /></template> <template #icon>
<span class="menu-title">流水线列表</span> <component :is="menu.icon"></component>
</a-menu-item> </template>
<a-menu-item key="2"> <span class="menu-title">{{ menu.title }}</span>
<template #icon><icon-branch /></template>
<span class="menu-title">分支管理</span>
</a-menu-item>
<a-menu-item key="3">
<template #icon><icon-history /></template>
<span class="menu-title">执行历史</span>
</a-menu-item>
<a-menu-item key="4">
<template #icon><icon-settings /></template>
<span class="menu-title">流水线模板</span>
</a-menu-item>
<a-menu-item key="5">
<template #icon><icon-monitor /></template>
<span class="menu-title">监控中心</span>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
@ -89,7 +47,7 @@
</div> </div>
<!-- 主内容区 --> <!-- 主内容区 -->
<a-watermark content="arco.design" :font="{ color: 'rgba(0, 0, 0, 0.06)' }"> <a-watermark :content="token.user_name" :font="{ color: 'rgba(0, 0, 0, 0.06)' }">
<main class="router-view-wrapper"> <main class="router-view-wrapper">
<router-view /> <router-view />
</main> </main>
@ -106,13 +64,86 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { computed, ref, shallowReactive } from 'vue';
import HeaderNav from './components/HeaderNav.vue';
import { useRouter } from 'vue-router';
import { IconApps, IconBranch, IconHistory, IconSettings, IconTags } from '@arco-design/web-vue/es/icon';
import token from '@/storage/token'
const router = useRouter()
const isSidebarCollapsed = ref(false); const isSidebarCollapsed = ref(false);
const activeMenuKey = ref('ProjectSystem');
const toggleSidebar = () => { const toggleSidebar = () => {
isSidebarCollapsed.value = !isSidebarCollapsed.value; isSidebarCollapsed.value = !isSidebarCollapsed.value;
}; };
const handleMenuChange = (key) => {
activeMenuKey.value = key;
router.push({ name: key })
//
console.log('菜单切换:', key);
};
const handleUserOption = (option) => {
console.log('用户操作:', option);
// option
switch (option) {
case 'profile':
//
break;
case 'settings':
//
break;
case 'logout':
// 退
break;
}
};
const currentMenus = computed(() => {
return systemMenus[activeMenuKey.value]
})
const systemMenus = shallowReactive({
ProjectSystem: [
{
key: '1',
icon: IconApps,
title: '应用管理'
},
],
DevelopSystem: [
{
key: '1',
icon: IconTags,
title: '版本迭代'
},
{
key: '2',
icon: IconBranch,
title: '分支管理'
},
{
key: '3',
icon: IconHistory,
title: '执行历史'
},
{
key: '4',
icon: IconSettings,
title: '流水线模板'
},
{
key: '5',
title: '监控中心'
}
]
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -0,0 +1,125 @@
<template>
<div class="fixed-header">
<div class="header-content">
<div class="logo-section">
<h1 class="platform-name">研发交付平台</h1>
</div>
<div class="main-nav-section">
<a-menu mode="horizontal" :default-selected-keys="[activeKey]">
<a-menu-item v-for="item in menuItems" :key="item.key" @click="handleMenuClick(item)">
{{ item.label }}
</a-menu-item>
</a-menu>
</div>
<div class="user-section">
<a-dropdown position="bottom">
<a-avatar :size="32" class="user-avatar">
<icon-user />
</a-avatar>
<template #content>
<a-doption v-for="option in userOptions" :key="option.key" @click="option.handler">
{{ option.label }}
</a-doption>
</template>
</a-dropdown>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import token from '@/storage/token'
import { useRouter } from 'vue-router';
defineProps({
activeKey: {
type: String,
default: 'HomePage'
}
});
const router = useRouter()
const emit = defineEmits(['menu-change', 'user-option-click']);
const menuItems = ref([
{ key: 'DashBoard', label: '工作台' },
{ key: 'ProjectSystem', label: '项目管理' },
{ key: 'DevelopSystem', label: '研发交付' },
{ key: '4', label: '制品库' },
{ key: '5', label: '测试中心' },
{ key: '6', label: '运维中心' }
]);
const userOptions = ref([
{ key: 'profile', label: '个人中心', handler: () => emit('user-option-click', 'profile') },
{ key: 'settings', label: '系统设置', handler: () => emit('user-option-click', 'settings') },
{
key: 'logout', label: '退出登录', handler: () => {
//
token.value = null
router.push({ name: 'LoginPage' })
}
}
]);
const handleMenuClick = (menuItem) => {
emit('menu-change', menuItem.key);
};
</script>
<style lang="less" scoped>
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
z-index: 100;
.header-content {
max-width: 100%;
height: 100%;
padding: 0 24px;
margin: 0 auto;
display: flex;
align-items: center;
.logo-section {
.platform-name {
color: var(--color-text-1);
font-size: 18px;
font-weight: 600;
margin: 0;
white-space: nowrap;
}
}
.main-nav-section {
flex: 1;
margin: 0 40px;
overflow-x: auto;
overflow-y: hidden;
:deep(.arco-menu-horizontal) {
height: 60px;
background: transparent;
border-bottom: none;
}
}
.user-avatar {
background-color: var(--color-primary-light-3);
color: var(--color-primary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--color-primary-light-2);
}
}
}
}
</style>

View File

@ -1,17 +0,0 @@
<template>
<div>
<a-space>
<a-button type="primary">Primary</a-button>
<a-button>Secondary</a-button>
<a-button type="dashed">Dashed</a-button>
<a-button type="outline">Outline</a-button>
<a-button type="text">Text</a-button>
</a-space>
</div>
</template>
<script setup>
</script>
<style lang="css" scoped></style>

View File

@ -93,7 +93,7 @@ const handleSubmit = async (data) => {
if (data.errors == null) { if (data.errors == null) {
const resp = await mcenter.Login(data.values); const resp = await mcenter.Login(data.values);
token.value = resp; token.value = resp;
router.push({ name: 'HomePage' }); router.push({ name: 'DashBoard' });
} }
}; };
</script> </script>

View File

@ -0,0 +1,337 @@
<template>
<div class="dashboard-container">
<!-- 顶部数据概览 -->
<section class="metrics-section">
<a-row :gutter="20">
<a-col :xs="24" :sm="12" :md="6" v-for="metric in metrics" :key="metric.title">
<a-card class="metric-card" hoverable>
<div class="metric-content">
<div class="metric-icon" :style="{ backgroundColor: metric.color }">
<component :is="metric.icon" size="20" />
</div>
<div class="metric-info">
<a-typography-text class="metric-title">{{ metric.title }}</a-typography-text>
<a-typography-title :heading="4" class="metric-value">{{ metric.value }}</a-typography-title>
<a-typography-text class="metric-trend" :type="metric.trend.type">
{{ metric.trend.value }}
<icon-caret-up v-if="metric.trend.direction === 'up'" />
<icon-caret-down v-else />
</a-typography-text>
</div>
</div>
</a-card>
</a-col>
</a-row>
</section>
<!-- 构建状态和活动流水线 -->
<section class="main-section">
<a-row :gutter="20">
<!-- 构建状态图表 -->
<a-col :xs="24" :md="16">
<a-card title="构建状态统计" :bordered="false" class="chart-card">
<template #extra>
<a-select v-model="buildTimeRange" size="small" style="width: 120px">
<a-option value="week">最近一周</a-option>
<a-option value="month">最近一月</a-option>
<a-option value="quarter">最近三月</a-option>
</a-select>
</template>
<BuildStatusChar :data="buildData" />
</a-card>
</a-col>
<!-- 活动流水线 -->
<a-col :xs="24" :md="8">
<a-card title="活动流水线" :bordered="false" class="pipeline-card">
<template #extra>
<a-link>查看全部</a-link>
</template>
<a-list :bordered="false" :split="false">
<a-list-item v-for="pipeline in activePipelines" :key="pipeline.id">
<a-list-item-meta>
<template #avatar>
<a-badge :status="pipeline.status" />
</template>
<template #title>
<a-link>{{ pipeline.name }}</a-link>
</template>
<template #description>
<a-space>
<a-typography-text type="secondary">#{{ pipeline.id }}</a-typography-text>
<a-typography-text type="secondary">{{ pipeline.duration }}</a-typography-text>
</a-space>
</template>
</a-list-item-meta>
<a-tag :color="pipeline.tagColor">{{ pipeline.stage }}</a-tag>
</a-list-item>
</a-list>
</a-card>
</a-col>
</a-row>
</section>
<!-- 部署统计和最近活动 -->
<section class="secondary-section">
<a-row :gutter="20">
<!-- 部署统计 -->
<a-col :xs="24" :md="12">
<a-card title="部署统计" :bordered="false" class="chart-card">
<DeployChart :data="deployData" />
</a-card>
</a-col>
<!-- 最近活动 -->
<a-col :xs="24" :md="12">
<a-card title="最近活动" :bordered="false" class="activity-card">
<a-timeline>
<a-timeline-item v-for="activity in recentActivities" :key="activity.id" :color="activity.color">
<a-space direction="vertical" size="2">
<div class="activity-content">
<a-typography-text strong>{{ activity.user }}</a-typography-text>
<a-typography-text type="secondary">{{ activity.action }}</a-typography-text>
<a-link>{{ activity.target }}</a-link>
</div>
<a-typography-text type="secondary" class="activity-time">{{ activity.time }}</a-typography-text>
</a-space>
</a-timeline-item>
</a-timeline>
</a-card>
</a-col>
</a-row>
</section>
</div>
</template>
<script setup>
import { ref } from 'vue';
import {
IconCheckCircle,
IconClockCircle,
IconCloseCircle,
IconCode,
IconCaretUp,
IconCaretDown
} from '@arco-design/web-vue/es/icon';
import DeployChart from './components/DeployChart.vue';
import BuildStatusChar from './components/BuildStatusChar.vue';
//
const buildTimeRange = ref('week');
const metrics = ref([
{
title: '成功构建',
value: '1,248',
icon: IconCheckCircle,
color: 'var(--color-success-light-1)',
trend: {
value: '12.5%',
direction: 'up',
type: 'success'
}
},
{
title: '失败构建',
value: '56',
icon: IconCloseCircle,
color: 'var(--color-danger-light-1)',
trend: {
value: '3.2%',
direction: 'down',
type: 'success'
}
},
{
title: '平均构建时间',
value: '2m 45s',
icon: IconClockCircle,
color: 'var(--color-warning-light-1)',
trend: {
value: '5.1%',
direction: 'down',
type: 'success'
}
},
{
title: '今日部署',
value: '42',
icon: IconCode,
color: 'var(--color-primary-light-1)',
trend: {
value: '8.7%',
direction: 'up',
type: 'success'
}
}
]);
const activePipelines = ref([
{
id: '2356',
name: 'frontend-web',
status: 'success',
duration: '2m 12s',
stage: '部署生产',
tagColor: 'green'
},
{
id: '2355',
name: 'backend-service',
status: 'processing',
duration: '1m 45s',
stage: '运行测试',
tagColor: 'orange'
},
{
id: '2354',
name: 'mobile-app',
status: 'error',
duration: '3m 28s',
stage: '构建失败',
tagColor: 'red'
},
{
id: '2353',
name: 'data-pipeline',
status: 'warning',
duration: '4m 15s',
stage: '等待审批',
tagColor: 'gold'
}
]);
const buildData = ref({/* 图表数据 */ });
const deployData = ref({/* 图表数据 */ });
const recentActivities = ref([
{
id: 1,
user: '张开发',
action: '触发了构建',
target: 'frontend-web #2356',
time: '10分钟前',
color: 'green'
},
{
id: 2,
user: '李测试',
action: '部署了版本',
target: 'backend-service v1.2.3',
time: '25分钟前',
color: 'blue'
},
{
id: 3,
user: '王运维',
action: '创建了新环境',
target: 'staging-environment',
time: '1小时前',
color: 'purple'
},
{
id: 4,
user: '系统',
action: '完成了扫描',
target: 'security-scan #142',
time: '2小时前',
color: 'gray'
}
]);
</script>
<style scoped>
.dashboard-container {
padding: 16px;
max-width: 1600px;
margin: 0 auto;
}
/* 指标卡片 */
.metrics-section {
margin-bottom: 20px;
}
.metric-card {
margin-bottom: 20px;
}
.metric-content {
display: flex;
align-items: center;
}
.metric-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
color: var(--color-white);
}
.metric-info {
flex: 1;
}
.metric-title {
display: block;
color: var(--color-text-2);
font-size: 12px;
}
.metric-value {
margin: 4px 0;
}
.metric-trend {
display: flex;
align-items: center;
font-size: 12px;
}
/* 主内容区 */
.main-section {
margin-bottom: 20px;
}
.chart-card {
margin-bottom: 20px;
height: 500px;
}
.pipeline-card {
height: 500px;
}
/* 活动时间线 */
.activity-card {
height: 500px;
}
.activity-content {
display: flex;
align-items: center;
gap: 8px;
}
.activity-time {
font-size: 12px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.dashboard-container {
padding: 8px;
}
.metric-card {
margin-bottom: 12px;
}
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<div style="height: 300px">构建状态图表</div>
</template>
<script setup>
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,9 @@
<template>
<div style="height: 300px">部署统计图表</div>
</template>
<script setup>
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,13 @@
<template>
<a-steps type="arrow" :current="2">
<a-step description="This is a description">Succeeded</a-step>
<a-step description="This is a description">Processing</a-step>
<a-step description="This is a description">Pending</a-step>
</a-steps>
</template>
<script setup>
</script>
<style lang="css" scoped></style>

View File

@ -0,0 +1,248 @@
<template>
<div class="product-page">
<!-- 核心价值部分 -->
<section class="section">
<a-divider class="driver" orientation="center">
<a-typography-title :heading="4">平台核心价值</a-typography-title>
</a-divider>
<a-row :gutter="24" justify="center" class="value-cards">
<a-col :xs="24" :sm="12" :md="8" v-for="(value, index) in values" :key="index">
<a-card class="value-card" hoverable>
<div class="card-icon">
<component :is="value.icon" size="48" />
</div>
<a-card-meta :title="value.title" :description="value.description" class="card-meta" />
</a-card>
</a-col>
</a-row>
</section>
<!-- 功能特性部分 -->
<section class="section">
<a-divider class="driver" orientation="center">
<a-typography-title :heading="4">功能特性</a-typography-title>
</a-divider>
<a-list :grid="{ gutter: 16, xs: 1, sm: 2, lg: 2, xl: 2 }" :data="features" class="feature-list">
<template #item="{ item }">
<a-list-item>
<a-card hoverable class="feature-card">
<a-list-item-meta>
<template #avatar>
<a-avatar :size="40" :style="{ backgroundColor: 'rgb(var(--primary-6))' }">
<icon-check />
</a-avatar>
</template>
<template #title>
<a-typography-text strong>{{ item.title }}</a-typography-text>
</template>
<template #description>
<a-typography-paragraph :ellipsis="{ rows: 2 }">{{ item.description }}</a-typography-paragraph>
</template>
</a-list-item-meta>
</a-card>
</a-list-item>
</template>
</a-list>
</section>
<!-- 客户案例部分 -->
<!-- <section class="section">
<a-divider orientation="center">
<a-typography-title :heading="4">客户案例</a-typography-title>
</a-divider>
<div class="case-container">
<a-carousel :auto-play="true" :interval="4000" animation-name="fade" show-arrow="hover" indicator-type="line"
class="case-carousel">
<a-carousel-item v-for="caseItem in cases" :key="caseItem.id">
<a-card :title="caseItem.company" class="case-card" hoverable>
<a-typography-paragraph>
{{ caseItem.content }}
</a-typography-paragraph>
<template #extra>
<a-link>查看详情</a-link>
</template>
</a-card>
</a-carousel-item>
</a-carousel>
</div>
</section> -->
<!-- 行动号召部分 -->
<section class="section cta-section">
<a-result status="success" title="准备好提升您的研发效能了吗?" sub-title="我们的研发交付平台将帮助您的团队实现高效稳定的软件交付">
<template #extra>
<a-space :size="24">
<a-button type="primary" size="large" shape="round">免费试用</a-button>
<a-button size="large" shape="round">预约演示</a-button>
</a-space>
</template>
</a-result>
</section>
</div>
</template>
<script setup>
import {
IconSync,
IconBranch,
IconUserGroup,
IconCheck
} from '@arco-design/web-vue/es/icon';
const values = [
{
icon: IconSync,
title: "持续集成",
description: "自动化构建、测试流程,快速反馈代码质量,提升开发效率"
},
{
icon: IconBranch,
title: "持续交付",
description: "一键部署,快速迭代,确保软件随时可发布"
},
{
icon: IconUserGroup,
title: "高效协作",
description: "跨团队无缝协作,可视化流程,透明化管理"
}
];
const features = [
{
title: '自动化构建与测试',
description: '支持多种语言和框架的自动化构建,集成单元测试、集成测试等质量门禁'
},
{
title: '多环境部署',
description: '支持开发、测试、预发、生产等多环境一键部署,配置灵活可定制'
},
{
title: '可视化流水线',
description: '直观展示构建、测试、部署全流程,实时监控各环节状态'
},
{
title: '质量门禁',
description: '代码规范检查、测试覆盖率要求、安全扫描等多维度质量管控'
}
];
// const cases = [
// {
// id: 1,
// company: "",
// content: "使300%60%",
// image: 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/cd7a1aaea8e1c5e3d26fe2591e561798.png~tplv-uwbnlip3yd-webp.webp',
// },
// {
// id: 2,
// company: "",
// content: "",
// },
// {
// id: 3,
// company: "",
// content: "线22"
// }
// ];
</script>
<style scoped>
.product-page {
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
}
.section {
margin-bottom: 48px;
}
.driver {
margin: 80px 0px;
}
/* 核心价值卡片 */
.value-cards {
margin: 32px 0;
}
.value-card {
height: 200px;
text-align: center;
transition: transform 0.2s;
}
.value-card:hover {
transform: translateY(-5px);
}
.card-icon {
display: flex;
justify-content: center;
padding: 24px 0 16px;
color: rgb(var(--primary-6));
}
.card-meta {
padding: 0 16px 24px;
}
/* 功能特性列表 */
.feature-list {
margin: 32px 0;
}
.feature-card {
height: 100%;
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-3px);
}
/* 客户案例 */
.case-container {
padding: 0 16px;
}
.case-carousel {
margin: 32px auto;
max-width: 800px;
}
.case-card {
padding: 24px;
margin: 0 8px;
}
/* 行动号召部分 */
.cta-section {
background: var(--color-fill-2);
padding: 48px 24px;
border-radius: 8px;
margin-top: 64px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.section {
margin-bottom: 32px;
}
.value-card,
.feature-card {
margin-bottom: 16px;
}
.cta-section {
margin-top: 48px;
padding: 32px 16px;
}
}
</style>

View File

@ -1,11 +0,0 @@
<template>
<div>
<!-- 登录表单 -->
</div>
</template>
<script setup>
</script>
<style lang="css" scoped></style>

View File

@ -1,35 +1,74 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import BackendLayout from '@/layout/BackendLayout.vue' import MenuLayout from '@/layout/MenuLayout.vue'
import DashboardLayout from '@/layout/DashboardLayout.vue'
import FrontendLayout from '@/layout/FrontendLayout.vue'
import { beforeEach } from './interceptor'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'HomePage', name: 'Frontend',
component: BackendLayout, redirect: { name: 'ProductPage' },
component: () => FrontendLayout,
children: [
{
path: 'product',
name: 'ProductPage',
component: () => import('@/pages/frontend/ProductPage.vue'),
},
],
}, },
{ {
path: '/login', path: '/login',
name: 'LoginPage', name: 'LoginPage',
// route level code-splitting component: () => import('@/pages/LoginPage.vue'),
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/LoginPage.vue'),
}, },
{ {
path: '/cmdb', path: '/person',
component: () => import('@/layout/BackendLayout.vue'), name: 'DashBoard',
redirect: { name: 'DashboardPage' },
component: DashboardLayout,
children: [ children: [
{ {
path: 'secret', path: 'dashboard',
name: 'SecretPage', name: 'DashboardPage',
component: () => import('@/pages/cmdb/SecretPage.vue'), component: () => import('@/pages/dashboard/DashboardPage.vue'),
},
],
},
{
path: '/project',
name: 'ProjectSystem',
redirect: { name: 'AppPage' },
component: MenuLayout,
children: [
{
path: 'app',
name: 'AppPage',
component: () => import('@/pages/project/AppPage.vue'),
},
],
},
{
path: '/develop',
name: 'DevelopSystem',
redirect: { name: 'SprintPage' },
component: MenuLayout,
children: [
{
path: 'sprint',
name: 'SprintPage',
component: () => import('@/pages/develop/SprintPage.vue'),
}, },
], ],
}, },
], ],
}) })
// 导航守卫
router.beforeEach(beforeEach)
export default router export default router

View File

@ -0,0 +1,20 @@
import token from '@/storage/token'
var witheList = ['ProductPage', 'LoginPage']
export var beforeEach = async (to) => {
// 白名单
for (var name of witheList) {
if (name === to.name) {
return true
}
}
// 检查用户是否已登录
if (token.value.access_token) {
return true
}
// 将用户重定向到登录页面
return { name: 'LoginPage' }
}