优化: 前端批(审批体系一致 + GitChanges UI + token/控件统一 + 死代码)
审批体系一致:ApprovalPopup 浮窗加挂起时长提示(H1)+session 按钮+path deny 二次确认(M4/M5);ToolCard/useToolApproval 加 session+onDeny 确认(对齐 DirAuthDialog 四按钮)+ToolCardList decision 类型加 session;ApprovalDialog 失败显 toast(治 store 吞错 M31);ApprovalOverlay dismiss 改响应式(L26) GitChanges UI(问题12 前端 12b-12h):showCommitFileDiff 精确匹配(12b)+去重 git log(12c)+详情滑出不挤压(12d)+双行提交行(12f)+搜索过滤(12g)+历史 Tab 显分支(12h)+diff 侧边栏内更宽可折叠面板(12e:FilePreview 不支持任意提交 diff,折中) token+控件:global.css 定义 --df-primary(治幽灵变量 M26)+settings.css 加 .setting-number/.setting-input/.setting-textarea+4 Section 套用(问题8) 死代码:删 PlanProgress.vue(零引用)+PlanLayerInfo 类型+i18n 孤儿 key(L1)
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useAiStore } from '@/stores/ai'
|
||||
|
||||
const store = useAiStore()
|
||||
@@ -57,18 +57,46 @@ const count = computed(() => store.state.pendingApprovals.length)
|
||||
const dismissedAt = ref<number>(0)
|
||||
const DISMISS_COOLDOWN_MS = 60_000
|
||||
|
||||
// 显隐:有 pending + 未在冷却期
|
||||
// L26: 响应式时钟。原 visible 直接读 Date.now()(非响应式),computed 仅在 dismissedAt/count 变化时
|
||||
// 重算 → dismiss 满 60s 时无触发源,浮层不会自动重现。now 每秒翻一次驱动重算(60s 冷却期颗粒度足够,
|
||||
// 浮层非精确计时器)。仅在 dismissed 时跑定时器节省空转。
|
||||
const now = ref(Date.now())
|
||||
let _tickTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function startTick() {
|
||||
if (_tickTimer) return
|
||||
_tickTimer = setInterval(() => { now.value = Date.now() }, 1000)
|
||||
}
|
||||
function stopTick() {
|
||||
if (_tickTimer) {
|
||||
clearInterval(_tickTimer)
|
||||
_tickTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 显隐:有 pending + 未在冷却期(用响应式 now 替代裸 Date.now())
|
||||
const visible = computed(() => {
|
||||
if (count.value === 0) return false
|
||||
if (dismissedAt.value && Date.now() - dismissedAt.value < DISMISS_COOLDOWN_MS) return false
|
||||
if (dismissedAt.value && now.value - dismissedAt.value < DISMISS_COOLDOWN_MS) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// 审批清空时复位 dismiss(下次有新审批会重新弹)
|
||||
watch(count, (c) => {
|
||||
if (c === 0) dismissedAt.value = 0
|
||||
// dismiss 启动时钟;冷却期到或审批清空时停时钟(避免无意义空转)
|
||||
watch(dismissedAt, (d) => {
|
||||
if (d > 0) startTick()
|
||||
else stopTick()
|
||||
})
|
||||
|
||||
// 审批清空时复位 dismiss(下次有新审批会重新弹)+ 停时钟
|
||||
watch(count, (c) => {
|
||||
if (c === 0) {
|
||||
dismissedAt.value = 0
|
||||
stopTick()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => stopTick())
|
||||
|
||||
function onActivate() {
|
||||
emit('activate')
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div class="plan-progress" v-if="visible && layers.length > 0">
|
||||
<div class="plan-progress-header">
|
||||
<span class="plan-progress-title">📋 {{ t('aiChat.planProgress') }}</span>
|
||||
<span class="plan-progress-count">{{ doneCount }}/{{ totalCount }}</span>
|
||||
</div>
|
||||
<div class="plan-progress-layers">
|
||||
<div v-for="(layer, li) in layers" :key="li" class="plan-layer">
|
||||
<div class="plan-layer-label">{{ t('aiChat.planLayer', { n: li + 1 }) }}</div>
|
||||
<div class="plan-layer-items">
|
||||
<div
|
||||
v-for="(item, ii) in layer.items"
|
||||
:key="ii"
|
||||
class="plan-item"
|
||||
:class="'plan-item--' + item.status"
|
||||
>
|
||||
<span class="plan-item-icon" :class="'persona-' + (item.persona_id || 'default')">
|
||||
{{ personaIcon(item.persona_id) }}
|
||||
</span>
|
||||
<span class="plan-item-label">{{ item.intent }}</span>
|
||||
<span class="plan-item-status">{{ statusIcon(item.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="li < layers.length - 1" class="plan-layer-arrow">↓</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 冲突徽章 -->
|
||||
<div v-if="conflicts.length > 0" class="plan-conflicts-badge" @click="emit('show-conflicts')">
|
||||
⚠ {{ t('aiChat.conflictsPending', { n: conflicts.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { PlanLayerInfo, ConflictInfo } from '@/api/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
layers: PlanLayerInfo[]
|
||||
conflicts?: ConflictInfo[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'show-conflicts'): void
|
||||
}>()
|
||||
|
||||
const conflicts = computed(() => props.conflicts || [])
|
||||
|
||||
const totalCount = computed(() =>
|
||||
props.layers.reduce((s, l) => s + l.items.length, 0)
|
||||
)
|
||||
const doneCount = computed(() =>
|
||||
props.layers.reduce((s, l) => s + l.items.filter(i => i.status === 'done').length, 0)
|
||||
)
|
||||
|
||||
function personaIcon(personaId: string | null): string {
|
||||
switch (personaId) {
|
||||
case 'coder': return '🔵'
|
||||
case 'reviewer': return '🔍'
|
||||
case 'architect': return '📐'
|
||||
case 'tester': return '🧪'
|
||||
case 'analyst': return '📊'
|
||||
default: return '⚪'
|
||||
}
|
||||
}
|
||||
|
||||
function statusIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'done': return '✓'
|
||||
case 'running': return '▶'
|
||||
case 'error': return '✗'
|
||||
default: return '○'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plan-progress {
|
||||
border: 1px solid var(--df-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 8px 0;
|
||||
background: var(--df-bg-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.plan-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.plan-progress-title { color: var(--df-text); }
|
||||
.plan-progress-count { color: var(--df-text-dim); }
|
||||
.plan-layer { margin-bottom: 4px; }
|
||||
.plan-layer-label {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.plan-layer-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.plan-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
background: var(--df-bg);
|
||||
border: 1px solid var(--df-border);
|
||||
}
|
||||
.plan-item--done { border-color: var(--df-success); color: var(--df-success); }
|
||||
.plan-item--running { border-color: var(--df-primary); color: var(--df-primary); }
|
||||
.plan-item--error { border-color: var(--df-danger); color: var(--df-danger); }
|
||||
.plan-item-icon { font-size: 10px; }
|
||||
.plan-item-status { font-size: 10px; font-weight: 700; }
|
||||
.plan-layer-arrow { text-align: center; color: var(--df-text-dim); font-size: 10px; }
|
||||
.plan-conflicts-badge {
|
||||
margin-top: 8px;
|
||||
padding: 4px 8px;
|
||||
background: var(--df-warning-bg, rgba(255, 193, 7, 0.1));
|
||||
border: 1px solid var(--df-warning, #ffc107);
|
||||
border-radius: 4px;
|
||||
color: var(--df-warning, #ffc107);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
.plan-conflicts-badge:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user