feat(mobile): learning page APP-ification (fold long content, batch 2 of app-like)

学习中心页(全站最长,实测 5452px)按「折叠不删减」治理:
- 岗位 SOP 长答案默认收起为 140 字预览+「展开全文/收起全文」
  (短答案不出现开关;切换主题自动复位)
- 学习资料段落(snippet)默认只显示标题行,点标题展开/收起正文;
  超长文档标题两行截断
- 错题本默认 3 条,「展开全部 N 条」可切换(随归因筛选联动)

实测页面高度 5452 → 3834(-30%),其余为八个功能面板固有高度,
继续压缩需删减面板,违背不影响功能原则,不做。
本地+生产复核:高度、段落默认收起、点击展开均通过。
发布备份 h5-before-$TS。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 09:44:30 +08:00
co-authored by Claude Fable 5
parent 6935476e3f
commit dd3f39ef44
+85 -5
View File
@@ -98,7 +98,7 @@
</button>
</view>
<view v-if="visibleMistakes.length" class="mistake-list">
<view v-for="item in visibleMistakes" :key="item.id" class="mistake-row">
<view v-for="item in displayedMistakes" :key="item.id" class="mistake-row">
<view class="mistake-copy">
<view class="mistake-meta">
<text class="mistake-cause">{{ item.cause }}</text>
@@ -117,6 +117,9 @@
</button>
<text v-else-if="item.retryAssignmentId" class="mistake-completed">已完成</text>
</view>
<button v-if="visibleMistakes.length > 3" class="fold-toggle" @click="showAllMistakes = !showAllMistakes">
{{ showAllMistakes ? '收起 ▲' : `展开全部 ${visibleMistakes.length} 条 ▼` }}
</button>
</view>
<view v-else-if="!mistakeLoading" class="empty">
{{ mistakes.length ? '当前归因筛选暂无错题' : '完成训练后,低分和红线回合会自动出现在这里' }}
@@ -224,16 +227,26 @@
{{ topic }}
</button>
</view>
<text v-if="knowledgeResult?.answer" class="answer-text">{{ knowledgeResult.answer }}</text>
<text v-if="knowledgeResult?.answer" class="answer-text">{{ displayedAnswer }}</text>
<button v-if="answerIsLong" class="fold-toggle" @click="showFullAnswer = !showFullAnswer">
{{ showFullAnswer ? '收起全文 ▲' : '展开全文 ▼' }}
</button>
<view v-if="knowledgeResult?.keyPoints?.length" class="keypoints">
<text v-for="(point, index) in knowledgeResult.keyPoints" :key="point" class="keypoint">
{{ index + 1 }}. {{ cleanOrderedText(point) }}
</text>
</view>
<view v-if="knowledgeResult?.snippets?.length" class="snippet-list">
<view v-for="(snippet, index) in knowledgeResult.snippets?.slice(0, 3)" :key="`${snippet.docTitle}-${index}`" class="snippet-row">
<text class="snippet-title">{{ snippet.docTitle || '知识片段' }}</text>
<text class="snippet-content">{{ snippetExcerpt(snippet.content) }}</text>
<view
v-for="(snippet, index) in knowledgeResult.snippets?.slice(0, 3)"
:key="`${snippet.docTitle}-${index}`"
class="snippet-row"
>
<button class="snippet-head" @click="toggleSnippet(index)">
<text class="snippet-title">{{ snippet.docTitle || '知识片段' }}</text>
<text class="snippet-arrow">{{ expandedSnippets[index] ? '▲' : '▼' }}</text>
</button>
<text v-if="expandedSnippets[index]" class="snippet-content">{{ snippetExcerpt(snippet.content) }}</text>
</view>
</view>
<view v-else-if="!knowledgeLoading" class="empty">知识库暂未命中可引用内容</view>
@@ -348,6 +361,22 @@ const visibleMistakes = computed(() => activeMistakeCause.value === '全部'
? mistakes.value
: mistakes.value.filter((item) => item.cause === activeMistakeCause.value));
/* APP-like folding: long content collapsed by default, nothing removed. */
const ANSWER_PREVIEW_CHARS = 140;
const showAllMistakes = ref(false);
const showFullAnswer = ref(false);
const expandedSnippets = ref<Record<number, boolean>>({});
const displayedMistakes = computed(() => (showAllMistakes.value ? visibleMistakes.value : visibleMistakes.value.slice(0, 3)));
const answerIsLong = computed(() => (knowledgeResult.value?.answer || '').length > ANSWER_PREVIEW_CHARS);
const displayedAnswer = computed(() => {
const answer = knowledgeResult.value?.answer || '';
if (showFullAnswer.value || answer.length <= ANSWER_PREVIEW_CHARS) return answer;
return `${answer.slice(0, ANSWER_PREVIEW_CHARS)}…`;
});
const toggleSnippet = (index: number) => {
expandedSnippets.value = { ...expandedSnippets.value, [index]: !expandedSnippets.value[index] };
};
const qualificationStatusLabel = computed(() => {
switch (qualification.value?.status) {
case 'PASSED': return '已通过';
@@ -554,6 +583,8 @@ const loadKnowledge = async (topic: string, request: RequestScopeTicket = reques
const nextKnowledge = await searchKnowledge(topic);
if (!requests.isCurrent(request)) return;
knowledgeResult.value = nextKnowledge;
showFullAnswer.value = false;
expandedSnippets.value = {};
} catch (error) {
if (!requests.isCurrent(request)) return;
knowledgeResult.value = null;
@@ -1157,4 +1188,53 @@ onUnload(invalidateRequests);
.action-button {
width: 100%;
}
.fold-toggle {
display: block;
width: 100%;
margin: 10px 0 0;
padding: 8px 0;
border: none;
border-top: 1px dashed #eceff2;
background: transparent;
color: #6b7480;
font-size: 13px;
text-align: center;
line-height: 1.5;
}
.fold-toggle::after {
border: none;
}
.snippet-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
text-align: left;
line-height: 1.4;
}
.snippet-head::after {
border: none;
}
.snippet-arrow {
flex-shrink: 0;
color: #98a1ad;
font-size: 12px;
}
.snippet-head .snippet-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>