a63942d105
TaskForm.vue: - 学习材料输入从单行改为 textarea,每行一个链接 Study.vue: - 改为迭代多 URL 展示,每行显示网页标题(通过 lpt-ai /fetch-title 获取)或回退到 URL 本身 - 选中不同任务时自动刷新标题 新增 src/utils/fetchTitle.ts: - fetchTitle(url) / fetchTitles(urls) 工具函数 - 内存缓存避免重复请求 - 失败时回退到 URL,不阻塞页面 后端: - V20260704_1 迁移:material_url varchar(255) → TEXT
463 lines
12 KiB
Vue
463 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
import request from "@/utils/request";
|
|
import { ElMessage } from "element-plus";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import {
|
|
createTaskApplication,
|
|
deleteTaskApplication,
|
|
getTaskApplications,
|
|
updateTaskApplication,
|
|
type TaskApplication,
|
|
} from "@/api/tasks";
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
const taskId = route.params.taskId ? Number(route.params.taskId) : null;
|
|
const buttonName = route.meta.buttonText as string;
|
|
const isUpdateMode = computed(() => route.meta.action === "update");
|
|
|
|
const taskNum = ref("");
|
|
const taskName = ref("");
|
|
const taskDescription = ref("");
|
|
const materialURL = ref("");
|
|
const priority = ref({
|
|
urgency: 0,
|
|
importance: 0,
|
|
contentDifficulty: 0,
|
|
futureValue: 0,
|
|
subjectivePriority: 0,
|
|
});
|
|
|
|
const priorityOptions = [0, 1, 2, 3, 4, 5];
|
|
const applicationStatusOptions: { label: string; value: TaskApplication["status"] }[] = [
|
|
{ label: "待应用", value: "TODO" },
|
|
{ label: "进行中", value: "DOING" },
|
|
{ label: "已完成", value: "DONE" },
|
|
];
|
|
|
|
const applications = ref<TaskApplication[]>([]);
|
|
const applicationLoading = ref(false);
|
|
const applicationSaving = ref(false);
|
|
const editingApplicationId = ref<number | null>(null);
|
|
const applicationForm = ref<{
|
|
title: string;
|
|
description: string;
|
|
resourceUrl: string;
|
|
status: TaskApplication["status"];
|
|
}>({
|
|
title: "",
|
|
description: "",
|
|
resourceUrl: "",
|
|
status: "TODO",
|
|
});
|
|
|
|
const resetApplicationForm = () => {
|
|
editingApplicationId.value = null;
|
|
applicationForm.value = {
|
|
title: "",
|
|
description: "",
|
|
resourceUrl: "",
|
|
status: "TODO",
|
|
};
|
|
};
|
|
|
|
const loadApplications = async (targetTaskNum: string) => {
|
|
applicationLoading.value = true;
|
|
try {
|
|
const res = await getTaskApplications(targetTaskNum);
|
|
applications.value = res?.data || [];
|
|
} catch (error: any) {
|
|
applications.value = [];
|
|
ElMessage.error(error?.message || "应用场景加载失败");
|
|
} finally {
|
|
applicationLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const saveApplication = async () => {
|
|
if (!taskNum.value) {
|
|
ElMessage.warning("任务编号未加载,暂不能维护应用场景");
|
|
return;
|
|
}
|
|
if (!applicationForm.value.title.trim()) {
|
|
ElMessage.warning("应用项目标题不能为空");
|
|
return;
|
|
}
|
|
applicationSaving.value = true;
|
|
try {
|
|
const payload = {
|
|
title: applicationForm.value.title,
|
|
description: applicationForm.value.description,
|
|
resourceUrl: applicationForm.value.resourceUrl,
|
|
status: applicationForm.value.status,
|
|
};
|
|
if (editingApplicationId.value) {
|
|
await updateTaskApplication(editingApplicationId.value, payload);
|
|
ElMessage.success("应用场景已更新");
|
|
} else {
|
|
await createTaskApplication(taskNum.value, payload);
|
|
ElMessage.success("应用场景已添加");
|
|
}
|
|
resetApplicationForm();
|
|
await loadApplications(taskNum.value);
|
|
} finally {
|
|
applicationSaving.value = false;
|
|
}
|
|
};
|
|
|
|
const editApplication = (item: TaskApplication) => {
|
|
editingApplicationId.value = item.id;
|
|
applicationForm.value = {
|
|
title: item.title || "",
|
|
description: item.description || "",
|
|
resourceUrl: item.resourceUrl || "",
|
|
status: item.status || "TODO",
|
|
};
|
|
};
|
|
|
|
const removeApplication = async (item: TaskApplication) => {
|
|
await deleteTaskApplication(item.id);
|
|
ElMessage.success("应用场景已删除");
|
|
if (editingApplicationId.value === item.id) {
|
|
resetApplicationForm();
|
|
}
|
|
if (taskNum.value) {
|
|
await loadApplications(taskNum.value);
|
|
}
|
|
};
|
|
|
|
const createTask = () => {
|
|
request
|
|
.post("/tasks", {
|
|
taskName: taskName.value,
|
|
taskDescription: taskDescription.value,
|
|
materialURL: materialURL.value,
|
|
...priority.value,
|
|
})
|
|
.then((result) => {
|
|
if (result.code === 200) {
|
|
ElMessage.success("创建任务成功");
|
|
router.push({ name: "study" });
|
|
} else {
|
|
ElMessage.error("任务创建失败:" + result.message);
|
|
}
|
|
});
|
|
};
|
|
|
|
const loadTask = () => {
|
|
if (taskId !== null) {
|
|
request.get(`/tasks/${taskId}`, {}).then((result) => {
|
|
if (result.code === 200) {
|
|
const data = result.data;
|
|
taskNum.value = data.taskNum || "";
|
|
taskName.value = data.taskName;
|
|
taskDescription.value = data.taskDescription;
|
|
materialURL.value = data.materialURL || data.materialUrl || "";
|
|
priority.value = {
|
|
urgency: data.urgency,
|
|
importance: data.importance,
|
|
contentDifficulty: data.contentDifficulty,
|
|
futureValue: data.futureValue,
|
|
subjectivePriority: data.subjectivePriority,
|
|
};
|
|
if (taskNum.value) {
|
|
loadApplications(taskNum.value);
|
|
}
|
|
} else {
|
|
ElMessage.error(`任务加载失败:${result.message}`);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const updateTask = () => {
|
|
request
|
|
.put("/tasks/" + taskId, {
|
|
id: taskId,
|
|
taskName: taskName.value,
|
|
taskDescription: taskDescription.value,
|
|
materialURL: materialURL.value,
|
|
...priority.value,
|
|
})
|
|
.then((result) => {
|
|
if (result.code === 200) {
|
|
ElMessage.success("更新任务成功");
|
|
router.push({ name: "study" });
|
|
} else {
|
|
ElMessage.error("任务更新失败:" + result.message);
|
|
}
|
|
});
|
|
};
|
|
|
|
const cancelTask = () => {
|
|
ElMessage.info("已取消操作");
|
|
router.push({ name: "study" });
|
|
};
|
|
|
|
const submitTask = () => {
|
|
switch (route.name) {
|
|
case "add-task":
|
|
createTask();
|
|
break;
|
|
case "update-task":
|
|
updateTask();
|
|
break;
|
|
default:
|
|
ElMessage.error("未知操作类型");
|
|
}
|
|
};
|
|
|
|
const screenWidth = ref(window.innerWidth);
|
|
|
|
const updateWidth = () => {
|
|
screenWidth.value = window.innerWidth;
|
|
};
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("resize", updateWidth);
|
|
if (route.meta.action === "update" && taskId !== null) {
|
|
loadTask();
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => window.removeEventListener("resize", updateWidth));
|
|
|
|
const isMobile = computed(() => screenWidth.value <= 900);
|
|
</script>
|
|
|
|
<template>
|
|
<section class="task-form-page">
|
|
<el-form label-position="top" class="surface-card form-shell">
|
|
<div class="group">
|
|
<h3>基础信息</h3>
|
|
<el-form-item label="任务名称">
|
|
<el-input v-model="taskName" placeholder="例如:Vue3 组件通信实践" />
|
|
</el-form-item>
|
|
<el-form-item label="任务描述">
|
|
<el-input v-model="taskDescription" type="textarea" :rows="5" placeholder="请输入任务描述" />
|
|
</el-form-item>
|
|
<el-form-item label="学习材料地址">
|
|
<el-input v-model="materialURL" type="textarea" :rows="3" placeholder="每行输入一个链接 例如:https://vuejs.org/guide https://github.com/vuejs/core" />
|
|
</el-form-item>
|
|
</div>
|
|
|
|
<div class="group">
|
|
<h3>优先级维度</h3>
|
|
<div class="priority-grid" :class="{ mobile: isMobile }">
|
|
<el-form-item label="急迫性">
|
|
<el-select v-model="priority.urgency">
|
|
<el-option v-for="item in priorityOptions" :key="'u' + item" :label="item" :value="item" />
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="重要性">
|
|
<el-select v-model="priority.importance">
|
|
<el-option v-for="item in priorityOptions" :key="'i' + item" :label="item" :value="item" />
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="内容难度">
|
|
<el-select v-model="priority.contentDifficulty">
|
|
<el-option v-for="item in priorityOptions" :key="'d' + item" :label="item" :value="item" />
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="未来价值">
|
|
<el-select v-model="priority.futureValue">
|
|
<el-option v-for="item in priorityOptions" :key="'f' + item" :label="item" :value="item" />
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="主观优先级">
|
|
<el-select v-model="priority.subjectivePriority">
|
|
<el-option v-for="item in priorityOptions" :key="'s' + item" :label="item" :value="item" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group" v-if="isUpdateMode" v-loading="applicationLoading">
|
|
<h3>应用场景</h3>
|
|
<div class="application-list" v-if="applications.length > 0">
|
|
<div class="application-item" v-for="item in applications" :key="item.id">
|
|
<div class="application-main">
|
|
<strong>{{ item.title }}</strong>
|
|
<el-tag size="small" effect="plain">
|
|
{{ applicationStatusOptions.find(option => option.value === item.status)?.label || item.status }}
|
|
</el-tag>
|
|
</div>
|
|
<p v-if="item.description">{{ item.description }}</p>
|
|
<el-link v-if="item.resourceUrl" :href="item.resourceUrl" target="_blank" type="primary">
|
|
{{ item.resourceUrl }}
|
|
</el-link>
|
|
<div class="application-actions">
|
|
<el-button text type="primary" size="small" @click="editApplication(item)">编辑</el-button>
|
|
<el-button text type="danger" size="small" @click="removeApplication(item)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-else class="empty-text">暂未设置应用场景</p>
|
|
|
|
<div class="application-form">
|
|
<el-input v-model="applicationForm.title" placeholder="应用项目" />
|
|
<el-input
|
|
v-model="applicationForm.description"
|
|
type="textarea"
|
|
:rows="3"
|
|
placeholder="应用描述"
|
|
/>
|
|
<el-input v-model="applicationForm.resourceUrl" placeholder="相关链接" />
|
|
<div class="application-form-row">
|
|
<el-select v-model="applicationForm.status" placeholder="状态">
|
|
<el-option
|
|
v-for="option in applicationStatusOptions"
|
|
:key="option.value"
|
|
:label="option.label"
|
|
:value="option.value"
|
|
/>
|
|
</el-select>
|
|
<el-button type="success" :loading="applicationSaving" @click="saveApplication">
|
|
{{ editingApplicationId ? "更新" : "添加" }}
|
|
</el-button>
|
|
<el-button v-if="editingApplicationId" @click="resetApplicationForm">取消</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-row">
|
|
<el-button type="success" size="large" @click="submitTask">{{ buttonName }}</el-button>
|
|
<el-button size="large" @click="cancelTask">取消</el-button>
|
|
</div>
|
|
</el-form>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.task-form-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.form-shell {
|
|
padding: 22px;
|
|
}
|
|
|
|
.group + .group {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.group h3 {
|
|
margin: 0 0 12px;
|
|
color: var(--green-900);
|
|
}
|
|
|
|
.priority-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 0 14px;
|
|
}
|
|
|
|
.priority-grid.mobile {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.application-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.application-item {
|
|
padding: 12px;
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
background: #fbfefc;
|
|
}
|
|
|
|
.application-main {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.application-main strong {
|
|
min-width: 0;
|
|
color: var(--text-primary);
|
|
word-break: break-word;
|
|
}
|
|
|
|
.application-item p {
|
|
margin: 8px 0 0;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.application-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 4px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.application-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.application-form-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(160px, 220px) auto auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
justify-content: start;
|
|
}
|
|
|
|
.empty-text {
|
|
margin: 0 0 12px;
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.action-row {
|
|
margin-top: 8px;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.form-shell {
|
|
padding: 16px;
|
|
}
|
|
|
|
.action-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.action-row .el-button {
|
|
width: 100%;
|
|
margin: 0;
|
|
}
|
|
|
|
.application-main {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.application-form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.application-form-row .el-button {
|
|
width: 100%;
|
|
margin: 0;
|
|
}
|
|
}
|
|
</style>
|