Initial commit: Gitea Project Management System

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
This commit is contained in:
huxunan
2025-09-22 14:53:53 +08:00
commit 885fad6c64
33 changed files with 4128 additions and 0 deletions

522
web/templates/index.html Normal file
View File

@@ -0,0 +1,522 @@
<!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>