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
+152
View File
@@ -0,0 +1,152 @@
<template>
<el-dialog :title="isEdit ? '编辑任务' : '新增任务'" v-model="visible" width="600px" @close="onClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" />
</el-form-item>
<el-form-item label="里程碑" prop="milestone_id">
<el-select v-model="form.milestone_id" placeholder="请选择">
<el-option v-for="m in milestones" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="assignee_id">
<el-input v-model="form.assignee_id" placeholder="用户ID" />
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择">
<el-option v-for="p in priorities" :key="p" :label="p" :value="p" />
</el-select>
</el-form-item>
<el-form-item label="截止日期" prop="due_date">
<el-date-picker v-model="form.due_date" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择">
<el-option v-for="s in statuses" :key="s" :label="s" :value="s" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { createTask, updateTask } from "../api/tasks";
import { useStudyStore } from "../store/study";
interface Props {
modelValue: boolean;
task?: Record<string, any>;
milestones: any[];
}
const props = defineProps<Props>();
const emit = defineEmits(["update:modelValue", "success"]);
const formRef = ref<FormInstance>();
const study = useStudyStore();
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v),
});
const priorities = ["LOW", "MEDIUM", "HIGH"];
const statuses = ["TODO", "DOING", "DONE", "BLOCKED"];
const form = reactive({
title: "",
description: "",
milestone_id: "",
assignee_id: "",
priority: "MEDIUM",
due_date: "",
status: "TODO",
});
const rules: FormRules = {
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
priority: [{ required: true, message: "请选择优先级", trigger: "change" }],
status: [{ required: true, message: "请选择状态", trigger: "change" }],
};
const submitting = ref(false);
const isEdit = computed(() => !!props.task);
watch(
() => props.task,
(val) => {
if (val) {
form.title = val.title || "";
form.description = val.description || "";
form.milestone_id = val.milestone_id || "";
form.assignee_id = val.assignee_id || "";
form.priority = val.priority || "MEDIUM";
form.due_date = val.due_date || "";
form.status = val.status || "TODO";
} else {
form.title = "";
form.description = "";
form.milestone_id = "";
form.assignee_id = "";
form.priority = "MEDIUM";
form.due_date = "";
form.status = "TODO";
}
},
{ immediate: true }
);
const onClose = () => {
visible.value = false;
};
const onSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) return;
if (!study.currentStudy) {
ElMessage.error("未选择项目");
return;
}
submitting.value = true;
try {
const payload: Record<string, any> = {};
const uuidLike = /^[0-9a-fA-F-]{36}$/;
Object.entries(form).forEach(([k, v]) => {
if (v !== "" && v !== null && v !== undefined) {
payload[k] = v;
}
});
if (payload.assignee_id && !uuidLike.test(String(payload.assignee_id))) {
ElMessage.warning("负责人ID 需为有效用户ID,已忽略该字段");
delete payload.assignee_id;
}
if (payload.milestone_id && !uuidLike.test(String(payload.milestone_id))) {
ElMessage.warning("里程碑无效,已忽略该字段");
delete payload.milestone_id;
}
if (isEdit.value && props.task) {
await updateTask(study.currentStudy.id, props.task.id, payload);
} else {
await createTask(study.currentStudy.id, payload);
}
ElMessage.success("提交成功");
emit("success");
onClose();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "提交失败");
} finally {
submitting.value = false;
}
});
};
</script>
+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>