diff --git a/src/components/Study.vue b/src/components/Study.vue index c5efcb0..89dfdba 100644 --- a/src/components/Study.vue +++ b/src/components/Study.vue @@ -12,6 +12,7 @@ import { type PriorityWeights, type TaskApplication, } from "@/api/tasks"; +import { fetchTitles } from "@/utils/fetchTitle"; interface TaskItem { title: string; @@ -48,6 +49,18 @@ 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 formatEffectiveTime = (seconds: number): string => { if (!seconds || seconds <= 0) return "0分钟"; const hours = Math.floor(seconds / 3600); @@ -154,8 +167,10 @@ watch(selectedTaskId, (newId) => { if (task) { loadTaskStats(task.taskNum); loadTaskApplications(task.taskNum); + loadUrlTitles(); } else { taskApplications.value = []; + urlTitles.value = []; } }); @@ -298,14 +313,13 @@ onMounted(() => {

学习材料

- - {{ selectedTask.materialURL }} - +
+
+ + {{ urlTitles[i] || url }} + +
+

暂未填写材料地址

@@ -559,6 +573,21 @@ onMounted(() => { color: var(--text-secondary); } +.material-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.material-item { + padding: 4px 0; + word-break: break-all; +} + +.material-item .el-link { + font-size: 13px; +} + .stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/src/components/TaskForm.vue b/src/components/TaskForm.vue index c3efc70..a04b375 100644 --- a/src/components/TaskForm.vue +++ b/src/components/TaskForm.vue @@ -239,7 +239,7 @@ const isMobile = computed(() => screenWidth.value <= 900); - + diff --git a/src/utils/fetchTitle.ts b/src/utils/fetchTitle.ts new file mode 100644 index 0000000..85a5b09 --- /dev/null +++ b/src/utils/fetchTitle.ts @@ -0,0 +1,36 @@ +/** + * 网页标题抓取工具。 + * 调用 lpt-ai 的 /fetch-title 接口获取 URL 对应的 标签内容。 + * + * 设计要点: + * - 内存缓存避免同页面重复请求 + * - 失败时回退到 URL 本身 + * - 仅在 lpt-ai 运行时可用 + */ + +const LPT_AI_URL = "http://localhost:5199"; + +const titleCache: Record<string, string> = {}; + +export async function fetchTitle(url: string): Promise<string> { + if (titleCache[url]) return titleCache[url]; + try { + const res = await fetch(`${LPT_AI_URL}/fetch-title`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + signal: AbortSignal.timeout(12_000), + }); + if (!res.ok) return url; + const data = await res.json(); + titleCache[url] = data.title || url; + return titleCache[url]; + } catch { + return url; + } +} + +/** 批量获取标题 */ +export async function fetchTitles(urls: string[]): Promise<string[]> { + return Promise.all(urls.map((url) => fetchTitle(url))); +}