前端开源初始化提交

This commit is contained in:
hukdoesn
2025-06-30 11:57:32 +08:00
parent 00090a0aca
commit fa52e9e571
50 changed files with 12349 additions and 12 deletions

View File

@@ -0,0 +1,30 @@
<template>
<a-layout-content>
<div class="content-container">
<router-view></router-view>
</div>
</a-layout-content>
</template>
<script setup>
</script>
<style scoped>
.content-container {
padding: 24px;
background: #fff;
min-height: 360px;
border-radius: 4px;
overflow-y: auto;
/* 设置最大高度,内容超出时可以滚动 */
max-height: calc(100vh - 112px);
}
:deep(.ant-layout-content) {
margin: 24px;
background: transparent;
position: relative;
flex: 1;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,14 @@
<template>
<a-layout-footer style="text-align: center; background-color: #fff;">
LiteOps ©2024 Created by 胡图图
</a-layout-footer>
</template>
<style scoped>
:deep(.ant-layout-footer) {
padding: 24px 50px;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
/* background: #f0f2f5; */
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<a-layout-header style="background: #fff; padding: 0; display: flex; justify-content: space-between; align-items: center;">
<div class="header-content">
<div class="header-left">
<a-breadcrumb>
<template v-if="breadcrumbItems.length">
<a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path || item.title">
<router-link v-if="item.clickable" :to="item.path">{{ item.title }}</router-link>
<span v-else>{{ item.title }}</span>
</a-breadcrumb-item>
</template>
<a-breadcrumb-item v-else>
<router-link to="/dashboard">首页</router-link>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div class="header-right">
<a-dropdown>
<a class="ant-dropdown-link" @click.prevent>
<UserOutlined /> {{ userName }}
</a>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="handleProfile">个人信息</a-menu-item>
<a-menu-item key="logout" @click="handleLogout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</a-layout-header>
<!-- 个人信息弹窗 -->
<a-modal
v-model:open="profileModalVisible"
title="个人信息"
width="500px"
:footer="null"
:maskClosable="true"
>
<a-spin :spinning="loading">
<div class="user-profile-info">
<div class="info-item">
<span class="info-label">用户名</span>
<span class="info-value">{{ userInfo.username }}</span>
</div>
<div class="info-item">
<span class="info-label">姓名</span>
<span class="info-value">{{ userInfo.name }}</span>
</div>
<div class="info-item">
<span class="info-label">邮箱</span>
<span class="info-value">{{ userInfo.email }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">
{{ userInfo.status === 1 ? '正常' : '禁用' }}
</span>
</div>
<div class="info-item">
<span class="info-label">最后登录</span>
<span class="info-value">{{ userInfo.login_time || '暂无记录' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ userInfo.create_time }}</span>
</div>
</div>
<a-divider /> <!-- 分割线 -->
<div class="user-profile-roles" v-if="userInfo.roles && userInfo.roles.length > 0">
<div class="info-item">
<span class="info-label">角色</span>
<span class="info-value">
<a-space>
<span v-for="role in userInfo.roles" :key="role.role_id">
{{ role.name }}
</span>
</a-space>
</span>
</div>
</div>
<div class="user-profile-roles" v-else>
<div class="info-item">
<span class="info-label">角色</span>
<span class="info-value">暂无角色信息</span>
</div>
</div>
</a-spin>
</a-modal>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { UserOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
// 弹窗相关状态
const profileModalVisible = ref(false);
const loading = ref(false);
const userInfo = ref({
user_id: '',
username: '',
name: '',
email: '',
status: 1,
roles: [],
login_time: '',
create_time: '',
update_time: ''
});
const breadcrumbItems = computed(() => {
const items = [];
const matched = route.matched;
// 如果当前路由是首页,不显示面包屑项
if (route.path === '/dashboard' || route.path === '/') {
return items;
}
// 首页可以被点击
items.push({
title: '首页',
path: '/dashboard',
clickable: true
});
// 定义一级菜单
const parentMenus = ['/projects', '/build', '/logs', '/user', '/environments', '/system'];
matched.forEach((item) => {
if (item.path !== '/' && item.meta && item.meta.title) {
// 判断是否为一级菜单
const isParentMenu = parentMenus.includes(item.path);
items.push({
title: item.meta.title,
path: item.path,
clickable: !isParentMenu // 只有一级菜单不可点击
});
}
});
return items;
});
// 获取用户名
const userName = computed(() => {
const userInfo = localStorage.getItem('user_info');
if (userInfo) {
return JSON.parse(userInfo).name;
}
return '';
});
// 获取用户信息
const fetchUserProfile = async () => {
loading.value = true;
try {
const token = localStorage.getItem('token');
const response = await axios.get('/api/user/profile/', {
headers: {
'Authorization': token
}
});
if (response.data.code === 200) {
userInfo.value = response.data.data;
} else {
message.error(response.data.message || '获取用户信息失败');
}
} catch (error) {
console.error('获取用户信息失败:', error);
message.error('获取用户信息失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 显示个人信息弹窗
const handleProfile = () => {
profileModalVisible.value = true;
fetchUserProfile();
};
// 退出登录
const handleLogout = async () => {
try {
const token = localStorage.getItem('token');
const response = await axios.post('/api/logout/', {}, {
headers: {
'Authorization': token
}
});
if (response.data.code === 200) {
message.success('退出成功');
localStorage.removeItem('token');
localStorage.removeItem('user_info');
router.push('/login');
} else {
message.error(response.data.message || '退出失败');
}
} catch (error) {
message.error('退出失败,请稍后重试');
console.error('Logout error:', error);
}
};
</script>
<style scoped>
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
}
.header-left {
margin-left: 24px;
}
.header-right {
margin-right: 24px;
}
:deep(.ant-layout-header) {
padding: 0;
height: 64px;
line-height: 64px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
}
:deep(.ant-dropdown-link) {
color: rgba(0, 0, 0, 0.85);
padding: 0 12px;
cursor: pointer;
}
:deep(.ant-breadcrumb a) {
color: rgba(0, 0, 0, 0.45);
transition: color 0.3s;
}
:deep(.ant-breadcrumb a:hover) {
color: rgba(0, 0, 0, 0.85);
}
:deep(.ant-breadcrumb span) {
color: rgba(0, 0, 0, 0.45);
}
/* 弹窗相关样式 */
:deep(.ant-divider) {
margin: 16px 0;
}
:deep(.ant-modal-body) {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
.user-profile-info,
.user-profile-roles {
padding: 0 8px;
}
.info-item {
display: flex;
margin-bottom: 12px;
align-items: center;
}
.info-label {
width: 80px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
.info-value {
flex: 1;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<a-layout style="min-height: 100vh">
<Sidebar />
<a-layout>
<Header />
<Content />
<Footer />
</a-layout>
</a-layout>
</template>
<script setup>
import Sidebar from './Sidebar.vue';
import Header from './Header.vue';
import Content from './Content.vue';
import Footer from './Footer.vue';
</script>
<style scoped>
:deep(.ant-layout) {
min-height: 100vh;
height: 100%;
background: #fff;
}
.site-layout .site-layout-background {
background: #fff;
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<a-layout-sider v-model:collapsed="collapsed" theme="light" collapsible>
<div class="logo">
<img v-if="collapsed" src="../../assets/image/liteops.png" alt="Logo" />
<img v-else src="../../assets/image/liteops-sidebar.png" alt="Logo" />
</div>
<div class="menu-container">
<a-menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
mode="inline"
@click="handleMenuClick"
v-if="permissionStore.initialized"
>
<!-- 首页 -->
<a-menu-item key="/dashboard" v-if="hasMenuPermission('/dashboard')">
<template #icon>
<DashboardOutlined />
</template>
<span>首页</span>
</a-menu-item>
<!-- 项目管理 -->
<a-sub-menu key="/projects" v-if="hasAnySubMenuPermission('/projects')">
<template #icon>
<ProjectOutlined />
</template>
<template #title>项目管理</template>
<a-menu-item key="/projects/list" v-if="hasMenuPermission('/projects/list')">项目列表</a-menu-item>
</a-sub-menu>
<!-- 构建与部署 -->
<a-sub-menu key="/build" v-if="hasAnySubMenuPermission('/build')">
<template #icon>
<BuildOutlined />
</template>
<template #title>构建与部署</template>
<a-menu-item key="/build/tasks" v-if="hasMenuPermission('/build/tasks')">构建任务</a-menu-item>
<a-menu-item key="/build/history" v-if="hasMenuPermission('/build/history')">构建历史</a-menu-item>
</a-sub-menu>
<!-- 日志与监控 -->
<a-sub-menu key="/logs" v-if="hasAnySubMenuPermission('/logs')">
<template #icon>
<FileSearchOutlined />
</template>
<template #title>日志与监控</template>
<a-menu-item key="/logs/login" v-if="hasMenuPermission('/logs/login')">登陆日志</a-menu-item>
</a-sub-menu>
<!-- 用户与权限管理 -->
<a-sub-menu key="/user" v-if="hasAnySubMenuPermission('/user')">
<template #icon>
<UserOutlined />
</template>
<template #title>用户与权限</template>
<a-menu-item key="/user/list" v-if="hasMenuPermission('/user/list')">用户管理</a-menu-item>
<a-menu-item key="/user/role" v-if="hasMenuPermission('/user/role')">角色管理</a-menu-item>
</a-sub-menu>
<!-- 凭证管理 -->
<a-menu-item key="/credentials" v-if="hasMenuPermission('/credentials')">
<template #icon>
<KeyOutlined />
</template>
<span>凭证管理</span>
</a-menu-item>
<!-- 环境配置 -->
<a-sub-menu key="/environments" v-if="hasAnySubMenuPermission('/environments')">
<template #icon>
<CloudServerOutlined />
</template>
<template #title>环境配置</template>
<a-menu-item key="/environments/list" v-if="hasMenuPermission('/environments/list')">环境列表</a-menu-item>
</a-sub-menu>
<!-- 系统配置 -->
<a-sub-menu key="/system" v-if="hasAnySubMenuPermission('/system')">
<template #icon>
<SettingOutlined />
</template>
<template #title>系统配置</template>
<a-menu-item key="/system/basic" v-if="hasMenuPermission('/system/basic')">基本设置</a-menu-item>
</a-sub-menu>
</a-menu>
<div v-else class="menu-loading">
<a-spin tip="加载菜单权限中..." />
</div>
</div>
</a-layout-sider>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { hasMenuPermission, hasAnySubMenuPermission, permissionStore } from '../../utils/permission';
import {
DashboardOutlined,
ProjectOutlined,
BuildOutlined,
FileSearchOutlined,
UserOutlined,
SettingOutlined,
KeyOutlined,
CloudServerOutlined
} from '@ant-design/icons-vue';
const router = useRouter();
const route = useRoute();
const collapsed = ref(false);
const selectedKeys = ref(['/dashboard']);
const openKeys = ref([]);
// 获取当前路由的父级路径
const getParentPath = (path) => {
const pathParts = path.split('/');
return pathParts.length > 2 ? `/${pathParts[1]}` : path;
};
// 初始化菜单状态
onMounted(() => {
const currentPath = route.path;
selectedKeys.value = [currentPath];
if (currentPath !== '/dashboard') {
const parentPath = getParentPath(currentPath);
openKeys.value = [parentPath];
}
});
// 监听路由变化
watch(() => route.path, (newPath) => {
selectedKeys.value = [newPath];
const parentPath = getParentPath(newPath);
if (!openKeys.value.includes(parentPath) && newPath !== '/dashboard') {
openKeys.value = [parentPath];
}
});
// 处理菜单点击
const handleMenuClick = ({ key }) => {
router.push(key);
};
</script>
<style scoped>
.logo {
height: 65px;
display: flex;
justify-content: center;
align-items: center;
/* padding: 8px; */
/* overflow: hidden; */
}
.logo img {
height: 65px;
/* height: auto; */
/* max-height: 50px; */
max-width: 100%;
object-fit: contain;
}
.menu-container {
height: calc(100vh - 64px);
overflow-y: auto;
overflow-x: hidden;
}
:deep(.ant-layout-sider) {
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
position: relative;
z-index: 10;
}
:deep(.ant-layout-sider-collapsed .logo) {
padding: 8px 0;
}
:deep(.ant-layout-sider-collapsed .logo img) {
max-width: 32px;
}
:deep(.ant-layout-sider-collapsed .ant-menu-item .anticon),
:deep(.ant-layout-sider-collapsed .ant-menu-submenu-title .anticon) {
margin-right: 0;
font-size: 16px;
}
:deep(.ant-menu-item) {
height: 40px !important;
line-height: 40px !important;
margin: 4px 0 !important;
padding-left: 24px !important;
}
:deep(.ant-menu-submenu-title) {
height: 40px !important;
line-height: 40px !important;
margin: 4px 0 !important;
padding-left: 24px !important;
}
/* 子菜单项的缩进一致 */
:deep(.ant-menu-sub .ant-menu-item) {
padding-left: 48px !important;
}
/* 图标对齐 */
:deep(.ant-menu-item .anticon),
:deep(.ant-menu-submenu-title .anticon) {
min-width: 14px;
margin-right: 10px;
font-size: 16px;
}
/* 滚动条样式 */
.menu-container::-webkit-scrollbar {
width: 3px;
}
.menu-container::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.menu-container::-webkit-scrollbar-track {
background: transparent;
}
.menu-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>