清理: 移除代码注释中的走查编号前缀(UX/B/SW/CR/ID/P0-P2-26xxxx,共102处)
This commit is contained in:
@@ -265,7 +265,7 @@ async function confirmClearChat() {
|
||||
* 同属真并发语义——不弹「中断并新建」确认(与并行矛盾)。对齐 switchConversation:89-91
|
||||
* 「允许生成中切换:后台继续生成,事件按 conversation_id 路由」。
|
||||
*
|
||||
* 历史:B-260615-10 单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
|
||||
* 历史:单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
|
||||
*/
|
||||
async function confirmNewConversation() {
|
||||
try {
|
||||
@@ -476,7 +476,7 @@ onBeforeUnmount(() => {
|
||||
store.stopListener()
|
||||
// F-15 阶段2: 释放上下文管理事件监听器
|
||||
store.stopContextListener()
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } // B-260616-12
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } //
|
||||
if (_unlistenAutoApproved) { _unlistenAutoApproved(); _unlistenAutoApproved = null } // AE-2025-04
|
||||
if (_unlistenAiToast) { _unlistenAiToast(); _unlistenAiToast = null } // P1-e-d
|
||||
if (_unlistenSkillInjected) { _unlistenSkillInjected(); _unlistenSkillInjected = null } // 技能注入反馈 toast
|
||||
@@ -802,7 +802,7 @@ async function initContextEventListeners(): Promise<void> {
|
||||
}
|
||||
.ai-queue-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; }
|
||||
.ai-queue-x:hover { color: var(--df-danger); }
|
||||
/* UX-260616-06 队列项编辑/立即发送按钮 + 编辑态 input */
|
||||
/* 队列项编辑/立即发送按钮 + 编辑态 input */
|
||||
.ai-queue-btn {
|
||||
background: none; border: none; color: var(--df-text-dim); cursor: pointer;
|
||||
font-size: 11px; line-height: 1; padding: 0 4px; flex-shrink: 0;
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
@result="answerConfirm"
|
||||
/>
|
||||
|
||||
<!-- UX-260617-14:审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
|
||||
<!-- 审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
|
||||
<Transition name="ai-tool-toast">
|
||||
<div v-if="approveToast.visible" class="ai-tool-toast ai-tool-toast--warning">{{ approveToast.msg }}</div>
|
||||
</Transition>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
@expand-content="toggleExpand"
|
||||
@approve="(e) => emit('approve', e)"
|
||||
@install-obscura="() => emit('install-obscura')"
|
||||
:class="{ 'ai-tool-card--group-hidden': group.calls.length >= 2 && isGroupCollapsed(gi) }"
|
||||
:class="{ 'ai-tool-card--group-hidden': isGroupHidden(tc, group, gi) }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -123,19 +123,36 @@ const pendingCount = computed(() =>
|
||||
* 响应式而非一次性初始化——流式场景第一张卡到达即触发旧一次性逻辑,
|
||||
* 此后组从 1 卡长到 N 卡不再折叠,批量读取的 N 张单行卡全展开刷屏;
|
||||
* 签名(组名:卡数)变化即幂等补齐,新达到阈值的组收起,已 touched 的组不动。
|
||||
*
|
||||
* 活组豁免:含 running/pending_approval 卡或用户展开过卡的组(groupHasLiveCard)
|
||||
* 不自动收;已被收的活组自动复活展开。治"执行轮整组(读取文件/搜索内容)被收只剩组头,
|
||||
* running 卡强制展开也被组 hidden 压住,用户想看进度要点两层"。
|
||||
*/
|
||||
watch(
|
||||
() => groupedToolCalls.value.map(g => `${g.name}:${g.calls.length}`).join('|'),
|
||||
() => {
|
||||
const next = new Set(collapsedGroups.value)
|
||||
groupedToolCalls.value.forEach((g, gi) => {
|
||||
if (g.calls.length >= 2 && !userTouchedGroups.value.has(g.name)) next.add(gi)
|
||||
if (g.calls.length < 2 || userTouchedGroups.value.has(g.name)) return
|
||||
if (groupHasLiveCard(g)) next.delete(gi)
|
||||
else next.add(gi)
|
||||
})
|
||||
collapsedGroups.value = next
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 组内是否存在"活着"的卡:running/pending_approval 进行中 或 用户主动展开过(userExpandedCards)。
|
||||
* 自动收起补齐对这类组跳过(不误收用户正在看的/正在执行的),
|
||||
* 且补齐时自动展开复活——治"组从 1 卡长到 2 卡瞬间整组被收、用户点开也被压回去"。 */
|
||||
function groupHasLiveCard(g: ToolCallGroup): boolean {
|
||||
return g.calls.some(tc =>
|
||||
tc.status === 'running'
|
||||
|| tc.status === 'pending_approval'
|
||||
|| userExpandedCards.value.has(tc.id)
|
||||
)
|
||||
}
|
||||
|
||||
function isGroupCollapsed(gi: number): boolean {
|
||||
return collapsedGroups.value.has(gi)
|
||||
}
|
||||
@@ -180,7 +197,9 @@ function groupDisplayName(group: ToolCallGroup): string {
|
||||
|
||||
/**
|
||||
* 单卡是否展开:
|
||||
* - 分组已折叠 → false(整组隐藏)
|
||||
* - 分组已折叠 → false(整组隐藏)。**例外**: 用户主动展开过的卡(userExpandedCards)
|
||||
* 不被组折叠压制()——组收起只隐藏"无用户记忆"的卡,
|
||||
* 用户点开过的卡保持展开,否则"点开又被组补齐自动压回去"死循环。
|
||||
* - 未手动操作过 → 默认折叠(U-260629):completed 卡一律单行 header(工具名+目标+结果摘要),
|
||||
* 杜绝"AI 一轮多工具各铺开一整卡"刷屏;点 header 才展开。
|
||||
* running/pending/rejected/write_file 不受此值影响——ToolCard 内部
|
||||
@@ -189,17 +208,26 @@ function groupDisplayName(group: ToolCallGroup): string {
|
||||
* 清了 expandedCards,记忆态仍保持展开(用户读过的卡不强制折叠)。
|
||||
*/
|
||||
function isCardExpanded(tc: AiToolCallInfo, gi: number): boolean {
|
||||
if (isGroupCollapsed(gi)) return false
|
||||
// 用户主动折叠最优先(覆盖用户展开记忆)
|
||||
// 用户主动折叠最优先(覆盖一切,含组折叠与展开记忆)
|
||||
if (userCollapsedCards.value.has(tc.id)) return false
|
||||
// 用户主动展开的卡片记忆态优先(不被自动收起影响)
|
||||
// 用户主动展开记忆次优先:压过组折叠(),点开过的卡组收起时仍展开
|
||||
if (userExpandedCards.value.has(tc.id)) return true
|
||||
// 组折叠(只影响无用户记忆的卡)
|
||||
if (isGroupCollapsed(gi)) return false
|
||||
// 用户手动切换过该卡(当前态)
|
||||
if (expandedCards.value.has(tc.id)) return true
|
||||
// 兜底:默认折叠(单行 header 已有 toolDisplayName 路径/pattern/host + toolResultSummary 摘要)
|
||||
return false
|
||||
}
|
||||
|
||||
/** 组收起时卡片是否整卡隐藏(display:none)。
|
||||
* 用户主动展开过的卡豁免——组收起只隐藏无用户记忆的卡,
|
||||
* 否则模板 class 的 display:none 会把 isCardExpanded 已放行的展开卡重新藏掉。 */
|
||||
function isGroupHidden(tc: AiToolCallInfo, group: ToolCallGroup, gi: number): boolean {
|
||||
if (group.calls.length < 2 || !isGroupCollapsed(gi)) return false
|
||||
return !userExpandedCards.value.has(tc.id)
|
||||
}
|
||||
|
||||
function toggleCardExpand(tc: AiToolCallInfo, gi: number) {
|
||||
const id = tc.id
|
||||
// 用 isCardExpanded 判当前真实态(含用户操作记忆),而非 expandedCards(记忆态不在其中,
|
||||
@@ -399,7 +427,7 @@ function groupIcon(name: string): string {
|
||||
}
|
||||
|
||||
/* -- 分组隐藏态的卡片。
|
||||
UX-260616-03: 保持 display:none(原行为)。
|
||||
保持 display:none(原行为)。
|
||||
不改 max-height/opacity 动画的原因:flex 容器 gap:8px 对零高 item 仍贡献 gap,
|
||||
会留双倍间距致布局回归;display:none 移出流才无 gap。
|
||||
真正的高度动画需 <Transition> JS 钩子(高度 auto→0 不可纯 CSS transition),
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</div>
|
||||
|
||||
<!-- patch_file 结果:文件路径/字节增减 + unified diff 红删绿增行级预览(复用 ai-tool-diff-pre/--add/--del/--ctx 既有样式)
|
||||
UX-260618-06:后端 generate_diff 产完整 diff,result.diff 字段此前被丢弃,用户无法验证改了什么 → 此分支渲染 diff -->
|
||||
后端 generate_diff 产完整 diff,result.diff 字段此前被丢弃,用户无法验证改了什么 → 此分支渲染 diff -->
|
||||
<div v-else-if="tc.name === 'patch_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-patch-result">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon" v-html="fileIcon"></span>
|
||||
@@ -153,7 +153,7 @@
|
||||
<pre v-if="cmdOutputExpanded && cmdOutput" class="ai-tool-cmd-output"><code>{{ cmdOutput }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- http_request 结果:method + url + 状态码徽标 + 可折叠响应 body(对齐 run_command 模式 UX-260618-08) -->
|
||||
<!-- http_request 结果:method + url + 状态码徽标 + 可折叠响应 body(对齐 run_command 模式 ) -->
|
||||
<div
|
||||
v-else-if="tc.name === 'http_request' && tc.result && tc.status === 'completed' && parsed"
|
||||
class="ai-tool-http-result"
|
||||
|
||||
@@ -279,7 +279,7 @@ watch(() => store.state.streaming, (s) => {
|
||||
// 收起旧的已完成/已拒绝卡片,保留 running/pending/write_file 活跃卡片。
|
||||
// 折叠态由 ToolCardList 自治,父级仅经 expose 调 collapseInactive ──
|
||||
//
|
||||
// B-260616-13 性能:快照仅含 toolCalls 的 (id,status)+消息条数,剔除 content.length。
|
||||
// 性能:快照仅含 toolCalls 的 (id,status)+消息条数,剔除 content.length。
|
||||
// 收起判定只依赖 active Set(由 toolCalls 状态推导),与文本长度无关。
|
||||
// 原快照含 len → 流式每 token 增长末条消息 content → snap 变 → body 每帧重算
|
||||
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
|
||||
@@ -1049,7 +1049,7 @@ defineExpose({
|
||||
正常消息 + 展开的折叠段内消息 emit 'msg' 项。连续 archived_segment/compressed
|
||||
合并一条分隔条,点击展开/收起看原文)
|
||||
A 过程压缩:trace 项=完成轮次执行轨迹卡,step 项=执行中实时步骤摘要,均先于 msg 分支拦截。
|
||||
UX-2025-19: 每项外层包 .ai-msg-slot(B-260618-01 虚拟滚动已移除,现恒渲染;
|
||||
每项外层包 .ai-msg-slot(虚拟滚动已移除,现恒渲染;
|
||||
slot 仅作 flex 子占位 + 消息分组的语义容器,gap 由 .ai-messages 承担)。
|
||||
UX 密度改造:timesep 项=相邻消息间隔 ≥30 分钟或跨天的居中时间分隔线。 -->
|
||||
<template v-for="item in renderItems" :key="item.key">
|
||||
@@ -1119,7 +1119,7 @@ defineExpose({
|
||||
<span class="ai-retry-text">{{ item.msg.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- B-260618-19: 纯空 assistant(无 content/error/toolCalls 且非末条流式)整 slot 不渲染,
|
||||
<!-- 纯空 assistant(无 content/error/toolCalls 且非末条流式)整 slot 不渲染,
|
||||
消除空 div + flex gap 堆积的大片空白;sep 始终渲染;有 toolCalls 的空 assistant
|
||||
仍渲染(工具卡可见——修复 agentic 工具轮 LLM 无文本时工具卡消失)。
|
||||
UX 密度改造:per-block 相对时间已移除,精确时刻上移到消息容器 title(hover 可查)。 -->
|
||||
@@ -1152,8 +1152,8 @@ defineExpose({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息(正常段 + 展开的折叠段内消息)— 虚拟滚动已移除(UX-260617-01:IO/RO 时序致重叠),消息恒渲染 -->
|
||||
<!-- B-260618-19: 纯空 assistant 过滤已上提到外层 .ai-msg-slot 的 v-if(shouldRenderMsg),
|
||||
<!-- 消息(正常段 + 展开的折叠段内消息)— 虚拟滚动已移除(IO/RO 时序致重叠),消息恒渲染 -->
|
||||
<!-- 纯空 assistant 过滤已上提到外层 .ai-msg-slot 的 v-if(shouldRenderMsg),
|
||||
此处 v-else 恒渲染 .ai-msg(sep 已在上方 v-if 分支处理,此处必为 msg)。 -->
|
||||
<div
|
||||
v-else
|
||||
@@ -1237,7 +1237,7 @@ defineExpose({
|
||||
<span class="ai-cursor"></span>
|
||||
</div>
|
||||
|
||||
<!-- UX-03 / UX-260616-01:错误气泡操作入口(重试 / 去设置) -->
|
||||
<!-- UX-03 / 错误气泡操作入口(重试 / 去设置) -->
|
||||
<div v-if="item.msg.isError" class="ai-msg-actions ai-msg-actions--error">
|
||||
<span v-if="!canRetry(item.msg)" class="ai-msg-not-retryable">{{ $t('ai.notRetryable') }}</span>
|
||||
<button
|
||||
@@ -1341,7 +1341,7 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.ai-msg-slot(B-260618-01:虚拟滚动已移除,恒渲染容器) -->
|
||||
</div><!-- /.ai-msg-slot(虚拟滚动已移除,恒渲染容器) -->
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1468,7 +1468,7 @@ defineExpose({
|
||||
color: var(--df-danger);
|
||||
border-color: rgba(240,101,101,0.3);
|
||||
}
|
||||
/* 修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
|
||||
/* 修正 的 display:block 方案——display:block 让 table 失去表格
|
||||
布局上下文,tr/td 脱离 table 上下文塌成块级堆叠丢列对齐(用户截图反馈测试报告四列表格错乱)。
|
||||
正解:table 保持默认 display:table(列对齐恢复)+ max-width:100%;宽表格横向滚动交由气泡
|
||||
容器 .ai-msg-bubble--ai.ai-md 的 overflow-x:auto 承载(CSS spec 中 table-display 下
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="status-tag" :class="'status-' + idea.status">{{ $t(ideaStatusLabelKey(idea.status)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- B-260615-25:灵感描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail),空值回退 — -->
|
||||
<!-- 灵感描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail),空值回退 — -->
|
||||
<template v-if="editing">
|
||||
<textarea v-model="editDesc" class="detail-desc-edit" rows="4"></textarea>
|
||||
<div class="desc-edit-actions">
|
||||
@@ -725,7 +725,7 @@ watch(() => props.idea.id, loadHistory)
|
||||
margin-bottom: var(--df-gap-page);
|
||||
}
|
||||
|
||||
/* ===== 灵感描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
||||
/* ===== 灵感描述 Markdown 渲染(,基础样式收敛至全局 ai-md.css) ===== */
|
||||
.detail-desc.ai-md { white-space: normal; color: var(--df-text-secondary); }
|
||||
|
||||
.detail-section {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div class="detail-field">
|
||||
<label>{{ t('knowledge.contentLabel') }}</label>
|
||||
<!-- B-260615-25:知识内容 Markdown 渲染(展示态),复用 useMarkdown composable(同 B-24 TaskDetail);编辑态保持 textarea -->
|
||||
<!-- 知识内容 Markdown 渲染(展示态),复用 useMarkdown composable(同 B-24 TaskDetail);编辑态保持 textarea -->
|
||||
<div
|
||||
v-if="!editing && detail.knowledge.content"
|
||||
ref="contentEl"
|
||||
@@ -150,7 +150,7 @@ import { useRendered } from '@/composables/useMarkdown'
|
||||
import { formatRelative } from '@/utils/time'
|
||||
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types'
|
||||
|
||||
// 详情面板子组件(Knowledge.vue 拆分,SW-260618-20):
|
||||
// 详情面板子组件(Knowledge.vue 拆分,):
|
||||
// 接收 detail 渲染只读四段(基本信息/溯源/引用/时间线),内部自管 editing/editForm/refLimit;
|
||||
// 编辑保存与审核操作通过 emit 回传父级执行 IPC(publish/reject/archive/save)。
|
||||
const props = defineProps<{
|
||||
@@ -202,7 +202,7 @@ function saveEdit() {
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
// ===== Markdown 渲染(B-260615-25,复用模块单例渲染器)=====
|
||||
// ===== Markdown 渲染(,复用模块单例渲染器)=====
|
||||
const { rendered: renderedContent, ensureLoaded } = useRendered(
|
||||
() => props.detail?.knowledge.content ?? '',
|
||||
)
|
||||
@@ -342,11 +342,11 @@ watch(renderedContent, () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 详情面板四段(SW-260618-20 从 Knowledge.vue 迁移,样式与原一致) */
|
||||
/* 详情面板四段(从 Knowledge.vue 迁移,样式与原一致) */
|
||||
.detail-section { padding-bottom: 18px; margin-bottom: 18px; border-bottom: 0.5px solid var(--df-border); }
|
||||
.detail-section:last-child { border-bottom: none; margin-bottom: 0; }
|
||||
|
||||
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; min-width: 0; }
|
||||
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; min-width: 0; flex-wrap: wrap; }
|
||||
.detail-title-row { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden; }
|
||||
.detail-kind-icon { font-size: 20px; flex-shrink: 0; }
|
||||
.detail-title { font-size: 18px; font-weight: 500; color: var(--df-text); word-break: break-word; overflow-wrap: break-word; min-width: 0; }
|
||||
@@ -358,7 +358,7 @@ watch(renderedContent, () => {
|
||||
.detail-field label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
|
||||
.detail-content { font-size: 13px; color: var(--df-text); line-height: 1.6; white-space: pre-wrap; }
|
||||
|
||||
/* ===== 知识内容 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css)===== */
|
||||
/* ===== 知识内容 Markdown 渲染(,基础样式收敛至全局 ai-md.css)===== */
|
||||
.detail-content.ai-md { white-space: normal; }
|
||||
.detail-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag { font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs); background: var(--df-accent-bg); color: var(--df-accent); }
|
||||
@@ -448,10 +448,18 @@ watch(renderedContent, () => {
|
||||
|
||||
/* 按钮基础样式 (.btn / .btn-sm / .btn-primary / .btn-ghost) 已收敛至 global.css */
|
||||
|
||||
/* ===== 响应式:窄屏详情标题防竖线化(B-260619)===== */
|
||||
/* ===== 响应式:窄屏详情标题防竖线化()===== */
|
||||
/* 双闸:@media 管窗口窄;@container 管「视口宽但面板被 AI 平分挤压」场景。
|
||||
注意:@container 需要 .knowledge(父链上有 container-type 的祖先)声明容器,
|
||||
已在 Knowledge.vue 根节点加 container-type: inline-size。 */
|
||||
@media (max-width: 760px) {
|
||||
.detail-head { flex-direction: column; align-items: flex-start; }
|
||||
.detail-actions { width: 100%; }
|
||||
.detail-title { font-size: 15px; }
|
||||
}
|
||||
@container (max-width: 420px) {
|
||||
.detail-head { flex-direction: column; align-items: flex-start; }
|
||||
.detail-actions { width: 100%; }
|
||||
.detail-title { font-size: 15px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 人工审批对话框(从 ProjectDetail.vue 抽离 UX-260618-17 submitting 保留)-->
|
||||
<!-- 人工审批对话框(从 ProjectDetail.vue 抽离 submitting 保留)-->
|
||||
<div v-if="visible" class="modal-overlay">
|
||||
<div class="modal-box" style="width: 500px">
|
||||
<h3 class="modal-title">{{ $t('projectDetail.approvalTitle') }}</h3>
|
||||
@@ -76,7 +76,7 @@ import { useToast } from '@/composables/useToast'
|
||||
|
||||
// 抽离自 ProjectDetail.vue(L175-225 模板 + L416-471 审批逻辑/watch)
|
||||
// store 共享:父级 useProjectStore() 实例传入(同源 store,响应式共享 pendingApproval)
|
||||
// submitting 自治:本组件持有禁用态(UX-260618-17 双击防抖)
|
||||
// submitting 自治:本组件持有禁用态(双击防抖)
|
||||
// ProjectStore 类型未 export(store.ts:135 内部 type),用 ReturnType 自推导同源类型
|
||||
type ProjectStore = ReturnType<typeof useProjectStore>
|
||||
|
||||
@@ -135,7 +135,7 @@ async function handleApproval(decision: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 多选审批(UX-260618-17 submitting 防双击保留)
|
||||
// 多选审批(submitting 防双击保留)
|
||||
async function handleApprovalMulti() {
|
||||
if (multiDecisions.value.length === 0) return
|
||||
submitting.value = true
|
||||
|
||||
@@ -71,10 +71,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片(外部 diff 存在时让位) -->
|
||||
<!-- 图片(外部 diff 存在时让位;加载失败显示错误占位,不静默空白) -->
|
||||
<div v-else-if="isImage && !isExternalDiff" class="preview-image">
|
||||
<img :src="imageUrl ?? ''" :alt="filePath ?? ''" />
|
||||
<img
|
||||
v-if="imageUrl && !imageLoadError"
|
||||
:src="imageUrl"
|
||||
:alt="filePath ?? ''"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
<div v-else class="preview-image-error">
|
||||
<div class="preview-placeholder-icon">🖼️</div>
|
||||
<div class="preview-placeholder-text">图片加载失败(路径: {{ filePath }})</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Diff 视图(外部注入的 diff 优先展示;置于 Markdown 之前,使 .md 文件也支持 Diff) -->
|
||||
@@ -171,6 +180,8 @@ const isBinary = ref(false)
|
||||
const fileSize = ref<number | null>(null)
|
||||
const truncated = ref(false)
|
||||
const imageUrl = ref<string | null>(null)
|
||||
/** 图片加载失败标记(asset 协议不可用/文件被删等);@error 置位,文件切换时复位 */
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
/** 符号概览(fileOutline 结果;仅支持语言 + 非 diff 视图时展示)。 */
|
||||
const outlineSupported = ref(false)
|
||||
@@ -312,7 +323,7 @@ const hljsClass = computed(() => {
|
||||
})
|
||||
|
||||
/** 图片后缀白名单。 */
|
||||
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico']
|
||||
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico', '.avif']
|
||||
|
||||
/** Markdown 后缀判定。 */
|
||||
const isMarkdown = computed(() => {
|
||||
@@ -486,6 +497,7 @@ async function loadFile() {
|
||||
URL.revokeObjectURL(imageUrl.value)
|
||||
imageUrl.value = null
|
||||
}
|
||||
imageLoadError.value = false
|
||||
// 重置上一文件的符号概览(新文件结果到达后再填充)
|
||||
outlineSupported.value = false
|
||||
outlineSymbols.value = []
|
||||
|
||||
@@ -87,7 +87,7 @@ const reviewSuggestions = computed<string[]>(() => {
|
||||
<style scoped>
|
||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||
/* 基础布局 .info-item / .info-block / .label / .value 已收敛至全局 components.css
|
||||
(DRY 收口 B-260619)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||
(DRY 收口)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||
故本子组件无需复制等价基础布局,仅保留本组件特有的 output/review 样式。 */
|
||||
|
||||
.output-block .output-body {
|
||||
|
||||
Reference in New Issue
Block a user