From 032317da7badeb72a306f88432e827fafc29ff2e Mon Sep 17 00:00:00 2001 From: developer Date: Sun, 12 Jul 2026 13:49:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20TaskForm=20=E5=AD=A6=E4=B9=A0=E6=9D=90?= =?UTF-8?q?=E6=96=99=20Markdown=20=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - materialURL 重命名为 materialUrl - 新增预览/编辑切换按钮,实时 Markdown 渲染预览 - 保存前自动将裸 URL 转换为 [标题](url) 格式 - 新增 convertMaterialUrls 函数批量抓取链接标题 - 预览区样式与页面风格统一(绿色链接 + 箭头图标) - textarea 行数从 3 增至 6,placeholder 更新为 Markdown 示例 --- src/components/TaskForm.vue | 158 ++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/src/components/TaskForm.vue b/src/components/TaskForm.vue index cb4872a..541234d 100644 --- a/src/components/TaskForm.vue +++ b/src/components/TaskForm.vue @@ -22,7 +22,7 @@ const isUpdateMode = computed(() => route.meta.action === "update"); const taskNum = ref(""); const taskName = ref(""); const taskDescription = ref(""); -const materialURL = ref(""); +const materialUrl = ref(""); const priority = ref({ urgency: 0, importance: 0, @@ -139,12 +139,35 @@ const removeApplication = async (item: TaskApplication) => { } }; +const convertMaterialUrls = async () => { + const raw = materialUrl.value || ""; + if (!raw.trim()) return; + const bareUrls = new Set(); + raw.replace(/https?:\/\/[^\s)\u3001\uFF09\u300D\u300B<>]+/g, (url) => { + const clean = url.replace(/[.。,,;;!!??)】」』\]]+$/, ""); + if (!raw.includes(`](${clean})`)) bareUrls.add(clean); + return url; + }); + if (bareUrls.size === 0) return; + const titles: Record = {}; + const results = await Promise.all([...bareUrls].map(async (u) => ({ u, t: await getUrlTitle(u) }))); + results.forEach(({ u, t }) => { titles[u] = t; }); + let result = raw; + for (const u of bareUrls) { + if (titles[u] && titles[u] !== u) { + result = result.replace(u, `[${titles[u].replace(/]/g, "\\]")}](${u})`); + } + } + materialUrl.value = result; +}; + const createTask = () => { - request + convertMaterialUrls().then(() => { + request .post("/tasks", { taskName: taskName.value, taskDescription: taskDescription.value, - materialURL: materialURL.value, + materialUrl: materialUrl.value, ...priority.value, }) .then((result) => { @@ -155,6 +178,7 @@ const createTask = () => { ElMessage.error("任务创建失败:" + result.message); } }); + }); }; const loadTask = () => { @@ -165,7 +189,7 @@ const loadTask = () => { taskNum.value = data.taskNum || ""; taskName.value = data.taskName; taskDescription.value = data.taskDescription; - materialURL.value = data.materialURL || data.materialUrl || ""; + materialUrl.value = data.materialUrl || ""; priority.value = { urgency: data.urgency, importance: data.importance, @@ -184,12 +208,13 @@ const loadTask = () => { }; const updateTask = () => { - request + convertMaterialUrls().then(() => { + request .put("/tasks/" + taskId, { id: taskId, taskName: taskName.value, taskDescription: taskDescription.value, - materialURL: materialURL.value, + materialUrl: materialUrl.value, ...priority.value, }) .then((result) => { @@ -200,6 +225,7 @@ const updateTask = () => { ElMessage.error("任务更新失败:" + result.message); } }); + }); }; const cancelTask = () => { @@ -236,6 +262,57 @@ onMounted(() => { onUnmounted(() => window.removeEventListener("resize", updateWidth)); const isMobile = computed(() => screenWidth.value <= 900); + +// 学习材料 Markdown 预览 +const showPreview = ref(false); +const materialPreview = ref(""); +const previewLoading = ref(false); + +const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); +const escapeAttr = (s: string) => s.replace(/"/g, """).replace(/&/g, "&"); + +function renderMarkdown(raw: string, urlTitles: Record = {}): string { + if (!raw.trim()) return ""; + let html = raw; + const links: string[] = []; + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m: string, text: string, url: string) => { + const tag = `${escapeHtml(text)}`; + links.push(tag); + return `__LINK_${links.length - 1}__`; + }); + html = html.replace(/https?:\/\/[^\s)\u3001\uFF09\u300D\u300B<>]+/g, (url) => { + const clean = url.replace(/[.。,,;;!!??)】」』\]]+$/, ""); + const label = urlTitles[clean] || url; + const tag = `${escapeHtml(label)}`; + links.push(tag); + return `__LINK_${links.length - 1}__`; + }); + html = html.replace(/\n/g, "
"); + for (let i = 0; i < links.length; i++) { + html = html.replace(`__LINK_${i}__`, links[i]); + } + return html; +} + +async function previewMaterial() { + showPreview.value = true; + const raw = materialUrl.value || ""; + if (!raw.trim()) { materialPreview.value = ""; return; } + previewLoading.value = true; + try { + const titles: Record = {}; + const bareUrls = new Set(); + raw.replace(/https?:\/\/[^\s)\u3001\uFF09\u300D\u300B<>]+/g, (url) => { + bareUrls.add(url.replace(/[.。,,;;!!??)】」』\]]+$/, "")); + return url; + }); + const results = await Promise.all([...bareUrls].map(async (u) => ({ u, t: await getUrlTitle(u) }))); + results.forEach(({ u, t }) => { titles[u] = t; }); + materialPreview.value = renderMarkdown(raw, titles); + } finally { + previewLoading.value = false; + } +}