feat(学习材料):多 URL 支持 + 网页标题自动抓取

TaskForm.vue:
- 学习材料输入从单行改为 textarea,每行一个链接

Study.vue:
- 改为迭代多 URL 展示,每行显示网页标题(通过 lpt-ai
  /fetch-title 获取)或回退到 URL 本身
- 选中不同任务时自动刷新标题

新增 src/utils/fetchTitle.ts:
- fetchTitle(url) / fetchTitles(urls) 工具函数
- 内存缓存避免重复请求
- 失败时回退到 URL,不阻塞页面

后端:
- V20260704_1 迁移:material_url varchar(255) → TEXT
This commit is contained in:
2026-07-04 22:37:01 +08:00
parent 5f7a5a55a4
commit a63942d105
3 changed files with 74 additions and 9 deletions
+37 -8
View File
@@ -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<string[]>([]);
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(() => {
<div class="detail-block">
<p class="label">学习材料</p>
<el-link
v-if="selectedTask.materialURL"
:href="selectedTask.materialURL"
target="_blank"
type="primary"
>
{{ selectedTask.materialURL }}
</el-link>
<div v-if="urlList.length > 0" class="material-list">
<div v-for="(url, i) in urlList" :key="i" class="material-item">
<el-link :href="url" target="_blank" type="success">
{{ urlTitles[i] || url }}
</el-link>
</div>
</div>
<p v-else class="empty-text">暂未填写材料地址</p>
</div>
@@ -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);