文档: 无人值守专项设计档+todo 销账(#10/#11/#12), Web 规范合规批(App/视图/组件样式)
This commit is contained in:
+1
-1
@@ -337,7 +337,7 @@ onUnmounted(() => {
|
||||
font-size: 13px; max-width: 90vw;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
|
||||
.toast-enter-active, .toast-leave-active { transition: opacity 0.25s var(--df-ease), transform 0.25s var(--df-ease); }
|
||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(10px); }
|
||||
/* 后台运行提示:同 df-toast 形态,信息语义用 accent 底色与错误红区分 */
|
||||
.df-toast--info { background: var(--df-accent); }
|
||||
|
||||
@@ -597,7 +597,12 @@ async function backfillQueued(idx: number) {
|
||||
function scrollToBottom(smooth = false) {
|
||||
messageListRef.value?.scrollToBottom(smooth)
|
||||
}
|
||||
// listener 租约句柄(mount 借/unmount 还,所有权模型;startListener 失败时为 null,归还侧判空)
|
||||
let _listenerRelease: (() => void) | null = null
|
||||
onBeforeUnmount(() => {
|
||||
// 归还 listener 租约(最后一份归还才真拆单例 listener;双重归还/下溢由 useAiEvents dev 断言暴露)
|
||||
_listenerRelease?.()
|
||||
_listenerRelease = null
|
||||
// V7 排队时钟 interval 随组件卸载清理(watch 随卸载停但 interval 本体不自停,须显式清)
|
||||
if (queueTickTimer !== null) { clearInterval(queueTickTimer); queueTickTimer = null }
|
||||
// 全局快捷键卸载时移除 window listener,防分离窗口/切组件后泄漏
|
||||
@@ -609,9 +614,8 @@ onBeforeUnmount(() => {
|
||||
// 流式 rAF 清理(streaming watch 内 cancelAnimationFrame)随 MessageList 迁移,子组件
|
||||
// onBeforeUnmount 自管(子 watch streaming 已注册,卸载自动停)。
|
||||
// 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
|
||||
// 之前 stopListener 定义但全仓零调用 → listener+watchdog 永久泄漏;分离窗口主窗口 AiChat
|
||||
// 卸载(走 App.vue v-if detach)时 listener 仍挂后端,watchdog 130s 后写已卸载 state。
|
||||
store.stopListener()
|
||||
// 旧 stopListener 已删除(所有权模型:租约在 onBeforeUnmount 顶部归还,store 侧 releaseLease
|
||||
// 在最后一份归还时拆 listener + 清看门狗/计时器,此处不再重复清)。
|
||||
// F-15 阶段2: 释放上下文管理事件监听器
|
||||
store.stopContextListener()
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } //
|
||||
@@ -635,9 +639,27 @@ onMounted(async () => {
|
||||
console.debug(`[启动] AiChat mounted: ${(performance.now() - t0).toFixed(0)}ms`)
|
||||
// Markdown 渲染器预热随 MessageList 子组件迁移(子 onMounted 内 loadMarkdown);
|
||||
// useMarkdown 模块级单例,父不再直接调用。
|
||||
await store.startListener()
|
||||
await store.loadProviders()
|
||||
await store.loadConversations()
|
||||
// ── 分级初始化(根本修 #5)──
|
||||
// 第一级(致命):listener 租约。失败 = 无事件通道,面板功能尽失,toast + 止损
|
||||
// (重开面板 re-mount 即重试);租约句柄 null,卸载侧判空归还。
|
||||
try {
|
||||
_listenerRelease = await store.startListener()
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
console.error('[AiChat] listener 注册失败(致命,面板初始化中止):', e)
|
||||
showToast(t('aiChat.initError', { msg }), 'error')
|
||||
return
|
||||
}
|
||||
// 第二级(可降级):providers/conversations 列表。失败仅列表空,面板/发送/流式仍可用,
|
||||
// toast 提示但继续初始化(skills/上下文监听/生成态恢复不依赖列表数据)。
|
||||
try {
|
||||
await store.loadProviders()
|
||||
await store.loadConversations()
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
console.error('[AiChat] 列表加载失败(降级继续):', e)
|
||||
showToast(t('aiChat.initDegraded', { msg }), 'warning')
|
||||
}
|
||||
store.loadSkills()
|
||||
store.loadToolRisks()
|
||||
// @ 提及预加载: AiChat 的 @ 联想读取 projectsStore.projects/tasks,
|
||||
|
||||
@@ -100,7 +100,7 @@ watch(() => props.visible, () => {})
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
border: 0.5px solid var(--df-border, #ddd);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.confirm-btn--cancel {
|
||||
background: transparent;
|
||||
|
||||
@@ -143,7 +143,7 @@ function onPageSizeChange(val: string) {
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
|
||||
@@ -491,7 +491,7 @@ const visibleArgs = computed(() => {
|
||||
.ai-tool-btn {
|
||||
padding: 5px 12px; font-size: 11px; font-weight: 500;
|
||||
border-radius: var(--df-radius-sm); border: 0.5px solid var(--df-border); cursor: pointer;
|
||||
display: flex; align-items: center; gap: 4px; transition: all 0.15s var(--df-ease); font-family: var(--df-font-sans);
|
||||
display: flex; align-items: center; gap: 4px; transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease), border-color 0.15s var(--df-ease); font-family: var(--df-font-sans);
|
||||
}
|
||||
.ai-tool-btn--approve { background: var(--df-success-bg); color: var(--df-success); }
|
||||
.ai-tool-btn--approve:hover { background: var(--df-success); color: #fff; border-color: var(--df-success); }
|
||||
|
||||
@@ -539,7 +539,7 @@ watch(() => props.tc.status, (s) => {
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: color 0.15s var(--df-ease), background 0.15s var(--df-ease), border-color 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-tool-expand-btn:hover {
|
||||
color: var(--df-text-secondary);
|
||||
|
||||
@@ -125,7 +125,7 @@ function onDismiss() {
|
||||
border-radius: var(--df-radius-md);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--df-accent-soft);
|
||||
cursor: pointer;
|
||||
transition: all 0.18s var(--df-ease);
|
||||
transition: transform 0.18s var(--df-ease), border-color 0.18s var(--df-ease), box-shadow 0.18s var(--df-ease);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ function onDismiss() {
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease);
|
||||
}
|
||||
|
||||
.approval-overlay-close:hover {
|
||||
@@ -208,7 +208,7 @@ function onDismiss() {
|
||||
/* 进出动画:从右侧滑入 */
|
||||
.approval-overlay-enter-active,
|
||||
.approval-overlay-leave-active {
|
||||
transition: all 0.25s var(--df-ease);
|
||||
transition: opacity 0.25s var(--df-ease), transform 0.25s var(--df-ease);
|
||||
}
|
||||
|
||||
.approval-overlay-enter-from,
|
||||
|
||||
@@ -1219,7 +1219,7 @@ defineExpose({
|
||||
background: var(--df-bg-raised);
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-img-pick-btn {
|
||||
@@ -1233,7 +1233,7 @@ defineExpose({
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ const examplePrompts = [
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: border-color 0.15s var(--df-ease), color 0.15s var(--df-ease), opacity 0.15s var(--df-ease), background 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-empty-prompt:hover {
|
||||
border-color: var(--df-accent);
|
||||
|
||||
@@ -935,7 +935,7 @@ watch(() => props.idea.id, loadHistory)
|
||||
border-radius: var(--df-radius);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.btn-evaluate:hover {
|
||||
|
||||
@@ -273,7 +273,7 @@ const secondaryNav = [
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
font-weight: 450;
|
||||
transition: all 0.18s var(--df-ease);
|
||||
transition: background 0.18s var(--df-ease), color 0.18s var(--df-ease);
|
||||
position: relative;
|
||||
}
|
||||
.df-sidebar--collapsed .nav-link {
|
||||
|
||||
@@ -740,7 +740,7 @@ async function onRemoveModule() {
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -859,7 +859,7 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
color: var(--df-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -896,7 +896,7 @@ onUnmounted(() => {
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.outline-overlay-btn:hover {
|
||||
|
||||
@@ -569,7 +569,7 @@ watch(() => props.refreshKey, () => {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.branch-switch-btn:hover {
|
||||
color: var(--df-text);
|
||||
@@ -581,7 +581,7 @@ watch(() => props.refreshKey, () => {
|
||||
.gc-tab {
|
||||
flex: 1; padding: 8px; background: none; border: none;
|
||||
border-bottom: 2px solid transparent; color: var(--df-text-dim);
|
||||
cursor: pointer; font-size: 12px; transition: all 0.15s;
|
||||
cursor: pointer; font-size: 12px; transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.gc-tab.active { color: var(--df-text); border-bottom-color: var(--df-accent); }
|
||||
.gc-tab:hover { color: var(--df-text); }
|
||||
|
||||
@@ -124,7 +124,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', onWinResize))
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
transition: all 0.15s;
|
||||
transition: border-color 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.project-card:hover {
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
.toggle input:checked + .toggle-slider::before { transform: translateX(18px); background: #fff; }
|
||||
|
||||
/* ===== 按钮(原 Settings.vue,列表型 Panel 共用) ===== */
|
||||
.btn { padding: 8px 16px; border: none; border-radius: var(--df-radius-sm); font-size: 13px; cursor: pointer; transition: all 0.15s; }
|
||||
.btn { padding: 8px 16px; border: none; border-radius: var(--df-radius-sm); font-size: 13px; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--df-accent-hover); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
@@ -351,7 +351,7 @@
|
||||
background: transparent;
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.ai-btn-icon:hover {
|
||||
background: var(--df-bg-card);
|
||||
|
||||
+21
-2
@@ -139,6 +139,25 @@ html, body, #app {
|
||||
::-webkit-scrollbar-thumb { background: var(--df-scrollbar); border-radius: var(--df-radius-xs); }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--df-scrollbar-hover); }
|
||||
|
||||
/* ── UX 走查 P2-1(2026-10-06):Web 规范合规全局两刀 ──
|
||||
* ① :focus-visible 兜底 outline:全仓 outline:none 25 处散点只删可见环不给补偿,
|
||||
* 键盘用户失焦可见性;全局一条兜底(仅键盘导航触发,鼠标点击不显),散点无需逐处改。
|
||||
* ② 弹层 overscroll-behavior:contain:弹层内滚到边界时滚动穿透到底层页面
|
||||
* (背景跟着晃),全仓此前零命中;约定所有浮层容器类统一滚动锁,新增弹层自动生效。
|
||||
* 选择器=实证类名清单(2026-10-06 全仓 grep 核对):modal-overlay 10 处 /
|
||||
* TopBar 四面板 inline-list / ai-more-menu / ConfirmDialog 根。
|
||||
*/
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--df-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.modal-overlay,
|
||||
.ai-goals-inline-list, .ai-history-inline-list,
|
||||
.ai-summary-inline-list, .ai-context-inline-list,
|
||||
.ai-more-menu, .confirm-box {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* — Selection — */
|
||||
/* 选中态用 accent 实色 + 白字:保证紫底气泡(accent)选中也高对比可读,
|
||||
避免 accent-soft(透明紫)叠 accent 紫底导致紫字紫底不可见(深色主题) */
|
||||
@@ -182,7 +201,7 @@ html, body, #app {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease), border-color 0.15s var(--df-ease);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
@@ -255,7 +274,7 @@ html, body, #app {
|
||||
color: var(--df-text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.filter-btn:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.filter-btn.active {
|
||||
|
||||
@@ -591,7 +591,7 @@ function onDragEnd() { isDragging.value = false }
|
||||
color: var(--df-text-dim);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease);
|
||||
}
|
||||
.popup-header-close:hover {
|
||||
background: var(--df-sidebar-hover);
|
||||
@@ -629,7 +629,7 @@ function onDragEnd() { isDragging.value = false }
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-sidebar-hover);
|
||||
border-left: 2px solid var(--df-accent);
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease);
|
||||
}
|
||||
.popup-item--path { border-left-color: var(--df-accent); }
|
||||
.popup-item--risk { border-left-color: var(--df-warning, #f0a020); }
|
||||
@@ -725,7 +725,7 @@ function onDragEnd() { isDragging.value = false }
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease), border-color 0.15s var(--df-ease);
|
||||
}
|
||||
.popup-btn:disabled {
|
||||
opacity: 0.6;
|
||||
@@ -806,7 +806,7 @@ function onDragEnd() { isDragging.value = false }
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
transition: background 0.15s var(--df-ease), color 0.15s var(--df-ease);
|
||||
}
|
||||
.popup-footer:hover {
|
||||
background: var(--df-accent);
|
||||
|
||||
@@ -656,7 +656,7 @@ onMounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
margin-bottom: -0.5px;
|
||||
}
|
||||
.audit-tabs .tab-btn:hover { color: var(--df-text); }
|
||||
|
||||
+1
-1
@@ -585,7 +585,7 @@ watch(() => route.params.id, (id) => {
|
||||
padding: 14px;
|
||||
border-radius: var(--df-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.idea-card-body {
|
||||
|
||||
@@ -498,7 +498,7 @@ onMounted(() => {
|
||||
.top-tabs { display: flex; gap: 4px; margin-bottom: var(--df-gap-page); border-bottom: 0.5px solid var(--df-border); }
|
||||
.top-tab {
|
||||
padding: 8px 16px; border: none; background: transparent; color: var(--df-text-secondary);
|
||||
font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s;
|
||||
font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.top-tab:hover { color: var(--df-text); }
|
||||
@@ -543,7 +543,7 @@ onMounted(() => {
|
||||
.list-cards { flex: 1; overflow-y: auto; padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.kn-card {
|
||||
background: var(--df-bg-card); border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius); padding: 10px 12px; cursor: pointer; transition: all 0.15s;
|
||||
border-radius: var(--df-radius); padding: 10px 12px; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.kn-card:hover { border-color: var(--df-accent); }
|
||||
.kn-card.selected { border-color: var(--df-accent); box-shadow: inset 3px 0 0 var(--df-accent); }
|
||||
|
||||
@@ -1399,7 +1399,7 @@ onUnmounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
margin-bottom: -0.5px;
|
||||
}
|
||||
.tab-btn:hover {
|
||||
@@ -1495,7 +1495,7 @@ onUnmounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.task-filter-chips .filter-chip:hover {
|
||||
|
||||
@@ -555,7 +555,7 @@ async function runImport() {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.view-toggle-btn:hover { background: var(--df-bg-card); color: var(--df-text); }
|
||||
.view-toggle-btn.active {
|
||||
|
||||
+2
-2
@@ -1022,7 +1022,7 @@ onUnmounted(() => {
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.seg-btn:first-child { border-radius: var(--df-radius-sm) 0 0 var(--df-radius-sm); }
|
||||
.seg-btn:last-child { border-radius: 0 var(--df-radius-sm) var(--df-radius-sm) 0; }
|
||||
@@ -1239,7 +1239,7 @@ onUnmounted(() => {
|
||||
color: var(--df-text-dim);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filter-chip:hover {
|
||||
|
||||
Reference in New Issue
Block a user