Files
gitpm/web/templates/index.html
huxunan 885fad6c64 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
2025-09-22 14:53:53 +08:00

522 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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