From 96d574b99f3ca88214d74f4c53c96237c0288841 Mon Sep 17 00:00:00 2001
From: cat-shark <1716967236@qq.com>
Date: Sun, 2 Aug 2026 11:36:18 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E6=95=9B=E9=87=8D?=
=?UTF-8?q?=E5=A4=8D=E7=9A=84=20Markdown=20=E6=B8=B2=E6=9F=93=E4=B8=8E?=
=?UTF-8?q?=E5=BA=94=E7=94=A8=E7=8A=B6=E6=80=81=E5=B8=B8=E9=87=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/StartTask.vue | 24 ++---------------------
src/components/Study.vue | 33 ++------------------------------
src/components/TaskForm.vue | 37 ++----------------------------------
src/utils/markdown.ts | 27 ++++++++++++++++++++++++++
src/utils/taskApplication.ts | 7 +++++++
5 files changed, 40 insertions(+), 88 deletions(-)
create mode 100644 src/utils/markdown.ts
create mode 100644 src/utils/taskApplication.ts
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" },
+];