diff --git a/src/components/StartTask.vue b/src/components/StartTask.vue index 0c4c073..9d2f1fb 100644 --- a/src/components/StartTask.vue +++ b/src/components/StartTask.vue @@ -16,6 +16,7 @@ import { getFragmentsBySession, updateFragments } from "@/api/reportFragments"; import { getExpectation, getReportDraft, getTaskFragments, getTaskReports, upsertExpectation } from "@/api/studySessions"; import router from "@/router"; import MarkdownRenderer from "@/components/MarkdownRenderer.vue"; +import { renderMarkdown } from "@/utils/markdown"; const { fragmentsDialogVisible, @@ -151,28 +152,7 @@ const taskInfo = ref({ const editingMaterial = ref(false); const editMaterialContent = ref(""); const savingMaterial = ref(false); -const materialHtml = computed(() => { - const raw = taskInfo.value.materialUrl || ""; - if (!raw.trim()) return ""; - let html = raw; - const links: string[] = []; - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m: string, text: string, url: string) => { - const tag = `${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 = `${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(taskInfo.value.materialUrl || "")); function startEditMaterial() { editMaterialContent.value = taskInfo.value.materialUrl; diff --git a/src/components/Study.vue b/src/components/Study.vue index d07459f..94361d8 100644 --- a/src/components/Study.vue +++ b/src/components/Study.vue @@ -5,6 +5,8 @@ import router from "@/router"; import { ElMessage, ElMessageBox } from "element-plus"; import { getReviewTaskStatsByTask } from "@/api/review"; import { getActiveSession } from "@/api/studySessions"; +import { renderMarkdown } from "@/utils/markdown"; +import { applicationStatusOptions } from "@/utils/taskApplication"; import { getPriorityWeights, @@ -44,41 +46,10 @@ const taskApplications = ref([]); const applicationsLoading = ref(false); const appUrlTitles = ref>({}); -const applicationStatusOptions: { label: string; value: TaskApplication["status"] }[] = [ - { label: "待应用", value: "TODO" }, - { label: "进行中", value: "DOING" }, - { label: "已完成", value: "DONE" }, -]; - const selectedTask = computed(() => tasks.value.find((item) => item.taskId === selectedTaskId.value) || null ); -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 => { diff --git a/src/components/TaskForm.vue b/src/components/TaskForm.vue index 3001bb9..825af50 100644 --- a/src/components/TaskForm.vue +++ b/src/components/TaskForm.vue @@ -11,6 +11,8 @@ import { type TaskApplication, } from "@/api/tasks"; import { getUrlTitle } from "@/utils/fetchTitle"; +import { renderMarkdown } from "@/utils/markdown"; +import { applicationStatusOptions } from "@/utils/taskApplication"; const router = useRouter(); const route = useRoute(); @@ -32,11 +34,6 @@ const priority = ref({ }); 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([]); const applicationLoading = ref(false); @@ -277,36 +274,6 @@ const isMobile = computed(() => screenWidth.value <= 900); const showPreview = ref(false); const materialPreview = ref(""); -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; - // 1) [text](url) → <a> 占位,防止后续裸 URL 匹配进属性里 - 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}__`; - }); - // 2) 裸 URL → <a> 占位 - 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}__`; - }); - // 3) 换行 - html = html.replace(/\n/g, "
"); - // 4) 还原占位 - 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 || ""; diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts new file mode 100644 index 0000000..29d9a80 --- /dev/null +++ b/src/utils/markdown.ts @@ -0,0 +1,27 @@ +export const escapeHtml = (s: string) => + s.replace(/&/g, "&").replace(//g, ">"); + +export const escapeAttr = (s: string) => s.replace(/"/g, """).replace(/&/g, "&"); + +export 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] || clean; + 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; +} diff --git a/src/utils/taskApplication.ts b/src/utils/taskApplication.ts new file mode 100644 index 0000000..a3da7e9 --- /dev/null +++ b/src/utils/taskApplication.ts @@ -0,0 +1,7 @@ +import type { TaskApplication } from "@/api/tasks"; + +export const applicationStatusOptions: { label: string; value: TaskApplication["status"] }[] = [ + { label: "待应用", value: "TODO" }, + { label: "进行中", value: "DOING" }, + { label: "已完成", value: "DONE" }, +];