应该管理
This commit is contained in:
parent
68a9b29458
commit
8df992dda5
@ -6,7 +6,7 @@
|
||||
<div class="decoration-wave"></div>
|
||||
|
||||
<!-- 使用顶部导航组件 -->
|
||||
<HeaderNav :active-key="activeMenuKey" @menu-change="handleMenuChange" @user-option-click="handleUserOption" />
|
||||
<HeaderNav @system-change="handleSystemChange" @user-option-click="handleUserOption" />
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content-wrapper">
|
||||
@ -31,20 +31,18 @@
|
||||
</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 })
|
||||
const handleSystemChange = (system) => {
|
||||
// 这里可以添加路由跳转逻辑
|
||||
console.log('菜单切换:', key);
|
||||
console.log('菜单切换:', system);
|
||||
|
||||
router.push({ name: system })
|
||||
};
|
||||
|
||||
const handleUserOption = (option) => {
|
||||
|
@ -6,14 +6,15 @@
|
||||
<div class="decoration-wave"></div>
|
||||
|
||||
<!-- 使用新的顶部导航组件 -->
|
||||
<HeaderNav :active-key="activeMenuKey" @menu-change="handleMenuChange" @user-option-click="handleUserOption" />
|
||||
<HeaderNav @system-change="handleSystemChange" @user-option-click="handleUserOption" />
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content-wrapper">
|
||||
<!-- 可收缩侧边栏 -->
|
||||
<div class="fixed-sidebar" :class="{ 'collapsed': isSidebarCollapsed }">
|
||||
<!-- 使用原生a-menu确保折叠效果 -->
|
||||
<a-menu theme="light" :default-selected-keys="['1']" :collapsed="isSidebarCollapsed" :collapsed-width="64"
|
||||
<a-menu theme="light" :selected-keys="[currentSelectMenuItem]" :collapsed="isSidebarCollapsed"
|
||||
:collapsed-width="64" @sub-menu-click="handleMenuClick" @menu-item-click="handleMenuItemClick" breakpoint="xl"
|
||||
:style="{ width: '100%', height: '100%' }">
|
||||
<a-menu-item v-for="menu in currentMenus" :key="menu.key">
|
||||
<template #icon>
|
||||
@ -40,9 +41,7 @@
|
||||
<!-- 面包屑(会随内容滚动) -->
|
||||
<div class="breadcrumb">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item>首页</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>流水线系统</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>流水线列表</a-breadcrumb-item>
|
||||
<a-breadcrumb-item v-for="m in $route.matched" :key="m.name">{{ m.meta.title }}</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
@ -66,26 +65,58 @@
|
||||
<script setup>
|
||||
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 { useRoute, useRouter } from 'vue-router';
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { IconApps, IconBranch, IconLock, IconSettings, IconTags } from '@arco-design/web-vue/es/icon';
|
||||
import token from '@/storage/token'
|
||||
import app from '@/storage/app'
|
||||
import { watch } from 'vue';
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isSidebarCollapsed = ref(false);
|
||||
const activeMenuKey = ref('ProjectSystem');
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isSidebarCollapsed.value = !isSidebarCollapsed.value;
|
||||
};
|
||||
|
||||
const handleMenuChange = (key) => {
|
||||
activeMenuKey.value = key;
|
||||
router.push({ name: key })
|
||||
// 侧边栏响应式
|
||||
const { width } = useWindowSize()
|
||||
watch(width, (newWidth) => {
|
||||
isSidebarCollapsed.value = newWidth < 1200; // 小于1200px时自动折叠
|
||||
}, { immediate: true });
|
||||
|
||||
const currentSelectMenuItem = computed(() => {
|
||||
return app.value.current_menu[app.value.current_system]?.menu_item || app.value.current_system
|
||||
})
|
||||
|
||||
const handleSystemChange = (system) => {
|
||||
// 这里可以添加路由跳转逻辑
|
||||
console.log('菜单切换:', key);
|
||||
console.log('菜单切换:', system);
|
||||
|
||||
// 跳转到当前选中的页面
|
||||
try {
|
||||
router.push({ name: currentSelectMenuItem.value })
|
||||
} catch (error) {
|
||||
console.log("swich system error, %s", error)
|
||||
router.push({ name: app.value.current_system })
|
||||
}
|
||||
};
|
||||
|
||||
// 菜单被选中
|
||||
const handleMenuClick = (key) => {
|
||||
app.value.current_menu
|
||||
console.log(key)
|
||||
}
|
||||
|
||||
// 菜单项被选中
|
||||
const handleMenuItemClick = (key) => {
|
||||
app.value.current_menu[app.value.current_system].menu_item = key
|
||||
router.push({ name: key })
|
||||
console.log(route)
|
||||
}
|
||||
|
||||
const handleUserOption = (option) => {
|
||||
console.log('用户操作:', option);
|
||||
// 根据不同的option执行不同的操作
|
||||
@ -103,47 +134,52 @@ const handleUserOption = (option) => {
|
||||
};
|
||||
|
||||
const currentMenus = computed(() => {
|
||||
return systemMenus[activeMenuKey.value]
|
||||
return systemMenus[app.value.current_system]
|
||||
})
|
||||
|
||||
const systemMenus = shallowReactive({
|
||||
ProjectSystem: [
|
||||
{
|
||||
key: '1',
|
||||
icon: IconApps,
|
||||
title: '应用管理'
|
||||
key: 'ProjectList',
|
||||
icon: IconLock,
|
||||
title: '项目空间'
|
||||
},
|
||||
],
|
||||
DevelopSystem: [
|
||||
{
|
||||
key: '1',
|
||||
key: 'AppPage',
|
||||
icon: IconApps,
|
||||
title: '应用管理'
|
||||
},
|
||||
{
|
||||
key: 'VersionIteration',
|
||||
icon: IconTags,
|
||||
title: '版本迭代'
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: IconBranch,
|
||||
title: '分支管理'
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: IconHistory,
|
||||
title: '执行历史'
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
key: 'PipelineTemplate',
|
||||
icon: IconSettings,
|
||||
title: '流水线模板'
|
||||
}
|
||||
],
|
||||
ArtifactSystem: [
|
||||
{
|
||||
key: 'RegistryPage',
|
||||
icon: IconTags,
|
||||
title: '制品仓库'
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
title: '监控中心'
|
||||
}
|
||||
key: 'AssetPage',
|
||||
icon: IconBranch,
|
||||
title: '制品管理'
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@ -195,7 +231,7 @@ const systemMenus = shallowReactive({
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
z-index: 100;
|
||||
|
||||
.header-content {
|
||||
@ -258,6 +294,7 @@ const systemMenus = shallowReactive({
|
||||
bottom: 0;
|
||||
width: 220px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-right: 1px solid var(--color-border);
|
||||
z-index: 90;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<h1 class="platform-name">研发交付平台</h1>
|
||||
</div>
|
||||
<div class="main-nav-section">
|
||||
<a-menu mode="horizontal" :default-selected-keys="[activeKey]">
|
||||
<a-menu mode="horizontal" :default-selected-keys="[app.current_system]">
|
||||
<a-menu-item v-for="item in menuItems" :key="item.key" @click="handleMenuClick(item)">
|
||||
{{ item.label }}
|
||||
</a-menu-item>
|
||||
@ -30,23 +30,18 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import token from '@/storage/token'
|
||||
import app from '@/storage/app'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
defineProps({
|
||||
activeKey: {
|
||||
type: String,
|
||||
default: 'HomePage'
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter()
|
||||
const emit = defineEmits(['menu-change', 'user-option-click']);
|
||||
const emit = defineEmits(['system-change', 'user-option-click']);
|
||||
|
||||
const menuItems = ref([
|
||||
{ key: 'DashBoard', label: '工作台' },
|
||||
{ key: 'ProjectSystem', label: '项目管理' },
|
||||
{ key: 'DevelopSystem', label: '研发交付' },
|
||||
{ key: '4', label: '制品库' },
|
||||
{ key: 'ArtifactSystem', label: '制品库' },
|
||||
{ key: '5', label: '测试中心' },
|
||||
{ key: '6', label: '运维中心' }
|
||||
]);
|
||||
@ -64,7 +59,8 @@ const userOptions = ref([
|
||||
]);
|
||||
|
||||
const handleMenuClick = (menuItem) => {
|
||||
emit('menu-change', menuItem.key);
|
||||
app.value.current_system = menuItem.key
|
||||
emit('system-change', menuItem.key);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -76,7 +72,7 @@ const handleMenuClick = (menuItem) => {
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
z-index: 100;
|
||||
|
||||
.header-content {
|
||||
|
76
devcloud/web/src/pages/artifact/AssetPage.vue
Normal file
76
devcloud/web/src/pages/artifact/AssetPage.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-typography-text>showLine</a-typography-text>
|
||||
<a-switch v-model="showLine" style="margin-left: 12px" />
|
||||
</div>
|
||||
<a-tree :default-selected-keys="['0-0-1']" :data="treeData" :show-line="showLine" />
|
||||
</template>
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const showLine = ref(true);
|
||||
|
||||
return {
|
||||
showLine,
|
||||
treeData,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const treeData = [
|
||||
{
|
||||
title: 'Trunk 1',
|
||||
key: '0-0',
|
||||
children: [
|
||||
{
|
||||
title: 'Trunk 1-0',
|
||||
key: '0-0-0',
|
||||
children: [
|
||||
{ title: 'leaf', key: '0-0-0-0' },
|
||||
{
|
||||
title: 'leaf',
|
||||
key: '0-0-0-1',
|
||||
children: [{ title: 'leaf', key: '0-0-0-1-0' }],
|
||||
},
|
||||
{ title: 'leaf', key: '0-0-0-2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Trunk 1-1',
|
||||
key: '0-0-1',
|
||||
},
|
||||
{
|
||||
title: 'Trunk 1-2',
|
||||
key: '0-0-2',
|
||||
children: [
|
||||
{ title: 'leaf', key: '0-0-2-0' },
|
||||
{
|
||||
title: 'leaf',
|
||||
key: '0-0-2-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Trunk 2',
|
||||
key: '0-1',
|
||||
},
|
||||
{
|
||||
title: 'Trunk 3',
|
||||
key: '0-2',
|
||||
children: [
|
||||
{
|
||||
title: 'Trunk 3-0',
|
||||
key: '0-2-0',
|
||||
children: [
|
||||
{ title: 'leaf', key: '0-2-0-0' },
|
||||
{ title: 'leaf', key: '0-2-0-1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
116
devcloud/web/src/pages/artifact/RegistryPage.vue
Normal file
116
devcloud/web/src/pages/artifact/RegistryPage.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<a-space direction="vertical" size="large" fill>
|
||||
<div>
|
||||
<span>OnlyCurrent: </span>
|
||||
<a-switch v-model="rowSelection.onlyCurrent" />
|
||||
</div>
|
||||
<a-table row-key="name" :columns="columns" :data="data" :row-selection="rowSelection"
|
||||
v-model:selectedKeys="selectedKeys" :pagination="pagination" />
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const selectedKeys = ref(['Jane Doe', 'Alisa Ross']);
|
||||
|
||||
const rowSelection = reactive({
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true,
|
||||
onlyCurrent: false,
|
||||
});
|
||||
const pagination = { pageSize: 5 }
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Salary',
|
||||
dataIndex: 'salary',
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
dataIndex: 'address',
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'email',
|
||||
},
|
||||
]
|
||||
const data = reactive([{
|
||||
key: '1',
|
||||
name: 'Jane Doe',
|
||||
salary: 23000,
|
||||
address: '32 Park Road, London',
|
||||
email: 'jane.doe@example.com'
|
||||
}, {
|
||||
key: '2',
|
||||
name: 'Alisa Ross',
|
||||
salary: 25000,
|
||||
address: '35 Park Road, London',
|
||||
email: 'alisa.ross@example.com'
|
||||
}, {
|
||||
key: '3',
|
||||
name: 'Kevin Sandra',
|
||||
salary: 22000,
|
||||
address: '31 Park Road, London',
|
||||
email: 'kevin.sandra@example.com',
|
||||
disabled: true
|
||||
}, {
|
||||
key: '4',
|
||||
name: 'Ed Hellen',
|
||||
salary: 17000,
|
||||
address: '42 Park Road, London',
|
||||
email: 'ed.hellen@example.com'
|
||||
}, {
|
||||
key: '5',
|
||||
name: 'William Smith',
|
||||
salary: 27000,
|
||||
address: '62 Park Road, London',
|
||||
email: 'william.smith@example.com'
|
||||
}, {
|
||||
key: '6',
|
||||
name: 'Jane Doe 2',
|
||||
salary: 15000,
|
||||
address: '32 Park Road, London',
|
||||
email: 'jane.doe@example.com'
|
||||
}, {
|
||||
key: '7',
|
||||
name: 'Alisa Ross 2',
|
||||
salary: 28000,
|
||||
address: '35 Park Road, London',
|
||||
email: 'alisa.ross@example.com'
|
||||
}, {
|
||||
key: '8',
|
||||
name: 'Kevin Sandra 2',
|
||||
salary: 26000,
|
||||
address: '31 Park Road, London',
|
||||
email: 'kevin.sandra@example.com',
|
||||
}, {
|
||||
key: '9',
|
||||
name: 'Ed Hellen 2',
|
||||
salary: 18000,
|
||||
address: '42 Park Road, London',
|
||||
email: 'ed.hellen@example.com'
|
||||
}, {
|
||||
key: '10',
|
||||
name: 'William Smith 2',
|
||||
salary: 12000,
|
||||
address: '62 Park Road, London',
|
||||
email: 'william.smith@example.com'
|
||||
}]);
|
||||
|
||||
return {
|
||||
rowSelection,
|
||||
columns,
|
||||
data,
|
||||
selectedKeys,
|
||||
pagination
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -93,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import {
|
||||
IconCheckCircle,
|
||||
IconClockCircle,
|
||||
@ -106,7 +106,7 @@ import {
|
||||
import DeployChart from './components/DeployChart.vue';
|
||||
import BuildStatusChar from './components/BuildStatusChar.vue';
|
||||
|
||||
const metrics = ref([
|
||||
const metrics = shallowRef([
|
||||
{
|
||||
title: '成功构建',
|
||||
value: '1,248',
|
||||
|
13
devcloud/web/src/pages/develop/VersionIteration.vue
Normal file
13
devcloud/web/src/pages/develop/VersionIteration.vue
Normal 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>
|
193
devcloud/web/src/pages/project/ProjectList.vue
Normal file
193
devcloud/web/src/pages/project/ProjectList.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="project-space-container">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="action-bar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showCreateModal = true">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建项目空间
|
||||
</a-button>
|
||||
|
||||
<a-input-search v-model="searchKey" placeholder="搜索项目名称/描述" @search="handleSearch" allow-clear
|
||||
style="width: 300px" />
|
||||
</a-space>
|
||||
|
||||
<a-space :size="18">
|
||||
<a-select v-model="filterParams.status" placeholder="项目状态" style="width: 120px" allow-clear
|
||||
@change="handleSearch">
|
||||
<a-option value="active">活跃</a-option>
|
||||
<a-option value="archived">已归档</a-option>
|
||||
<a-option value="planning">规划中</a-option>
|
||||
</a-select>
|
||||
|
||||
<a-button type="text" @click="resetFilters">
|
||||
<template #icon><icon-refresh /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 项目卡片列表 -->
|
||||
<div class="project-list">
|
||||
<a-row :gutter="16">
|
||||
<a-col v-for="project in projectList" :key="project.id" :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<ProjectCard :project="project" @edit="handleEdit" @delete="handleDelete" @archive="handleArchive" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && projectList.length === 0" description="暂无项目数据" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<a-pagination v-model="pagination.current" :total="pagination.total" :page-size="pagination.pageSize" show-total
|
||||
show-jumper @change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<ProjectFormModal v-model:visible="showCreateModal" :project-data="currentProject" @submit="handleSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import ProjectCard from './components/ProjectCard.vue';
|
||||
import ProjectFormModal from './components/ProjectFormModal.vue';
|
||||
import { mockProjects } from './mockData';
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false);
|
||||
const projectList = ref([]);
|
||||
const searchKey = ref('');
|
||||
const filterParams = reactive({
|
||||
status: '',
|
||||
});
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 表单相关
|
||||
const showCreateModal = ref(false);
|
||||
const currentProject = ref(null);
|
||||
|
||||
// 初始化加载数据
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
let filtered = [...mockProjects];
|
||||
|
||||
if (searchKey.value) {
|
||||
const key = searchKey.value.toLowerCase();
|
||||
filtered = filtered.filter(p =>
|
||||
p.name.toLowerCase().includes(key) ||
|
||||
p.description.toLowerCase().includes(key)
|
||||
);
|
||||
}
|
||||
|
||||
if (filterParams.status) {
|
||||
filtered = filtered.filter(p => p.status === filterParams.status);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const start = (pagination.current - 1) * pagination.pageSize;
|
||||
const end = start + pagination.pageSize;
|
||||
projectList.value = filtered.slice(start, end);
|
||||
pagination.total = filtered.length;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 事件处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
searchKey.value = '';
|
||||
filterParams.status = '';
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
pagination.current = page;
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const handleEdit = (project) => {
|
||||
currentProject.value = project;
|
||||
showCreateModal.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
Message.success(`已删除项目 ${id}`);
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const handleArchive = (id) => {
|
||||
Message.success(`已归档项目 ${id}`);
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
Message.success(currentProject.value ? '项目更新成功' : '项目创建成功');
|
||||
showCreateModal.value = false;
|
||||
loadProjects();
|
||||
currentProject.value = null;
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadProjects();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-space-container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.project-list {
|
||||
flex: 1;
|
||||
margin-bottom: 16px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.action-bar>* {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
131
devcloud/web/src/pages/project/components/ProjectCard.vue
Normal file
131
devcloud/web/src/pages/project/components/ProjectCard.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<a-card class="project-card" :style="{ borderTop: `4px solid ${statusColor}` }" hoverable>
|
||||
<a-card-meta :title="project.name">
|
||||
<template #description>
|
||||
<div class="project-meta">
|
||||
<div class="project-desc">{{ project.description }}</div>
|
||||
|
||||
<div class="project-footer">
|
||||
<a-space>
|
||||
<a-avatar-group :max-count="3">
|
||||
<a-avatar v-for="member in project.members" :key="member.id" :size="24">
|
||||
{{ member.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
</a-avatar-group>
|
||||
<span>创建于 {{ formatDate(project.createTime) }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
|
||||
<template #actions>
|
||||
<a-tooltip content="编辑">
|
||||
<a-button type="text" @click.stop="$emit('edit', project)">
|
||||
<icon-edit />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip content="删除">
|
||||
<a-popconfirm content="确认删除该项目?" @ok="$emit('delete', project.id)">
|
||||
<a-button type="text">
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
|
||||
<a-dropdown @click.stop>
|
||||
<a-button type="text">
|
||||
<icon-more />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="$emit('archive', project.id)">归档</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
IconEdit,
|
||||
IconDelete,
|
||||
IconMore
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
members: [],
|
||||
createTime: Date.now()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['edit', 'delete', 'archive']);
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const map = {
|
||||
active: '#00B42A',
|
||||
archived: '#86909C',
|
||||
planning: '#FF7D00'
|
||||
};
|
||||
return map[props.project.status] || '#86909C';
|
||||
});
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.project-footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.arco-card-actions) {
|
||||
border-top: none;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-card-meta-description) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
100
devcloud/web/src/pages/project/components/ProjectFormModal.vue
Normal file
100
devcloud/web/src/pages/project/components/ProjectFormModal.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="modelVisible" :title="editMode ? '编辑项目' : '新建项目'" @ok="handleOk" @cancel="handleCancel"
|
||||
:ok-text="editMode ? '保存' : '创建'" unmount-on-close>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="项目名称" field="name" :rules="[{ required: true, message: '请输入项目名称' }]"
|
||||
:validate-trigger="['change', 'blur']">
|
||||
<a-input v-model="form.name" placeholder="请输入项目名称" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目描述" field="description">
|
||||
<a-textarea v-model="form.description" placeholder="请输入项目描述" :auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目状态" field="status" :rules="[{ required: true, message: '请选择项目状态' }]">
|
||||
<a-select v-model="form.status" placeholder="请选择状态" allow-clear>
|
||||
<a-option value="active">活跃</a-option>
|
||||
<a-option value="archived">已归档</a-option>
|
||||
<a-option value="planning">规划中</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
projectData: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'submit']);
|
||||
|
||||
// 1. 先定义所有函数和变量
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
};
|
||||
};
|
||||
|
||||
// 2. 再定义其他响应式数据
|
||||
const modelVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
const editMode = computed(() => !!props.projectData);
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 3. 最后定义watch(此时resetForm已经定义)
|
||||
watch(() => props.projectData, (newVal) => {
|
||||
if (newVal) {
|
||||
form.value = { ...newVal };
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const handleOk = () => {
|
||||
if (!form.value.name.trim()) {
|
||||
Message.error('项目名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
...form.value,
|
||||
name: form.value.name.trim()
|
||||
});
|
||||
modelVisible.value = false;
|
||||
resetForm(); // 提交后重置表单
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
modelVisible.value = false;
|
||||
resetForm(); // 取消时也重置表单
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加自定义样式 */
|
||||
:deep(.arco-form-item-label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
193
devcloud/web/src/pages/project/mockData.js
Normal file
193
devcloud/web/src/pages/project/mockData.js
Normal file
@ -0,0 +1,193 @@
|
||||
// mockData.js
|
||||
|
||||
/**
|
||||
* 生成随机日期(最近365天内)
|
||||
*/
|
||||
const generateRandomDate = () => {
|
||||
const now = new Date()
|
||||
const past = new Date(now)
|
||||
past.setDate(now.getDate() - 365)
|
||||
return new Date(past.getTime() + Math.random() * (now.getTime() - past.getTime()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机项目描述
|
||||
*/
|
||||
const generateDescription = (index) => {
|
||||
const descriptors = [
|
||||
'企业级应用开发项目',
|
||||
'微服务架构重构项目',
|
||||
'前端技术升级专项',
|
||||
'DevOps平台建设项目',
|
||||
'AI能力集成项目',
|
||||
'大数据分析平台',
|
||||
'移动端应用重设计',
|
||||
'云原生迁移项目',
|
||||
]
|
||||
const types = ['核心业务系统', '技术中台', '数据平台', '用户增长项目', '基础设施升级']
|
||||
return `${types[index % types.length]} - ${descriptors[index % descriptors.length]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟用户数据
|
||||
*/
|
||||
export const mockUsers = [
|
||||
{ id: 'user-1', name: '张管理员', role: 'admin' },
|
||||
{ id: 'user-2', name: '李开发', role: 'dev' },
|
||||
{ id: 'user-3', name: '王测试', role: 'qa' },
|
||||
{ id: 'user-4', name: '赵产品', role: 'product' },
|
||||
{ id: 'user-5', name: '刘架构', role: 'arch' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 模拟项目数据
|
||||
*/
|
||||
export const mockProjects = Array.from({ length: 36 }, (_, i) => {
|
||||
const statuses = ['active', 'archived', 'planning']
|
||||
const status = statuses[i % 3]
|
||||
|
||||
return {
|
||||
id: `project-${i + 1}`,
|
||||
name: `研发项目 ${String(i + 1).padStart(2, '0')}`,
|
||||
description: generateDescription(i),
|
||||
status,
|
||||
owner: mockUsers[i % mockUsers.length].id,
|
||||
members: mockUsers.slice(0, (i % 4) + 1),
|
||||
createTime: generateRandomDate().getTime(),
|
||||
updateTime: generateRandomDate().getTime(),
|
||||
stats: {
|
||||
tasks: Math.floor(Math.random() * 50) + 10,
|
||||
completed: Math.floor(Math.random() * 45),
|
||||
progress: Math.floor(Math.random() * 100),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 项目状态映射
|
||||
*/
|
||||
export const projectStatusMap = {
|
||||
active: { text: '活跃', color: '#00B42A' },
|
||||
archived: { text: '已归档', color: '#86909C' },
|
||||
planning: { text: '规划中', color: '#FF7D00' },
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目状态信息
|
||||
*/
|
||||
export const getProjectStatusInfo = (status) => {
|
||||
return projectStatusMap[status] || { text: '未知', color: '#86909C' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟API响应格式
|
||||
*/
|
||||
export const mockApiResponse = (data, options = {}) => {
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
message: 'success',
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟获取项目列表API
|
||||
*/
|
||||
export const fetchProjects = (params = {}) => {
|
||||
const { page = 1, pageSize = 10, search = '', status } = params
|
||||
|
||||
let filtered = [...mockProjects]
|
||||
|
||||
// 模拟筛选
|
||||
if (search) {
|
||||
const keyword = search.toLowerCase()
|
||||
filtered = filtered.filter(
|
||||
(p) =>
|
||||
p.name.toLowerCase().includes(keyword) || p.description.toLowerCase().includes(keyword),
|
||||
)
|
||||
}
|
||||
|
||||
if (status) {
|
||||
filtered = filtered.filter((p) => p.status === status)
|
||||
}
|
||||
|
||||
// 模拟分页
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const pageData = filtered.slice(start, end)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(
|
||||
mockApiResponse({
|
||||
list: pageData,
|
||||
total: filtered.length,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
)
|
||||
}, 500) // 模拟网络延迟
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟获取项目详情API
|
||||
*/
|
||||
export const fetchProjectDetail = (id) => {
|
||||
const project = mockProjects.find((p) => p.id === id)
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (project) {
|
||||
resolve(mockApiResponse(project))
|
||||
} else {
|
||||
resolve(mockApiResponse(null, { code: 404, message: '项目不存在' }))
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟创建项目API
|
||||
*/
|
||||
export const createProject = (data) => {
|
||||
const newProject = {
|
||||
id: `project-${mockProjects.length + 1}`,
|
||||
...data,
|
||||
createTime: Date.now(),
|
||||
updateTime: Date.now(),
|
||||
members: [mockUsers[0]],
|
||||
stats: {
|
||||
tasks: 0,
|
||||
completed: 0,
|
||||
progress: 0,
|
||||
},
|
||||
}
|
||||
|
||||
mockProjects.unshift(newProject)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockApiResponse(newProject))
|
||||
}, 400)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟更新项目API
|
||||
*/
|
||||
export const updateProject = (id, data) => {
|
||||
const index = mockProjects.findIndex((p) => p.id === id)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (index >= 0) {
|
||||
const updated = { ...mockProjects[index], ...data, updateTime: Date.now() }
|
||||
mockProjects.splice(index, 1, updated)
|
||||
resolve(mockApiResponse(updated))
|
||||
} else {
|
||||
resolve(mockApiResponse(null, { code: 404, message: '项目不存在' }))
|
||||
}
|
||||
}, 400)
|
||||
})
|
||||
}
|
@ -39,29 +39,87 @@ const router = createRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
// 项目管理
|
||||
{
|
||||
path: '/project',
|
||||
name: 'ProjectSystem',
|
||||
redirect: { name: 'AppPage' },
|
||||
redirect: { name: 'ProjectList' },
|
||||
component: MenuLayout,
|
||||
meta: {
|
||||
title: '项目管理',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'app',
|
||||
name: 'AppPage',
|
||||
component: () => import('@/pages/project/AppPage.vue'),
|
||||
path: 'list',
|
||||
name: 'ProjectList',
|
||||
component: () => import('@/pages/project/ProjectList.vue'),
|
||||
meta: {
|
||||
title: '项目空间',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 研发交付
|
||||
{
|
||||
path: '/develop',
|
||||
name: 'DevelopSystem',
|
||||
redirect: { name: 'SprintPage' },
|
||||
component: MenuLayout,
|
||||
meta: {
|
||||
title: '研发交付',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'sprint',
|
||||
name: 'SprintPage',
|
||||
component: () => import('@/pages/develop/SprintPage.vue'),
|
||||
path: 'app',
|
||||
name: 'AppPage',
|
||||
component: () => import('@/pages/develop/AppPage.vue'),
|
||||
meta: {
|
||||
title: '应用管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'version_iteration',
|
||||
name: 'VersionIteration',
|
||||
component: () => import('@/pages/develop/VersionIteration.vue'),
|
||||
meta: {
|
||||
title: '版本迭代',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pipeline_template',
|
||||
name: 'PipelineTemplate',
|
||||
component: () => import('@/pages/develop/PipelineTemplate.vue'),
|
||||
meta: {
|
||||
title: '流水线模板',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 制品库
|
||||
{
|
||||
path: '/artifact',
|
||||
name: 'ArtifactSystem',
|
||||
redirect: { name: 'SprintPage' },
|
||||
component: MenuLayout,
|
||||
meta: {
|
||||
title: '制品库',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'registry',
|
||||
name: 'RegistryPage',
|
||||
component: () => import('@/pages/artifact/RegistryPage.vue'),
|
||||
meta: {
|
||||
title: '制品仓库',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'asset',
|
||||
name: 'AssetPage',
|
||||
component: () => import('@/pages/artifact/AssetPage.vue'),
|
||||
meta: {
|
||||
title: '制品管理',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
export default useStorage(
|
||||
'app',
|
||||
{
|
||||
current_system: 'DashBoard',
|
||||
current_menu: {
|
||||
DashBoard: {
|
||||
sub_menu: '',
|
||||
menu_item: '1',
|
||||
},
|
||||
ProjectSystem: {
|
||||
sub_menu: '',
|
||||
menu_item: '1',
|
||||
},
|
||||
DevelopSystem: {
|
||||
sub_menu: '',
|
||||
menu_item: '1',
|
||||
},
|
||||
ArtifactSystem: {
|
||||
sub_menu: '',
|
||||
menu_item: 'RegistryPage',
|
||||
},
|
||||
},
|
||||
},
|
||||
localStorage,
|
||||
{ mergeDefaults: true },
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user