Step F4:任务(Task)& 里程碑(Milestone)管理页面

This commit is contained in:
Cheng Zhou
2025-12-16 21:13:11 +08:00
parent ed25d93fcd
commit 582655a66d
2 changed files with 310 additions and 2 deletions
+158 -2
View File
@@ -1,13 +1,169 @@
<template>
<div class="page">
<el-card><h2>任务</h2></el-card>
<el-card class="mb-12">
<div class="filters">
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadTasks">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
</el-select>
<el-select v-model="filters.milestone_id" placeholder="里程碑" clearable @change="loadTasks">
<el-option v-for="m in milestones" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
<el-input v-model="filters.assignee_id" placeholder="负责人ID" style="width: 180px" @change="loadTasks" />
<div class="spacer" />
<el-button type="primary" v-if="canEdit" @click="openCreate">新增任务</el-button>
</div>
</el-card>
<el-card>
<el-table :data="tasks" v-loading="loading" style="width: 100%">
<el-table-column prop="title" label="标题" />
<el-table-column prop="milestone_id" label="里程碑" />
<el-table-column prop="assignee_id" label="负责人" width="160" />
<el-table-column prop="priority" label="优先级" width="100" />
<el-table-column prop="status" label="状态" width="120" />
<el-table-column prop="due_date" label="截止日期" width="140" />
<el-table-column label="操作" width="120" v-if="canEdit">
<template #default="scope">
<el-button type="primary" link size="small" @click="openEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
layout="prev, pager, next"
:page-size="pageSize"
:current-page="page"
:total="total"
@current-change="onPageChange"
/>
</el-card>
<TaskForm v-model="showForm" :task="editingTask" :milestones="milestones" @success="loadTasks" />
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { ElMessage } from "element-plus";
import { fetchTasks } from "../api/tasks";
import { fetchMilestones } from "../api/milestones";
import { useStudyStore } from "../store/study";
import { useAuthStore } from "../store/auth";
import TaskForm from "../components/TaskForm.vue";
const study = useStudyStore();
const auth = useAuthStore();
const tasks = ref<any[]>([]);
const milestones = ref<any[]>([]);
const loading = ref(false);
const total = ref(0);
const page = ref(1);
const pageSize = 10;
const statuses = ["TODO", "DOING", "DONE", "BLOCKED"];
const filters = ref({
status: "",
milestone_id: "",
assignee_id: "",
});
const showForm = ref(false);
const editingTask = ref<any | null>(null);
const canEdit = computed(() => {
const role = auth.user?.role;
// 简化:ADMIN/PM 可编辑
return role === "ADMIN" || role === "PM";
});
const loadMilestones = async () => {
if (!study.currentStudy) return;
try {
const { data } = await fetchMilestones(study.currentStudy.id);
milestones.value = data.items || data; // 兼容
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "里程碑加载失败");
}
};
const loadTasks = async () => {
if (!study.currentStudy) return;
loading.value = true;
try {
const params: Record<string, any> = {
skip: (page.value - 1) * pageSize,
limit: pageSize,
};
if (filters.value.status) params.status = filters.value.status;
if (filters.value.milestone_id) params.milestone_id = filters.value.milestone_id;
if (filters.value.assignee_id) params.assignee_id = filters.value.assignee_id;
const { data } = await fetchTasks(study.currentStudy.id, params);
if (Array.isArray(data)) {
tasks.value = data;
total.value = data.length;
} else {
tasks.value = data.items || [];
total.value = data.total || tasks.value.length;
}
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "任务加载失败");
} finally {
loading.value = false;
}
};
const onPageChange = (p: number) => {
page.value = p;
loadTasks();
};
const openCreate = () => {
editingTask.value = null;
showForm.value = true;
};
const openEdit = (row: any) => {
editingTask.value = row;
showForm.value = true;
};
const ensureUser = async () => {
if (!auth.user && auth.token) {
try {
await auth.fetchMe();
} catch {
// ignore, canEdit 将保持 false
}
}
};
onMounted(async () => {
await ensureUser();
loadMilestones();
loadTasks();
});
</script>
<style scoped>
.page {
padding: 16px;
}
.filters {
display: flex;
gap: 8px;
align-items: center;
}
.spacer {
flex: 1;
}
.pagination {
margin-top: 12px;
text-align: right;
}
.mb-12 {
margin-bottom: 12px;
}
</style>