From a81f83f27c1d7fd50e2a33dc23c2eb01f559db81 Mon Sep 17 00:00:00 2001 From: developer Date: Sun, 12 Jul 2026 13:48:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Study=20=E9=A1=B5=E9=9D=A2=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0=E6=9D=90=E6=96=99=20Markdown=20=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 接口 materialURL/materialUrl,移除 lastLearningStatus - 新增 Markdown 渲染函数:支持 [text](url) 和裸链接 - 学习材料区从 URL 列表改为 HTML 渲染展示 - 任务描述从只读 textarea 改为纯文本段落 - 优化 detail-block 分隔线、label 样式 - material-content 链接样式(绿色虚线+箭头图标) - 微调统计标签/数值字号 --- src/components/Study.vue | 114 ++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/src/components/Study.vue b/src/components/Study.vue index 6ca419d..a3bdfb3 100644 --- a/src/components/Study.vue +++ b/src/components/Study.vue @@ -19,8 +19,7 @@ import { getUrlTitle } from "@/utils/fetchTitle"; interface TaskItem { title: string; description: string; - materialURL: string; - lastLearningStatus: string; + materialUrl: string; priority: number | string; taskNum: string; taskId: number; @@ -52,18 +51,33 @@ const selectedTask = computed(() => tasks.value.find((item) => item.taskId === selectedTaskId.value) || null ); -// 多 URL 支持 -const urlList = computed(() => { - const raw = selectedTask.value?.materialURL || ""; - return raw.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.startsWith("http")); -}); -const urlTitles = ref([]); -async function loadUrlTitles() { - const urls = urlList.value; - if (urls.length === 0) { urlTitles.value = []; return; } - urlTitles.value = await fetchTitles(urls); +const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); +const escapeAttr = (s: string) => s.replace(/"/g, """).replace(/&/g, "&"); + +function renderMarkdown(raw: string): 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 tag = `${escapeHtml(clean)}`; + 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; } +const materialHtml = computed(() => renderMarkdown(selectedTask.value?.materialUrl || "")); + const formatEffectiveTime = (seconds: number): string => { if (!seconds || seconds <= 0) return "0分钟"; const hours = Math.floor(seconds / 3600); @@ -141,8 +155,7 @@ const fetchTasks = async () => { tasks.value = (res?.data?.records || []).map((record: any) => ({ title: record.taskName, description: record.taskDescription || "", - materialURL: record.materialURL || record.materialUrl || "", - lastLearningStatus: record.lastLearningStatus || "未开始", + materialUrl: record.materialUrl || "", priority: Number.isFinite(record.taskPriority) ? Math.round(record.taskPriority) : "-", taskNum: record.taskNum, taskId: record.id, @@ -324,7 +337,6 @@ onMounted(() => { {{ item.priority }} {{ item.title }}

-

状态:{{ item.lastLearningStatus }}

@@ -339,18 +351,12 @@ onMounted(() => {

任务描述

- +

{{ selectedTask.description || '暂无描述' }}

学习材料

-
-
- - {{ urlTitles[i] || url }} - -
-
+

暂未填写材料地址

@@ -578,11 +584,6 @@ onMounted(() => { justify-content: center; } -.task-meta { - margin: 4px 0 0; - font-size: 12px; - color: var(--text-secondary); -} .content-zone { display: grid; @@ -606,14 +607,33 @@ onMounted(() => { font-size: 22px; } +.description-text { + margin: 0; + font-size: 14px; + line-height: 1.7; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; +} + .detail-block { - margin-top: 16px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--border-soft); +} + +.detail-block:first-child { + margin-top: 0; + padding-top: 0; + border-top: none; } .label { - margin: 0 0 8px; + margin: 0 0 10px; color: var(--text-secondary); - font-size: 13px; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.5px; } .empty-text { @@ -621,19 +641,29 @@ onMounted(() => { color: var(--text-secondary); } -.material-list { - display: flex; - flex-direction: column; - gap: 6px; +.material-content { + font-size: 14px; + line-height: 1.8; + word-break: break-word; + color: var(--text-primary); } -.material-item { - padding: 4px 0; - word-break: break-all; +.material-content :deep(a) { + color: var(--green-600); + text-decoration: none; + border-bottom: 1px dashed var(--green-400); + transition: color 0.15s, border-color 0.15s; } -.material-item .el-link { - font-size: 13px; +.material-content :deep(a:hover) { + color: var(--green-800); + border-bottom-style: solid; +} + +.material-content :deep(a)::after { + content: " ↗"; + font-size: 11px; + opacity: 0.5; } .stats-grid { @@ -649,12 +679,12 @@ onMounted(() => { } .stat-label { - font-size: 12px; + font-size: 13px; color: var(--text-secondary); } .stat-value { - font-size: 14px; + font-size: 15px; font-weight: 600; color: var(--text-primary); }