refactor: 收敛重复的 Markdown 渲染与应用状态常量
This commit is contained in:
@@ -16,6 +16,7 @@ import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
|
|||||||
import { getExpectation, getReportDraft, getTaskFragments, getTaskReports, upsertExpectation } from "@/api/studySessions";
|
import { getExpectation, getReportDraft, getTaskFragments, getTaskReports, upsertExpectation } from "@/api/studySessions";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
||||||
|
import { renderMarkdown } from "@/utils/markdown";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fragmentsDialogVisible,
|
fragmentsDialogVisible,
|
||||||
@@ -151,28 +152,7 @@ const taskInfo = ref({
|
|||||||
const editingMaterial = ref(false);
|
const editingMaterial = ref(false);
|
||||||
const editMaterialContent = ref("");
|
const editMaterialContent = ref("");
|
||||||
const savingMaterial = ref(false);
|
const savingMaterial = ref(false);
|
||||||
const materialHtml = computed(() => {
|
const materialHtml = computed(() => renderMarkdown(taskInfo.value.materialUrl || ""));
|
||||||
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 = `<a href="${url.replace(/"/g, """)}" target="_blank" rel="noopener">${text}</a>`;
|
|
||||||
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 = `<a href="${clean.replace(/"/g, """)}" target="_blank" rel="noopener">${clean}</a>`;
|
|
||||||
links.push(tag);
|
|
||||||
return `__LINK_${links.length - 1}__`;
|
|
||||||
});
|
|
||||||
html = html.replace(/\n/g, "<br>");
|
|
||||||
for (let i = 0; i < links.length; i++) {
|
|
||||||
html = html.replace(`__LINK_${i}__`, links[i]);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
});
|
|
||||||
|
|
||||||
function startEditMaterial() {
|
function startEditMaterial() {
|
||||||
editMaterialContent.value = taskInfo.value.materialUrl;
|
editMaterialContent.value = taskInfo.value.materialUrl;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import router from "@/router";
|
|||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { getReviewTaskStatsByTask } from "@/api/review";
|
import { getReviewTaskStatsByTask } from "@/api/review";
|
||||||
import { getActiveSession } from "@/api/studySessions";
|
import { getActiveSession } from "@/api/studySessions";
|
||||||
|
import { renderMarkdown } from "@/utils/markdown";
|
||||||
|
import { applicationStatusOptions } from "@/utils/taskApplication";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getPriorityWeights,
|
getPriorityWeights,
|
||||||
@@ -44,41 +46,10 @@ const taskApplications = ref<TaskApplication[]>([]);
|
|||||||
const applicationsLoading = ref(false);
|
const applicationsLoading = ref(false);
|
||||||
const appUrlTitles = ref<Record<number, string>>({});
|
const appUrlTitles = ref<Record<number, string>>({});
|
||||||
|
|
||||||
const applicationStatusOptions: { label: string; value: TaskApplication["status"] }[] = [
|
|
||||||
{ label: "待应用", value: "TODO" },
|
|
||||||
{ label: "进行中", value: "DOING" },
|
|
||||||
{ label: "已完成", value: "DONE" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedTask = computed(() =>
|
const selectedTask = computed(() =>
|
||||||
tasks.value.find((item) => item.taskId === selectedTaskId.value) || null
|
tasks.value.find((item) => item.taskId === selectedTaskId.value) || null
|
||||||
);
|
);
|
||||||
|
|
||||||
const escapeHtml = (s: string) => s.replace(/&/g, "&").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 = `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${escapeHtml(text)}</a>`;
|
|
||||||
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 = `<a href="${escapeAttr(clean)}" target="_blank" rel="noopener">${escapeHtml(clean)}</a>`;
|
|
||||||
links.push(tag);
|
|
||||||
return `__LINK_${links.length - 1}__`;
|
|
||||||
});
|
|
||||||
html = html.replace(/\n/g, "<br>");
|
|
||||||
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 materialHtml = computed(() => renderMarkdown(selectedTask.value?.materialUrl || ""));
|
||||||
|
|
||||||
const formatEffectiveTime = (seconds: number): string => {
|
const formatEffectiveTime = (seconds: number): string => {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
type TaskApplication,
|
type TaskApplication,
|
||||||
} from "@/api/tasks";
|
} from "@/api/tasks";
|
||||||
import { getUrlTitle } from "@/utils/fetchTitle";
|
import { getUrlTitle } from "@/utils/fetchTitle";
|
||||||
|
import { renderMarkdown } from "@/utils/markdown";
|
||||||
|
import { applicationStatusOptions } from "@/utils/taskApplication";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -32,11 +34,6 @@ const priority = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const priorityOptions = [0, 1, 2, 3, 4, 5];
|
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<TaskApplication[]>([]);
|
const applications = ref<TaskApplication[]>([]);
|
||||||
const applicationLoading = ref(false);
|
const applicationLoading = ref(false);
|
||||||
@@ -277,36 +274,6 @@ const isMobile = computed(() => screenWidth.value <= 900);
|
|||||||
const showPreview = ref(false);
|
const showPreview = ref(false);
|
||||||
const materialPreview = ref("");
|
const materialPreview = ref("");
|
||||||
|
|
||||||
const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
const escapeAttr = (s: string) => s.replace(/"/g, """).replace(/&/g, "&");
|
|
||||||
|
|
||||||
function renderMarkdown(raw: string, urlTitles: Record<string, string> = {}): 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 = `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${escapeHtml(text)}</a>`;
|
|
||||||
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 = `<a href="${escapeAttr(clean)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`;
|
|
||||||
links.push(tag);
|
|
||||||
return `__LINK_${links.length - 1}__`;
|
|
||||||
});
|
|
||||||
// 3) 换行
|
|
||||||
html = html.replace(/\n/g, "<br>");
|
|
||||||
// 4) 还原占位
|
|
||||||
for (let i = 0; i < links.length; i++) {
|
|
||||||
html = html.replace(`__LINK_${i}__`, links[i]);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function previewMaterial() {
|
async function previewMaterial() {
|
||||||
showPreview.value = true;
|
showPreview.value = true;
|
||||||
const raw = materialUrl.value || "";
|
const raw = materialUrl.value || "";
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export const escapeHtml = (s: string) =>
|
||||||
|
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
|
export const escapeAttr = (s: string) => s.replace(/"/g, """).replace(/&/g, "&");
|
||||||
|
|
||||||
|
export function renderMarkdown(raw: string, urlTitles: Record<string, string> = {}): string {
|
||||||
|
if (!raw.trim()) return "";
|
||||||
|
let html = raw;
|
||||||
|
const links: string[] = [];
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m: string, text: string, url: string) => {
|
||||||
|
const tag = `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${escapeHtml(text)}</a>`;
|
||||||
|
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 = `<a href="${escapeAttr(clean)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`;
|
||||||
|
links.push(tag);
|
||||||
|
return `__LINK_${links.length - 1}__`;
|
||||||
|
});
|
||||||
|
html = html.replace(/\n/g, "<br>");
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
html = html.replace(`__LINK_${i}__`, links[i]);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@@ -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" },
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user