diff --git a/.gitignore b/.gitignore index d2f0fb6..cbc6202 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,4 @@ dist-ssr *.xml # web -/web/ \ No newline at end of file +# /web/ \ No newline at end of file diff --git a/README.md b/README.md index 361cb9e..0aa04e8 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ LiteOps主要适用于以下场景: ### 前置要求 -在开始部署之前,请确保您的系统满足以下要求: +在开始部署之前,请确保你的系统满足以下要求: - **操作系统**:Linux (推荐 Ubuntu 20.04+、CentOS 7+) - **Docker**:版本 20.0+ @@ -179,7 +179,7 @@ LiteOps主要适用于以下场景: #### 1. 获取部署文件 -您需要获取以下部署文件: +你需要获取以下部署文件: - `start-containers.sh` - 一键部署脚本 - `liteops_init.sql` - 数据库初始化文件 @@ -226,7 +226,7 @@ chmod +x start-containers.sh #### 5. 验证部署 -部署完成后,您可以通过以下方式验证: +部署完成后,你可以通过以下方式验证: ```bash # 检查容器状态 @@ -257,7 +257,7 @@ docker logs liteops-mysql ### 访问应用 -部署成功后,您可以通过以下地址访问: +部署成功后,你可以通过以下地址访问: - **前端界面**:http://localhost - **后端API**:http://localhost:8900/api/ @@ -284,7 +284,7 @@ LiteOps目前处于未完善状态,虽然核心功能已经初步实现,但 ## 📞 联系我 -如果您对LiteOps有任何建议、问题或需求,欢迎通过以下方式联系我: +如果你对LiteOps有任何建议、问题或需求,欢迎通过以下方式联系我: - **邮箱**:hukdoesn@163.com - **GitHub Issues**:[提交问题或建议](https://github.com/hukdoesn/liteops/issues) diff --git a/backend/apps/views/build.py b/backend/apps/views/build.py index d05854d..5c6cb60 100644 --- a/backend/apps/views/build.py +++ b/backend/apps/views/build.py @@ -819,7 +819,7 @@ class BuildExecuteView(View): history_id=generate_id(), task=task, build_number=build_number, - branch=branch if branch else '', # 对于预发布和生产环境,分支可能为空 + branch=branch if branch else '', # 对于预发布和生产环境,分支为空 commit_id=commit_id, version=version if version else None, # 对于预发布和生产环境,使用传入的版本号 status='pending', # 初始状态为等待中 diff --git a/backend/apps/views/notification.py b/backend/apps/views/notification.py index 46f54c6..0f848aa 100644 --- a/backend/apps/views/notification.py +++ b/backend/apps/views/notification.py @@ -319,7 +319,7 @@ class NotificationTestView(View): # 准备测试消息 timestamp = str(int(time.time() * 1000)) - test_message = "这是一条测试消息,如果您收到了这条消息,说明机器人配置正确。" + test_message = "这是一条测试消息,如果你收到了这条消息,说明机器人配置正确。" # 根据不同类型的机器人发送测试消息 try: diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 86e49fb..69ac827 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -194,6 +194,6 @@ STATIC_URL = 'static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 构建相关配置 -#BUILD_ROOT = Path('/data/liteops/build') # 修改为指定目录 -BUILD_ROOT = Path('/data') +BUILD_ROOT = Path('/Users/huk/Downloads/data') # 修改为指定目录 +# BUILD_ROOT = Path('/data') BUILD_ROOT.mkdir(exist_ok=True, parents=True) # 确保目录存在,包括父目录 \ No newline at end of file diff --git a/backend/conf/config.txt b/backend/conf/config.txt index e8c4f77..41a1f92 100644 --- a/backend/conf/config.txt +++ b/backend/conf/config.txt @@ -1,7 +1,7 @@ [client] -#host = 127.0.0.1 +host = 127.0.0.1 #host = mysql -host = liteops-mysql +#host = liteops-mysql port = 3306 database = liteops user = root diff --git a/web/.env.development b/web/.env.development new file mode 100644 index 0000000..de96daa --- /dev/null +++ b/web/.env.development @@ -0,0 +1,2 @@ +# 在开发环境中使用完整路径 +VITE_API_URL=http://localhost:8900 \ No newline at end of file diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..8388c4b --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + + +
+ + + diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..71897ae --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/web/src/assets/css/global.css b/web/src/assets/css/global.css new file mode 100644 index 0000000..fa36090 --- /dev/null +++ b/web/src/assets/css/global.css @@ -0,0 +1,125 @@ +/* 定义自定义PingFang字体 - 强制使用项目中的字体文件 */ +@font-face { + font-family: 'PingFangCustom'; + src: url('../font/PingFang.ttc') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'PingFangCustom'; + src: url('../font/PingFang.ttc') format('truetype'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'PingFangCustom'; + src: url('../font/PingFang.ttc') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'PingFangCustom'; + src: url('../font/PingFang.ttc') format('truetype'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'PingFangCustom'; + src: url('../font/PingFang.ttc') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +* { + font-family: 'PingFangCustom', Arial, sans-serif !important; +} + +body, html { + font-family: 'PingFangCustom', Arial, sans-serif !important; + margin: 0; + padding: 0; +} + +:root { + font-family: 'PingFangCustom', Arial, sans-serif !important; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.ant-typography, +.ant-btn, +.ant-input, +.ant-select, +.ant-table, +.ant-menu, +.ant-dropdown, +.ant-modal, +.ant-message, +.ant-notification, +.ant-form, +.ant-card, +.ant-tabs, +.ant-breadcrumb, +.ant-pagination, +.ant-steps, +.ant-tree, +.ant-list, +.ant-drawer, +.ant-popover, +.ant-tooltip, +.ant-alert, +.ant-badge, +.ant-tag, +.ant-progress, +.ant-spin, +.ant-switch, +.ant-radio, +.ant-checkbox, +.ant-rate, +.ant-slider, +.ant-upload, +.ant-calendar, +.ant-date-picker, +.ant-time-picker, +.ant-config-provider, +.ant-app { + font-family: 'PingFangCustom', Arial, sans-serif !important; +} + +[class*="ant-"] { + font-family: 'PingFangCustom', Arial, sans-serif !important; +} + +input, textarea, select, button { + font-family: 'PingFangCustom', Arial, sans-serif !important; +} + +.ant-input, +.ant-input-affix-wrapper, +.ant-input-number, +.ant-select-selector, +.ant-cascader-picker, +.ant-picker, +.ant-mentions, +.ant-checkbox-wrapper, +.ant-radio-wrapper { + font-family: 'PingFangCustom', Arial, sans-serif !important; +} \ No newline at end of file diff --git a/web/src/assets/css/reset.css b/web/src/assets/css/reset.css new file mode 100644 index 0000000..67d7f8e --- /dev/null +++ b/web/src/assets/css/reset.css @@ -0,0 +1,43 @@ +/* global.css */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/web/src/assets/font/PingFang.ttc b/web/src/assets/font/PingFang.ttc new file mode 100644 index 0000000..d06aa64 Binary files /dev/null and b/web/src/assets/font/PingFang.ttc differ diff --git a/web/src/assets/image/liteops-sidebar.png b/web/src/assets/image/liteops-sidebar.png new file mode 100644 index 0000000..706ab53 Binary files /dev/null and b/web/src/assets/image/liteops-sidebar.png differ diff --git a/web/src/assets/image/liteops.png b/web/src/assets/image/liteops.png new file mode 100644 index 0000000..2d83118 Binary files /dev/null and b/web/src/assets/image/liteops.png differ diff --git a/web/src/assets/image/liteopsv1.png b/web/src/assets/image/liteopsv1.png new file mode 100644 index 0000000..6c2cd72 Binary files /dev/null and b/web/src/assets/image/liteopsv1.png differ diff --git a/web/src/assets/vue.svg b/web/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/layout/Content.vue b/web/src/components/layout/Content.vue new file mode 100644 index 0000000..07d1571 --- /dev/null +++ b/web/src/components/layout/Content.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/layout/Footer.vue b/web/src/components/layout/Footer.vue new file mode 100644 index 0000000..7aea788 --- /dev/null +++ b/web/src/components/layout/Footer.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/web/src/components/layout/Header.vue b/web/src/components/layout/Header.vue new file mode 100644 index 0000000..77371dd --- /dev/null +++ b/web/src/components/layout/Header.vue @@ -0,0 +1,297 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/layout/MainLayout.vue b/web/src/components/layout/MainLayout.vue new file mode 100644 index 0000000..3987230 --- /dev/null +++ b/web/src/components/layout/MainLayout.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/layout/Sidebar.vue b/web/src/components/layout/Sidebar.vue new file mode 100644 index 0000000..e1dfe4a --- /dev/null +++ b/web/src/components/layout/Sidebar.vue @@ -0,0 +1,241 @@ + + + + + \ No newline at end of file diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..794024e --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,24 @@ +import { createApp } from 'vue' +// import './style.css' +import App from './App.vue' +import Antd from 'ant-design-vue'; +import './assets/css/reset.css'; +import './assets/css/global.css'; +import axios from 'axios' +import router from './router' +// import permissionDirective from './directives/permission'; + +// 配置 axios +// 使用环境变量中的API URL +axios.defaults.baseURL = import.meta.env.VITE_API_URL; + +const app = createApp(App); + +// 使用 Ant Design Vue 和 Vue Router +// app.use(router).use(Antd).use(permissionDirective); +app.use(router).use(Antd) + +// 将 axios 添加到 Vue 实例的全局属性中 +app.config.globalProperties.$axios = axios; + +app.mount('#app') \ No newline at end of file diff --git a/web/src/router/index.js b/web/src/router/index.js new file mode 100644 index 0000000..262d1c6 --- /dev/null +++ b/web/src/router/index.js @@ -0,0 +1,246 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import MainLayout from '../components/layout/MainLayout.vue'; +import axios from 'axios'; +import { permissionStore, initUserPermissions, hasMenuPermission, hasAnySubMenuPermission } from '../utils/permission'; +import { message } from 'ant-design-vue'; + +// 添加axios响应拦截器 +axios.interceptors.response.use( + response => response, + error => { + if (error.response && error.response.status === 401) { + // 清除登录信息 + localStorage.removeItem('token'); + localStorage.removeItem('user_info'); + // 跳转到登录页 + router.push('/login'); + } + return Promise.reject(error); + } +); + +const routes = [ + { + path: '/login', + name: 'login', + component: () => import('../views/login/LoginView.vue'), + meta: { title: '登录' } + }, + { + path: '/', + component: MainLayout, + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + name: 'dashboard', + component: () => import('../views/dashboard/Dashboard.vue'), + meta: { title: '首页', permission: '/dashboard' } + }, + // 项目管理 + { + path: 'projects', + name: 'projects', + meta: { title: '项目管理', permission: '/projects' }, + redirect: '/projects/list', + children: [ + { + path: 'list', + name: 'project-list', + component: () => import('../views/projects/ProjectList.vue'), + meta: { title: '项目列表', permission: '/projects/list' } + }, + { + path: 'detail', + name: 'project-detail', + component: () => import('../views/projects/ProjectDetail.vue'), + meta: { title: '项目详情', permission: '/projects/list' } + } + ] + }, + // 构建与部署 + { + path: 'build', + name: 'build', + meta: { title: '构建与部署', permission: '/build' }, + redirect: '/build/tasks', + children: [ + { + path: 'tasks', + name: 'build-tasks', + component: () => import('../views/build/BuildTasks.vue'), + meta: { title: '构建任务', permission: '/build/tasks' } + }, + { + path: 'tasks/detail', + name: 'build-task-detail', + component: () => import('../views/build/BuildTaskDetail.vue'), + meta: { title: '任务详情', permission: '/build/tasks' } + }, + { + path: 'tasks/create', + name: 'build-task-create', + component: () => import('../views/build/BuildTaskEdit.vue'), + meta: { title: '新建构建任务', permission: '/build/tasks' } + }, + { + path: 'tasks/edit', + name: 'build-task-edit', + component: () => import('../views/build/BuildTaskEdit.vue'), + meta: { title: '编辑构建任务', permission: '/build/tasks' } + }, + { + path: 'history', + name: 'build-history', + component: () => import('../views/build/BuildHistory.vue'), + meta: { title: '构建历史', permission: '/build/history' } + } + ] + }, + // 日志与监控 + { + path: 'logs', + name: 'logs', + meta: { title: '日志与监控', permission: '/logs' }, + redirect: '/logs/login', + children: [ + { + path: 'login', + name: 'login-logs', + component: () => import('../views/logs/LoginLogs.vue'), + meta: { title: '登陆日志', permission: '/logs/login' } + }, + { + path: 'login/detail', + name: 'login-log-detail', + component: () => import('../views/logs/LoginLogDetail.vue'), + meta: { title: '登录日志详情', permission: '/logs/login' } + } + ] + }, + // 用户与权限 + { + path: 'user', + name: 'user', + meta: { title: '用户与权限', permission: '/user' }, + redirect: '/user/list', + children: [ + { + path: 'list', + name: 'user-list', + component: () => import('../views/user/UserList.vue'), + meta: { title: '用户管理', permission: '/user/list' } + }, + { + path: 'role', + name: 'user-role', + component: () => import('../views/user/UserRole.vue'), + meta: { title: '角色管理', permission: '/user/role' } + } + ] + }, + // 凭证管理 + { + path: 'credentials', + name: 'credentials', + component: () => import('../views/credentials/CredentialsList.vue'), + meta: { title: '凭证管理', permission: '/credentials' } + }, + // 环境配置 + { + path: 'environments', + name: 'environments', + meta: { title: '环境配置', permission: '/environments' }, + redirect: '/environments/list', + children: [ + { + path: 'list', + name: 'environment-list', + component: () => import('../views/environments/EnvironmentList.vue'), + meta: { title: '环境列表', permission: '/environments/list' } + }, + { + path: 'detail', + name: 'environment-detail', + component: () => import('../views/environments/EnvironmentDetail.vue'), + meta: { title: '环境详情', permission: '/environments/list' } + } + ] + }, + // 系统配置 + { + path: 'system', + name: 'system', + meta: { title: '系统配置', permission: '/system' }, + redirect: '/system/basic', + children: [ + { + path: 'basic', + name: 'system-basic', + component: () => import('../views/system/BasicSettings.vue'), + meta: { title: '基本设置', permission: '/system/basic' } + } + ] + }, + + ] + } +]; + +const router = createRouter({ + history: createWebHistory(), + routes +}); + +// 权限初始化标志 +let permissionInitialized = false; + +// 路由守卫 +router.beforeEach(async (to, from, next) => { + // 如果是登录页,直接通过 + if (to.path === '/login') { + return next(); + } + + // 检查是否已登录 + const token = localStorage.getItem('token'); + if (!token) { + return next('/login'); + } + + // 初始化权限 + if (!permissionInitialized && !permissionStore.initialized) { + permissionInitialized = true; + const success = await initUserPermissions(); + + // 如果权限初始化失败,跳转到登录页 + if (!success) { + console.error('权限初始化失败,重定向到登录页'); + localStorage.removeItem('token'); + localStorage.removeItem('user_info'); + return next('/login'); + } + } + + // 检查菜单权限 + if (to.meta && to.meta.permission) { + const permissionPath = to.meta.permission; + const pathParts = permissionPath.split('/'); + const isSubMenu = pathParts.length > 2; + + // 如果是子菜单,同时检查直接权限和父菜单的子权限 + const hasPermission = isSubMenu ? + (hasMenuPermission(permissionPath) || hasAnySubMenuPermission(permissionPath)) : + hasMenuPermission(permissionPath); + + if (!hasPermission) { + console.warn(`用户无权访问路由: ${to.path}, 所需权限: ${to.meta.permission}`); + message.error(`你没有访问${to.meta.title || '该页面'}的权限`); + return next('/dashboard'); + } + } + + next(); +}); + +export default router; \ No newline at end of file diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..06458a9 --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,75 @@ +:root { + font-family: 'PingFangCustom', Arial, sans-serif !important; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + width: 100%; + height: 100vh; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/web/src/utils/permission.js b/web/src/utils/permission.js new file mode 100644 index 0000000..5831e56 --- /dev/null +++ b/web/src/utils/permission.js @@ -0,0 +1,222 @@ +import axios from 'axios'; +import { ref, reactive } from 'vue'; +import { message } from 'ant-design-vue'; + +// 权限数据存储 +export const permissionStore = reactive({ + initialized: false, + menuPermissions: [], + functionPermissions: {}, + dataPermissions: { + project_scope: 'all', + project_ids: [], + environment_scope: 'all', + environment_types: [] + } +}); + +// 初始化用户权限 +export const initUserPermissions = async () => { + try { + // 从本地存储获取用户信息 + const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}'); + + if (!userInfo.user_id) { + console.error('用户信息不存在,权限初始化失败'); + return false; + } + + // 获取用户的角色权限 + const token = localStorage.getItem('token'); + const response = await axios.get('/api/user/permissions', { + headers: { + 'Authorization': token + } + }); + + if (response.data.code === 200) { + const permissions = response.data.data; + + // 存储权限信息 + permissionStore.menuPermissions = permissions.menu || []; + permissionStore.functionPermissions = permissions.function || {}; + permissionStore.dataPermissions = permissions.data || { + project_scope: 'all', + project_ids: [], + environment_scope: 'all', + environment_types: [] + }; + + permissionStore.initialized = true; + + return true; + } else { + console.error('获取用户权限失败:', response.data.message); + return false; + } + } catch (error) { + console.error('初始化用户权限失败:', error); + return false; + } +}; + +// 检查菜单权限 +export const hasMenuPermission = (menuPath) => { + if (!permissionStore.initialized) { + console.warn('权限尚未初始化,拒绝所有菜单权限', menuPath); + return false; + } + + const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}'); + if (userInfo.is_admin) { + console.log(`用户是管理员,自动拥有菜单权限: ${menuPath}`); + return true; + } + + const hasPermission = permissionStore.menuPermissions.includes(menuPath); + return hasPermission; +}; + +// 检查是否有子菜单权限 +export const hasAnySubMenuPermission = (parentPath) => { + if (!permissionStore.initialized) { + return false; + } + + // 管理员拥有所有权限 + const userInfo = JSON.parse(localStorage.getItem('user_info') || '{}'); + if (userInfo.is_admin) { + return true; + } + + // 检查是否直接拥有父菜单权限 + if (permissionStore.menuPermissions.includes(parentPath)) { + return true; + } + + // 检查是否拥有任何以父菜单路径开头的子菜单权限 + return permissionStore.menuPermissions.some(permission => + permission !== parentPath && permission.startsWith(`${parentPath}/`) + ); +}; + +// 检查功能权限 +export const hasFunctionPermission = (module, action) => { + if (!permissionStore.initialized) { + return false; + } + + const modulePermissions = permissionStore.functionPermissions[module] || []; + return modulePermissions.includes(action); +}; + +// 检查项目数据权限 +export const hasProjectPermission = (projectId) => { + if (!permissionStore.initialized) { + return false; + } + + // 如果有所有项目的权限 + if (permissionStore.dataPermissions.project_scope === 'all') { + return true; + } + + return permissionStore.dataPermissions.project_ids.includes(projectId); +}; + +// 检查环境数据权限 +export const hasEnvironmentPermission = (environmentType) => { + if (!permissionStore.initialized) { + return false; + } + + // 如果有所有环境的权限 + if (permissionStore.dataPermissions.environment_scope === 'all') { + return true; + } + + return permissionStore.dataPermissions.environment_types.includes(environmentType); +}; + +export const getPermittedProjectIds = () => { + if (!permissionStore.initialized) { + return []; + } + + if (permissionStore.dataPermissions.project_scope === 'all') { + return null; + } + + return permissionStore.dataPermissions.project_ids || []; +}; + +// 获取有权限的环境类型 +export const getPermittedEnvironmentTypes = () => { + if (!permissionStore.initialized) { + return []; + } + + if (permissionStore.dataPermissions.environment_scope === 'all') { + return null; + } + + return permissionStore.dataPermissions.environment_types || []; +}; + +// 统一的权限错误提示 +export const showPermissionError = (module, action) => { + let errorMsg = '你没有权限执行此操作'; + + if (module && action) { + const actionText = { + 'view': '查看', + 'create': '创建', + 'edit': '编辑', + 'delete': '删除', + 'execute': '执行', + 'deploy': '部署', + 'rollback': '回滚', + 'approve': '审批', + 'test': '测试', + 'view_log': '查看日志', + 'disable': '禁用/启用', + }[action] || action; + + const moduleText = { + 'project': '项目', + 'build': '构建任务', + 'build_task': '构建任务', + 'build_history': '构建历史', + 'environment': '环境', + 'credential': '凭证', + 'user': '用户', + 'role': '角色', + 'notification': '通知' + }[module] || module; + + errorMsg = `你没有${moduleText}${actionText}权限`; + } + + message.error(errorMsg); + return false; +}; + +// 检查功能权限和数据权限 +export const checkPermission = (module, action, entityId = null, entityType = 'project') => { + if (!hasFunctionPermission(module, action)) { + showPermissionError(module, action); + return false; + } + + if (entityId) { + if (entityType === 'project' && !hasProjectPermission(entityId)) { + message.error('你没有该项目的访问权限'); + return false; + } else if (entityType === 'environment' && !hasEnvironmentPermission(entityId)) { + message.error('你没有该环境的访问权限'); + return false; + } + } + + return true; +}; \ No newline at end of file diff --git a/web/src/views/build/BuildHistory.vue b/web/src/views/build/BuildHistory.vue new file mode 100644 index 0000000..bb82c9b --- /dev/null +++ b/web/src/views/build/BuildHistory.vue @@ -0,0 +1,682 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/BuildTaskDetail.vue b/web/src/views/build/BuildTaskDetail.vue new file mode 100644 index 0000000..f52de10 --- /dev/null +++ b/web/src/views/build/BuildTaskDetail.vue @@ -0,0 +1,300 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/BuildTaskEdit.vue b/web/src/views/build/BuildTaskEdit.vue new file mode 100644 index 0000000..c6b011d --- /dev/null +++ b/web/src/views/build/BuildTaskEdit.vue @@ -0,0 +1,879 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/BuildTasks.vue b/web/src/views/build/BuildTasks.vue new file mode 100644 index 0000000..0dd2c27 --- /dev/null +++ b/web/src/views/build/BuildTasks.vue @@ -0,0 +1,1701 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/components/BuildNotification.vue b/web/src/views/build/components/BuildNotification.vue new file mode 100644 index 0000000..89c43a8 --- /dev/null +++ b/web/src/views/build/components/BuildNotification.vue @@ -0,0 +1,101 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/components/CodeEditor.vue b/web/src/views/build/components/CodeEditor.vue new file mode 100644 index 0000000..534f03a --- /dev/null +++ b/web/src/views/build/components/CodeEditor.vue @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/components/FullscreenLogViewer.vue b/web/src/views/build/components/FullscreenLogViewer.vue new file mode 100644 index 0000000..b4c7e87 --- /dev/null +++ b/web/src/views/build/components/FullscreenLogViewer.vue @@ -0,0 +1,311 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/build/components/SystemVariablesList.vue b/web/src/views/build/components/SystemVariablesList.vue new file mode 100644 index 0000000..e05182c --- /dev/null +++ b/web/src/views/build/components/SystemVariablesList.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/credentials/CredentialsList.vue b/web/src/views/credentials/CredentialsList.vue new file mode 100644 index 0000000..d33efe1 --- /dev/null +++ b/web/src/views/credentials/CredentialsList.vue @@ -0,0 +1,904 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/dashboard/Dashboard.vue b/web/src/views/dashboard/Dashboard.vue new file mode 100644 index 0000000..abfa0f2 --- /dev/null +++ b/web/src/views/dashboard/Dashboard.vue @@ -0,0 +1,895 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/environments/EnvironmentDetail.vue b/web/src/views/environments/EnvironmentDetail.vue new file mode 100644 index 0000000..a3d82da --- /dev/null +++ b/web/src/views/environments/EnvironmentDetail.vue @@ -0,0 +1,290 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/environments/EnvironmentList.vue b/web/src/views/environments/EnvironmentList.vue new file mode 100644 index 0000000..bd3bf77 --- /dev/null +++ b/web/src/views/environments/EnvironmentList.vue @@ -0,0 +1,459 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/login/LoginView.vue b/web/src/views/login/LoginView.vue new file mode 100644 index 0000000..f30f7b8 --- /dev/null +++ b/web/src/views/login/LoginView.vue @@ -0,0 +1,269 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/logs/LoginLogDetail.vue b/web/src/views/logs/LoginLogDetail.vue new file mode 100644 index 0000000..a5c0b72 --- /dev/null +++ b/web/src/views/logs/LoginLogDetail.vue @@ -0,0 +1,147 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/logs/LoginLogs.vue b/web/src/views/logs/LoginLogs.vue new file mode 100644 index 0000000..1048634 --- /dev/null +++ b/web/src/views/logs/LoginLogs.vue @@ -0,0 +1,219 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/projects/ProjectDetail.vue b/web/src/views/projects/ProjectDetail.vue new file mode 100644 index 0000000..d42c311 --- /dev/null +++ b/web/src/views/projects/ProjectDetail.vue @@ -0,0 +1,312 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/projects/ProjectEdit.vue b/web/src/views/projects/ProjectEdit.vue new file mode 100644 index 0000000..c3881eb --- /dev/null +++ b/web/src/views/projects/ProjectEdit.vue @@ -0,0 +1,239 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/projects/ProjectList.vue b/web/src/views/projects/ProjectList.vue new file mode 100644 index 0000000..8831b35 --- /dev/null +++ b/web/src/views/projects/ProjectList.vue @@ -0,0 +1,428 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/system/BasicSettings.vue b/web/src/views/system/BasicSettings.vue new file mode 100644 index 0000000..34c8667 --- /dev/null +++ b/web/src/views/system/BasicSettings.vue @@ -0,0 +1,670 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/system/Notification.vue b/web/src/views/system/Notification.vue new file mode 100644 index 0000000..ded6a8e --- /dev/null +++ b/web/src/views/system/Notification.vue @@ -0,0 +1,476 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/user/UserList.vue b/web/src/views/user/UserList.vue new file mode 100644 index 0000000..7af3934 --- /dev/null +++ b/web/src/views/user/UserList.vue @@ -0,0 +1,501 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/user/UserRole.vue b/web/src/views/user/UserRole.vue new file mode 100644 index 0000000..d76236c --- /dev/null +++ b/web/src/views/user/UserRole.vue @@ -0,0 +1,788 @@ + + + + + \ No newline at end of file diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..15cf964 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + port: 8000, + host: true, + strictPort: true, + cors: true, + allowedHosts: [ + '0.0.0.0', + ] + } +}) \ No newline at end of file