应该管理

This commit is contained in:
yumaojun03 2025-08-17 09:49:26 +08:00
parent 68a9b29458
commit 8df992dda5
15 changed files with 998 additions and 59 deletions

View File

@ -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) => {

View File

@ -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;

View File

@ -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 {

View 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>

View 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>

View File

@ -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',

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,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>

View 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>

View 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. watchresetForm
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>

View 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)
})
}

View File

@ -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: '制品管理',
},
},
],
},

View File

@ -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 },
)