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" },
+];