
Features: - Complete project management system with Epic/Story/Task hierarchy - Vue.js 3 + Element Plus frontend with kanban board - Go backend with Gin framework and GORM - OAuth2 integration with Gitea - Docker containerization with MySQL - RESTful API for project, task, and user management - JWT authentication and authorization - Responsive web interface with dashboard
522 lines
23 KiB
HTML
522 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Gitea Project Management</title>
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||
<script src="https://unpkg.com/element-plus@2.4.0/dist/index.full.min.js"></script>
|
||
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.0/dist/index.css">
|
||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||
<style>
|
||
.main-container {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.header {
|
||
background: #001529;
|
||
color: white;
|
||
padding: 0 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
height: 60px;
|
||
}
|
||
.content {
|
||
flex: 1;
|
||
display: flex;
|
||
}
|
||
.sidebar {
|
||
width: 240px;
|
||
background: #f0f2f5;
|
||
border-right: 1px solid #d9d9d9;
|
||
}
|
||
.main-content {
|
||
flex: 1;
|
||
padding: 20px;
|
||
background: white;
|
||
}
|
||
.kanban-board {
|
||
display: flex;
|
||
gap: 20px;
|
||
padding: 20px;
|
||
overflow-x: auto;
|
||
}
|
||
.kanban-column {
|
||
min-width: 300px;
|
||
background: #f7f8fa;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
}
|
||
.kanban-header {
|
||
font-weight: 500;
|
||
margin-bottom: 16px;
|
||
padding: 8px 12px;
|
||
background: white;
|
||
border-radius: 4px;
|
||
}
|
||
.task-card {
|
||
background: white;
|
||
border: 1px solid #e1e4e8;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
.task-card:hover {
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
.task-title {
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
.task-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #586069;
|
||
}
|
||
.priority-high { border-left: 4px solid #f5222d; }
|
||
.priority-medium { border-left: 4px solid #fa8c16; }
|
||
.priority-low { border-left: 4px solid #52c41a; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="main-container">
|
||
<div class="header">
|
||
<div>
|
||
<h2 style="margin: 0;">Gitea Project Management</h2>
|
||
</div>
|
||
<div v-if="user">
|
||
<el-dropdown>
|
||
<el-avatar :src="user.avatar_url" :size="36">{{ user.username[0] }}</el-avatar>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item>{{ user.full_name || user.username }}</el-dropdown-item>
|
||
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
<div v-else>
|
||
<el-button type="primary" @click.prevent="login">登录</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content" v-if="user">
|
||
<div class="sidebar">
|
||
<el-menu :default-active="activeMenu" @select="handleMenuSelect">
|
||
<el-menu-item index="dashboard">
|
||
<el-icon><house /></el-icon>
|
||
<span>仪表板</span>
|
||
</el-menu-item>
|
||
<el-menu-item index="projects">
|
||
<el-icon><folder /></el-icon>
|
||
<span>项目管理</span>
|
||
</el-menu-item>
|
||
<el-menu-item index="tasks">
|
||
<el-icon><document /></el-icon>
|
||
<span>任务看板</span>
|
||
</el-menu-item>
|
||
<el-menu-item index="sprints">
|
||
<el-icon><calendar /></el-icon>
|
||
<span>迭代管理</span>
|
||
</el-menu-item>
|
||
<el-menu-item index="reports">
|
||
<el-icon><data-line /></el-icon>
|
||
<span>报表统计</span>
|
||
</el-menu-item>
|
||
</el-menu>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<!-- 项目管理页面 -->
|
||
<div v-if="activeMenu === 'projects'">
|
||
<div style="margin-bottom: 20px; display: flex; justify-content: between; align-items: center;">
|
||
<h3>项目管理</h3>
|
||
<el-button type="primary" @click="showCreateProject = true">创建项目</el-button>
|
||
</div>
|
||
|
||
<el-table :data="projects" style="width: 100%">
|
||
<el-table-column prop="name" label="项目名称"></el-table-column>
|
||
<el-table-column prop="description" label="描述"></el-table-column>
|
||
<el-table-column prop="status" label="状态">
|
||
<template #default="scope">
|
||
<el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="priority" label="优先级">
|
||
<template #default="scope">
|
||
<el-tag :type="getPriorityType(scope.row.priority)">{{ scope.row.priority }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="owner.username" label="负责人"></el-table-column>
|
||
<el-table-column label="操作">
|
||
<template #default="scope">
|
||
<el-button size="small" @click="viewProject(scope.row)">查看</el-button>
|
||
<el-button size="small" type="primary" @click="editProject(scope.row)">编辑</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 任务看板页面 -->
|
||
<div v-if="activeMenu === 'tasks'">
|
||
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
||
<h3>任务看板</h3>
|
||
<div>
|
||
<el-select v-model="selectedProject" placeholder="选择项目" @change="loadTasks" style="margin-right: 10px;">
|
||
<el-option
|
||
v-for="project in projects"
|
||
:key="project.id"
|
||
:label="project.name"
|
||
:value="project.id">
|
||
</el-option>
|
||
</el-select>
|
||
<el-button type="primary" @click="showCreateTask = true" :disabled="!selectedProject">创建任务</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kanban-board">
|
||
<div class="kanban-column" v-for="status in taskStatuses" :key="status.value">
|
||
<div class="kanban-header">{{ status.label }}</div>
|
||
<div class="task-card"
|
||
v-for="task in getTasksByStatus(status.value)"
|
||
:key="task.id"
|
||
:class="`priority-${task.priority}`"
|
||
@click="viewTask(task)">
|
||
<div class="task-title">{{ task.title }}</div>
|
||
<div class="task-meta">
|
||
<span>{{ task.assignee?.username || '未分配' }}</span>
|
||
<el-tag size="small" :type="getPriorityType(task.priority)">{{ task.priority }}</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 仪表板页面 -->
|
||
<div v-if="activeMenu === 'dashboard'">
|
||
<h3>仪表板</h3>
|
||
<el-row :gutter="20">
|
||
<el-col :span="6">
|
||
<el-card>
|
||
<el-statistic title="总项目数" :value="projects.length"></el-statistic>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card>
|
||
<el-statistic title="总任务数" :value="tasks.length"></el-statistic>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card>
|
||
<el-statistic title="已完成任务" :value="getCompletedTasks()"></el-statistic>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card>
|
||
<el-statistic title="我的任务" :value="getMyTasks()"></el-statistic>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 其他页面占位符 -->
|
||
<div v-if="activeMenu === 'sprints'">
|
||
<h3>迭代管理</h3>
|
||
<p>功能开发中...</p>
|
||
</div>
|
||
|
||
<div v-if="activeMenu === 'reports'">
|
||
<h3>报表统计</h3>
|
||
<p>功能开发中...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未登录状态 -->
|
||
<div v-else style="display: flex; justify-content: center; align-items: center; height: 80vh;">
|
||
<el-card style="width: 400px;">
|
||
<h2 style="text-align: center; margin-bottom: 30px;">Gitea Project Management</h2>
|
||
<p style="text-align: center; margin-bottom: 30px;">请登录以开始使用项目管理功能</p>
|
||
<el-button type="primary" @click.prevent="login" style="width: 100%;">使用 Gitea 登录</el-button>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建项目对话框 -->
|
||
<el-dialog v-model="showCreateProject" title="创建项目" width="600px">
|
||
<el-form :model="newProject" label-width="100px">
|
||
<el-form-item label="项目名称" required>
|
||
<el-input v-model="newProject.name" placeholder="请输入项目名称"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="项目描述">
|
||
<el-input type="textarea" v-model="newProject.description" placeholder="请输入项目描述"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="优先级">
|
||
<el-select v-model="newProject.priority" placeholder="选择优先级">
|
||
<el-option label="低" value="low"></el-option>
|
||
<el-option label="中" value="medium"></el-option>
|
||
<el-option label="高" value="high"></el-option>
|
||
<el-option label="紧急" value="critical"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreateProject = false">取消</el-button>
|
||
<el-button type="primary" @click="createProject">创建</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 创建任务对话框 -->
|
||
<el-dialog v-model="showCreateTask" title="创建任务" width="600px">
|
||
<el-form :model="newTask" label-width="100px">
|
||
<el-form-item label="任务标题" required>
|
||
<el-input v-model="newTask.title" placeholder="请输入任务标题"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="任务描述">
|
||
<el-input type="textarea" v-model="newTask.description" placeholder="请输入任务描述"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="优先级">
|
||
<el-select v-model="newTask.priority" placeholder="选择优先级">
|
||
<el-option label="低" value="low"></el-option>
|
||
<el-option label="中" value="medium"></el-option>
|
||
<el-option label="高" value="high"></el-option>
|
||
<el-option label="紧急" value="critical"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="任务类型">
|
||
<el-select v-model="newTask.task_type" placeholder="选择任务类型">
|
||
<el-option label="功能" value="feature"></el-option>
|
||
<el-option label="缺陷" value="bug"></el-option>
|
||
<el-option label="改进" value="improvement"></el-option>
|
||
<el-option label="研究" value="research"></el-option>
|
||
<el-option label="文档" value="documentation"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreateTask = false">取消</el-button>
|
||
<el-button type="primary" @click="createTask">创建</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
|
||
<script>
|
||
const { createApp, ref, reactive, onMounted } = Vue;
|
||
const { ElMessage } = ElementPlus;
|
||
|
||
createApp({
|
||
setup() {
|
||
const user = ref(null);
|
||
const activeMenu = ref('dashboard');
|
||
const projects = ref([]);
|
||
const tasks = ref([]);
|
||
const selectedProject = ref(null);
|
||
const showCreateProject = ref(false);
|
||
const showCreateTask = ref(false);
|
||
|
||
const newProject = reactive({
|
||
name: '',
|
||
description: '',
|
||
priority: 'medium'
|
||
});
|
||
|
||
const newTask = reactive({
|
||
title: '',
|
||
description: '',
|
||
priority: 'medium',
|
||
task_type: 'feature'
|
||
});
|
||
|
||
const taskStatuses = [
|
||
{ value: 'todo', label: '待办' },
|
||
{ value: 'in_progress', label: '进行中' },
|
||
{ value: 'review', label: '待审核' },
|
||
{ value: 'testing', label: '测试中' },
|
||
{ value: 'done', label: '已完成' }
|
||
];
|
||
|
||
// 设置 axios 默认配置
|
||
const token = localStorage.getItem('token');
|
||
if (token) {
|
||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
|
||
const login = async () => {
|
||
try {
|
||
const response = await axios.get('/api/v1/auth/login');
|
||
console.log('Login response:', response.data);
|
||
if (response.data && response.data.code === 200 && response.data.data && response.data.data.auth_url) {
|
||
console.log('Redirecting to:', response.data.data.auth_url);
|
||
window.location.href = response.data.data.auth_url;
|
||
} else {
|
||
ElMessage.error('获取登录URL失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Login error:', error);
|
||
ElMessage.error('登录失败');
|
||
}
|
||
};
|
||
|
||
const logout = () => {
|
||
localStorage.removeItem('token');
|
||
user.value = null;
|
||
delete axios.defaults.headers.common['Authorization'];
|
||
};
|
||
|
||
const loadCurrentUser = async () => {
|
||
try {
|
||
const response = await axios.get('/api/v1/users/me');
|
||
user.value = response.data.data;
|
||
} catch (error) {
|
||
logout();
|
||
}
|
||
};
|
||
|
||
const loadProjects = async () => {
|
||
try {
|
||
const response = await axios.get('/api/v1/projects');
|
||
projects.value = response.data.data || [];
|
||
} catch (error) {
|
||
ElMessage.error('加载项目失败');
|
||
}
|
||
};
|
||
|
||
const loadTasks = async () => {
|
||
if (!selectedProject.value) return;
|
||
try {
|
||
const response = await axios.get(`/api/v1/tasks?project_id=${selectedProject.value}`);
|
||
tasks.value = response.data.data || [];
|
||
} catch (error) {
|
||
ElMessage.error('加载任务失败');
|
||
}
|
||
};
|
||
|
||
const createProject = async () => {
|
||
try {
|
||
await axios.post('/api/v1/projects', newProject);
|
||
ElMessage.success('创建项目成功');
|
||
showCreateProject.value = false;
|
||
Object.assign(newProject, { name: '', description: '', priority: 'medium' });
|
||
loadProjects();
|
||
} catch (error) {
|
||
ElMessage.error('创建项目失败');
|
||
}
|
||
};
|
||
|
||
const createTask = async () => {
|
||
try {
|
||
const taskData = { ...newTask, project_id: selectedProject.value };
|
||
await axios.post('/api/v1/tasks', taskData);
|
||
ElMessage.success('创建任务成功');
|
||
showCreateTask.value = false;
|
||
Object.assign(newTask, { title: '', description: '', priority: 'medium', task_type: 'feature' });
|
||
loadTasks();
|
||
} catch (error) {
|
||
ElMessage.error('创建任务失败');
|
||
}
|
||
};
|
||
|
||
const handleMenuSelect = (index) => {
|
||
activeMenu.value = index;
|
||
if (index === 'projects') {
|
||
loadProjects();
|
||
}
|
||
};
|
||
|
||
const getTasksByStatus = (status) => {
|
||
return tasks.value.filter(task => task.status === status);
|
||
};
|
||
|
||
const getStatusType = (status) => {
|
||
const types = {
|
||
'planning': 'info',
|
||
'active': 'success',
|
||
'archived': 'warning'
|
||
};
|
||
return types[status] || '';
|
||
};
|
||
|
||
const getPriorityType = (priority) => {
|
||
const types = {
|
||
'low': 'success',
|
||
'medium': 'warning',
|
||
'high': 'danger',
|
||
'critical': 'danger'
|
||
};
|
||
return types[priority] || '';
|
||
};
|
||
|
||
const getCompletedTasks = () => {
|
||
return tasks.value.filter(task => task.status === 'done').length;
|
||
};
|
||
|
||
const getMyTasks = () => {
|
||
return tasks.value.filter(task => task.assignee?.id === user.value?.id).length;
|
||
};
|
||
|
||
const viewProject = (project) => {
|
||
ElMessage.info('项目详情功能开发中');
|
||
};
|
||
|
||
const editProject = (project) => {
|
||
ElMessage.info('编辑项目功能开发中');
|
||
};
|
||
|
||
const viewTask = (task) => {
|
||
ElMessage.info('任务详情功能开发中');
|
||
};
|
||
|
||
onMounted(() => {
|
||
// 检查是否有 token 参数(OAuth 回调)
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const token = urlParams.get('token');
|
||
if (token) {
|
||
localStorage.setItem('token', token);
|
||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
|
||
if (localStorage.getItem('token')) {
|
||
loadCurrentUser();
|
||
loadProjects();
|
||
}
|
||
});
|
||
|
||
return {
|
||
user,
|
||
activeMenu,
|
||
projects,
|
||
tasks,
|
||
selectedProject,
|
||
showCreateProject,
|
||
showCreateTask,
|
||
newProject,
|
||
newTask,
|
||
taskStatuses,
|
||
login,
|
||
logout,
|
||
loadTasks,
|
||
createProject,
|
||
createTask,
|
||
handleMenuSelect,
|
||
getTasksByStatus,
|
||
getStatusType,
|
||
getPriorityType,
|
||
getCompletedTasks,
|
||
getMyTasks,
|
||
viewProject,
|
||
editProject,
|
||
viewTask
|
||
};
|
||
}
|
||
}).use(ElementPlus).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html> |