优化: 前端DRY收敛与命名对齐

审批等待格式化与阈值沉 utils/time 正典
相对时间阶梯三套收敛 formatRelative 参数化
灵感状态表沉 constants,透传通道退役
aggText/clearOwn 等命名对齐,IPC 参数名统一 conversation_id
This commit is contained in:
lxy
2026-10-01 19:35:20 +08:00
parent 6ef15a0940
commit 0560137840
16 changed files with 111 additions and 97 deletions
+4 -15
View File
@@ -55,7 +55,7 @@
<!-- H1 修复:审批挂起时长提示(对齐 ToolCard: >2min 橙色 / >5min 红色脉冲)。
后端 APPROVAL_TIMEOUT_MS 5min 静默拒绝,浮窗原本无计时,用户误以为还有时间。
基于审批到达时间(popup 首次见到该 tc.id 的时刻)计时,与 ToolCard 同款语义同款样式。 -->
<div v-if="waitSecsFor(tc.id) > 0" class="popup-item-wait" :class="'is-' + waitLevelFor(tc.id)">
<div v-if="waitSecsFor(tc.id) > 0" class="popup-item-wait" :class="'is-' + waitLevelOf(waitSecsFor(tc.id))">
{{ $t('aiTool.waitTime', { time: formatWaitTime(waitSecsFor(tc.id)) }) }}
</div>
<!-- path 类:目录提示 -->
@@ -153,6 +153,7 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { toolDisplayName } from '@/composables/ai/useToolCardHeader'
import { dispatchApprovalIPC } from '@/composables/ai/useToolApproval'
import { ApprovalPopupEvents } from '@/composables/ai/useApprovalPopup'
import { formatWaitTime, waitLevelOf } from '@/utils/time'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { useToast } from '@/composables/useToast'
import type { AiToolCallInfo } from '@/api/types'
@@ -189,30 +190,18 @@ function answerConfirm(ok: boolean) {
// ── H1:审批挂起时长计时(对齐 ToolCard useToolApproval 的 waitSecs/waitLevel)──
// 浮窗独立于主窗口,不直接复用 useToolApproval(那个绑定单 tc + 完整状态机 emit/防抖,
// 浮窗已用 processingId/runApproval 自管)。这里基于「到达时间」(popup 首次见到 tc.id 的时刻)
// 每秒刷新。语义/样式/阈值与 ToolCard 同款:120s long / 300s urgent。
// 每秒刷新。格式化(formatWaitTime)与分级阈值(waitLevelOf,120s long / 300s urgent)
// 收敛至 @/utils/time,与 ToolCard 同源。
/** tc.id → 到达时刻(ms)。审批首次出现在 approvals 即记时;移除即清。 */
const arrivalTimes = reactive(new Map<string, number>())
/** tc.id → 已等待秒数(每秒 tick 更新)。 */
const waitSecsMap = reactive(new Map<string, number>())
let _waitTimer: ReturnType<typeof setInterval> | null = null
/** 格式化秒为 MM:SS(与 useToolApproval.formatWaitTime 同款) */
function formatWaitTime(secs: number): string {
const m = Math.floor(secs / 60)
const s = secs % 60
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
}
/** 取某条审批已等待秒数(无记录返回 0) */
function waitSecsFor(id: string): number {
return waitSecsMap.get(id) ?? 0
}
/** 取等待级别(对齐 ToolCard: 300s urgent / 120s long / 其余 normal) */
function waitLevelFor(id: string): 'normal' | 'long' | 'urgent' {
const s = waitSecsFor(id)
if (s >= 300) return 'urgent'
if (s >= 120) return 'long'
return 'normal'
}
/** 每秒刷新所有挂起审批的 waitSecs(单 timer 驱动多 tc,避免每条一个 interval) */
function _tickWaitSecs() {
const now = Date.now()
+7 -19
View File
@@ -70,7 +70,7 @@
</div>
<p class="idea-desc-preview">{{ descPreviews.get(idea.id) || '' }}</p>
<div class="idea-card-footer">
<span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
<span class="status-tag" :class="'status-' + idea.status">{{ $t(ideaStatusLabelKey(idea.status)) }}</span>
<span class="idea-date">{{ formatDate(idea.created_at) }}</span>
</div>
</div>
@@ -93,7 +93,6 @@
:eval-error="evalError"
:promoting="promoting"
:deleting="deleting"
:status-options="statusOptions"
@evaluate="evaluateCurrentIdea"
@promote="promoteToProject"
@delete="deleteCurrentIdea"
@@ -165,6 +164,7 @@ import type { IdeaStatus, IdeaQuery } from '../api/types'
import { formatDate } from '../utils/time'
import { stripMd } from '../utils/markdown'
import { scoreTier } from '../utils/ideaEval'
import { ideaStatusLabelKey } from '../constants/project'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import IdeaDetail from '../components/ideas/IdeaDetail.vue'
import { useConfirm } from '../composables/useConfirm'
@@ -214,19 +214,7 @@ const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
{ key: 'promoted', labelKey: 'ideas.filter.promoted', icon: '🚀' },
]
// ── 状态映射(单一数据源,列表/详情/下拉共用) ──
const statusOptions: { value: IdeaStatus; labelKey: string }[] = [
{ value: 'draft', labelKey: 'ideas.status.draft' },
{ value: 'pending_review', labelKey: 'ideas.status.pending_review' },
{ value: 'approved', labelKey: 'ideas.status.approved' },
{ value: 'promoted', labelKey: 'ideas.status.promoted' },
{ value: 'rejected', labelKey: 'ideas.status.rejected' },
]
// 任意 status 字符串 → 对应 i18n key;未知状态回退到原值显示
function statusLabelKey(status: IdeaStatus): string {
return statusOptions.find(s => s.value === status)?.labelKey ?? status
}
// ── 状态映射(单一数据源在 @/constants/project,列表/详情/下拉共用) ──
// ── 后端查询构造status/keyword/order_by 下沉后端 WHERE) ──
//
@@ -260,11 +248,11 @@ function loadCurrentView() {
// 让 store.loading 驱动「加载中」空态显示(对齐 Tasks.vue 三态)。
// G5.3:keyword 防抖 timer 用 useTimerOwnership 每实例管理 + onUnmounted 自动清理,
// 根治卸载后 300ms 内 timer 触发 → store.loadIdeas 写单例 store(setState-after-unmount)。
const { setOwnTimeout, clearOwn } = useTimerOwnership()
const { setOwnTimeout, clearOwnTimeout } = useTimerOwnership()
let keywordDebounce: ReturnType<typeof setTimeout> | null = null
async function reloadIdeas(immediate = false) {
if (keywordDebounce) {
clearOwn(keywordDebounce)
clearOwnTimeout(keywordDebounce)
keywordDebounce = null
}
const run = () => loadCurrentView()
@@ -444,8 +432,8 @@ async function onStatusChange(newStatus: IdeaStatus) {
if (needsConfirm) {
const msg = t('ideas.confirmStatus', {
title: currentIdea.value.title,
from: t(statusOptions.find(s => s.value === from)?.labelKey ?? from),
to: t(statusOptions.find(s => s.value === newStatus)?.labelKey ?? newStatus),
from: t(ideaStatusLabelKey(from)),
to: t(ideaStatusLabelKey(newStatus)),
})
if (!await confirmDialog(msg, t('common.confirm'))) return
}