diff --git a/src/api/ai.ts b/src/api/ai.ts index bb03fcd..2a677d7 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -103,8 +103,8 @@ export const aiApi = { /** 查询某对话积压的待审批工具(重启后恢复 toolCard pending_approval 态用)。 * 返 kind 字段(risk/path),前端按 kind 渲染不同决策按钮。 */ - pendingToolCalls(convId: string): Promise<{ tool_call_id: string; conversation_id: string | null; kind: 'risk' | 'path' }[]> { - return invoke('ai_pending_tool_calls', { convId }) + pendingToolCalls(conversationId: string): Promise<{ tool_call_id: string; conversation_id: string | null; kind: 'risk' | 'path' }[]> { + return invoke('ai_pending_tool_calls', { conversationId }) }, /** 清空对话 */ @@ -251,9 +251,14 @@ export const aiApi = { return invoke('ai_set_agent_max_retries', { value }) }, - /** 列出本机 Claude 技能(skills + commands + plugins) */ - listSkills(): Promise { - return invoke('ai_list_skills') + /** + * 列出本机 Claude 技能(project + skills + commands + plugins)。 + * @param projectPath 当前项目根目录(可选)。传入时后端额外扫项目级 + * `.claude/skills` 各技能目录下的 SKILL.md(source="project",同名覆盖 home 级); + * 不传沿用已设项目根(无则仅 home 级)。 + */ + listSkills(projectPath?: string): Promise { + return invoke('ai_list_skills', { projectPath: projectPath ?? null }) }, /** diff --git a/src/api/settings.ts b/src/api/settings.ts index 77bee67..cd39924 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -38,6 +38,6 @@ export const settingsApi = { /** 读取多 ReAct Plan 执行开关 */ getPlanExecution(): Promise { - return invoke('get_plan_execution') + return invoke('is_plan_execution_enabled') }, } diff --git a/src/components/ToolCardList.vue b/src/components/ToolCardList.vue index 50c4685..7ffae73 100644 --- a/src/components/ToolCardList.vue +++ b/src/components/ToolCardList.vue @@ -52,7 +52,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue' import { useI18n } from 'vue-i18n' import ToolCard from './ToolCard.vue' import { listen, type UnlistenFn } from '@tauri-apps/api/event' -import { aggregateCalls, aggText } from '@/composables/ai/useTurnTrace' +import { aggregateCalls, turnSummaryText } from '@/composables/ai/useTurnTrace' import type { AiToolCallInfo } from '@/api/types' // useI18n needed for $t() in template (collapseAll / expandAll) + groupDisplayName(i18n key) @@ -230,7 +230,7 @@ function groupDisplayName(group: ToolCallGroup): string { /** 分组聚合行文案:「N 次调用 · 成功 x 失败 y」(零值项省略;与单卡状态点同源 isToolFailure 判定) */ function groupAggText(group: ToolCallGroup): string { - return aggText(aggregateCalls(group.calls)) + return turnSummaryText(aggregateCalls(group.calls)) } /** diff --git a/src/components/ai/MessageList.vue b/src/components/ai/MessageList.vue index aba9b20..5ab8017 100644 --- a/src/components/ai/MessageList.vue +++ b/src/components/ai/MessageList.vue @@ -21,7 +21,7 @@ import { useMarkdown } from '../../composables/useMarkdown' import { useMessageScroll } from '../../composables/ai/useMessageScroll' import { useStreamRenderer } from '../../composables/ai/useStreamRenderer' import { loadMoreHistory, getLoadMoreState } from '../../composables/ai/useAiConversations' -import { collectTurnStats, aggText, type TurnCallStats } from '../../composables/ai/useTurnTrace' +import { collectTurnStats, turnSummaryText, type TurnCallStats } from '../../composables/ai/useTurnTrace' import ToolCardList from '../ToolCardList.vue' import MessageItem from './MessageItem.vue' import EmptyState from './EmptyState.vue' @@ -741,9 +741,9 @@ function toggleTrace(key: string): void { expandedTraceIds.value = next } -/** 轨迹卡聚合行:「N 次调用 · 成功 x 失败 y · 总耗时 T」(拼装统一走 useTurnTrace.aggText) */ +/** 轨迹卡聚合行:「N 次调用 · 成功 x 失败 y · 总耗时 T」(拼装统一走 useTurnTrace.turnSummaryText) */ function traceAggText(stats: TurnCallStats, startTs: number, endTs: number): string { - return aggText(stats, endTs - startTs) + return turnSummaryText(stats, endTs - startTs) } /** 轨迹卡 hover:绝对起止时刻(相对时长之外可溯源具体时间) */ diff --git a/src/components/ideas/IdeaDetail.vue b/src/components/ideas/IdeaDetail.vue index 1048e8b..4538dc0 100644 --- a/src/components/ideas/IdeaDetail.vue +++ b/src/components/ideas/IdeaDetail.vue @@ -19,7 +19,7 @@ > 🚀 {{ $t('ideas.promotedProject') }} → - {{ $t(statusLabelKey(idea.status)) }} + {{ $t(ideaStatusLabelKey(idea.status)) }} @@ -268,7 +268,7 @@ :class="'status-' + idea.status" @click="statusMenuOpen = !statusMenuOpen" > - {{ $t(statusLabelKey(idea.status)) }} + {{ $t(ideaStatusLabelKey(idea.status)) }} ▾
    @@ -306,6 +306,7 @@ import { useRendered } from '../../composables/useMarkdown' import { ideaApi } from '../../api' import { formatDate } from '../../utils/time' import { parseScores, assessmentClass, assessmentLabel, scoreTier } from '../../utils/ideaEval' +import { IDEA_STATUS_OPTIONS, ideaStatusLabelKey } from '../../constants/project' import type { IdeaRecord, IdeaStatus, IdeaEvaluationRecord } from '../../api/types' const props = defineProps<{ @@ -314,7 +315,6 @@ const props = defineProps<{ evalError: string promoting: boolean deleting: boolean - statusOptions: { value: IdeaStatus; labelKey: string }[] }>() const emit = defineEmits<{ @@ -390,11 +390,11 @@ watch(() => props.idea.id, () => { watch(() => props.idea.description, () => void nextTick(measureDesc)) // P1-④ 状态切换 select → badge 点击:点击当前 badge 展开/收起下拉菜单, -// 选中项后 emit('status-change') 交父组件确认+落库(沿用既有契约,不破坏 statusOptions 来源)。 +// 选中项后 emit('status-change') 交父组件确认+落库(沿用既有契约)。 const statusMenuOpen = ref(false) // 状态下拉排除 promoted(立项走独立 promoteToProject 流程,列表状态菜单同排除, -// 见 Ideas.vue onStatusChange 兜底);statusOptions 仍含 promoted 供 label 映射(已转立项的灵感 badge 正常显示)。 -const menuStatusOptions = computed(() => props.statusOptions.filter(s => s.value !== 'promoted')) +// 见 Ideas.vue onStatusChange 兜底);状态表(含 promoted 供 badge label 映射)单一数据源在 @/constants/project。 +const menuStatusOptions = computed(() => IDEA_STATUS_OPTIONS.filter(s => s.value !== 'promoted')) function closeStatusMenu() { statusMenuOpen.value = false } @@ -515,10 +515,7 @@ function cancelTagsEdit() { tagsEditing.value = false } -// 任意 status 字符串 → 对应 i18n key;未知状态回退到原值显示 -function statusLabelKey(status: IdeaStatus): string { - return props.statusOptions.find(s => s.value === status)?.labelKey ?? status -} +// 任意 status → i18n key 的映射走 @/constants/project ideaStatusLabelKey(与列表同源) // 灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例), // useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热) diff --git a/src/components/knowledge/KnowledgeDetail.vue b/src/components/knowledge/KnowledgeDetail.vue index 428b4df..cf9bbf1 100644 --- a/src/components/knowledge/KnowledgeDetail.vue +++ b/src/components/knowledge/KnowledgeDetail.vue @@ -147,6 +147,7 @@ import { knowledgeConfidenceLabel as confidenceLabel, } from '@/stores/knowledge' import { useRendered } from '@/composables/useMarkdown' +import { formatRelative } from '@/utils/time' import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types' // 详情面板子组件(Knowledge.vue 拆分,SW-260618-20): @@ -317,20 +318,12 @@ const timelineNodes = computed(() => { return nodes }) -// ===== 标签/时间辅助(kind*/status/confidence label 由 @/stores/knowledge 共用,相对时间本组件自留 30 天截止)===== -// 相对时间:复用 common.* 既有 key(justNow/minutesAgo/hoursAgo/dayAgo),30 天以上回退绝对值 +// ===== 标签/时间辅助(kind*/status/confidence label 由 @/stores/knowledge 共用)===== +// 相对时间:走 @/utils/time formatRelative(30 天以上回退绝对日期,档位与全局同源) function relativeTime(millisStr: string): string { const ms = Number(millisStr) if (!ms) return '' - const diff = Date.now() - ms - const min = Math.floor(diff / 60000) - if (min < 1) return t('common.justNow') - if (min < 60) return t('common.minutesAgo', { n: min }) - const hr = Math.floor(min / 60) - if (hr < 24) return t('common.hoursAgo', { n: hr }) - const day = Math.floor(hr / 24) - if (day < 30) return t('common.dayAgo', { n: day }) - return t('common.ago', { time: new Date(ms).toLocaleDateString() }) + return formatRelative(ms, { absAfterDays: 30 }) } // 切换知识时重置引用列表「加载更多」上限 + 折叠态(对齐原 Knowledge.vue selectKnowledge 内 refLimit=10 行为) diff --git a/src/components/project/GitChanges.vue b/src/components/project/GitChanges.vue index ba88b18..ac0b779 100644 --- a/src/components/project/GitChanges.vue +++ b/src/components/project/GitChanges.vue @@ -165,6 +165,7 @@ */ import { ref, computed, watch } from 'vue' import { moduleApi, type GitStatusResult } from '@/api/module' +import { formatRelative } from '@/utils/time' import { useI18n } from 'vue-i18n' const { t } = useI18n() @@ -274,19 +275,10 @@ function statusLabel(s: string): string { return x.charAt(0) || '?' } -/** 格式化 Unix 时间戳为相对时间或具体时间 */ +/** 格式化 Unix 时间戳为相对时间或具体时间(阶梯走 @/utils/time formatRelative:昨天档 + 30 天截断 ISO 日期) */ function formatTime(ts: number): string { if (!ts) return '' - const now = Math.floor(Date.now() / 1000) - const diffSec = now - ts - if (diffSec < 60) return t('common.justNow') - if (diffSec < 3600) return t('common.minutesAgo', { n: Math.floor(diffSec / 60) }) - if (diffSec < 86400) return t('common.hoursAgo', { n: Math.floor(diffSec / 3600) }) - if (diffSec < 172800) return t('common.yesterday') - if (diffSec < 2592000) return t('common.dayAgo', { n: Math.floor(diffSec / 86400) }) - // 超过 30 天显示具体日期 - const d = new Date(ts * 1000) - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` + return formatRelative(ts, { yesterday: true, absAfterDays: 30, absStyle: 'iso' }) } /** 计算文件路径的目录深度(用于缩进)。root 级文件返回 0。 */ diff --git a/src/composables/ai/useToolApproval.ts b/src/composables/ai/useToolApproval.ts index 885a42e..ae36117 100644 --- a/src/composables/ai/useToolApproval.ts +++ b/src/composables/ai/useToolApproval.ts @@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n' import { useConfirm } from '@/composables/useConfirm' import { aiApi } from '@/api' import { getApprovalRemainingMs } from './aiShared' +import { formatWaitTime, waitLevelOf, type WaitLevel } from '@/utils/time' import type { AiToolCallInfo } from '@/api/types' /** @@ -115,17 +116,9 @@ export function useToolApproval(tc: Ref, emit: ToolApprovalEmit) waitSecs.value = 0 } - 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')}` - } + // formatWaitTime/waitLevelOf 收敛至 @/utils/time(原与 ApprovalPopup 逐字双写) - const waitLevel = computed<'normal' | 'long' | 'urgent'>(() => { - if (waitSecs.value >= 300) return 'urgent' // 5分钟 - if (waitSecs.value >= 120) return 'long' // 2分钟 - return 'normal' - }) + const waitLevel = computed(() => waitLevelOf(waitSecs.value)) // ── 审批到期倒计时(BUG-260624-02)── // 剩余 = aiShared 审批超时计时器真实起点推算(自动拒绝时刻精确对齐,非本地近似)。 diff --git a/src/composables/ai/useTurnTrace.ts b/src/composables/ai/useTurnTrace.ts index acde9fc..b0ef05a 100644 --- a/src/composables/ai/useTurnTrace.ts +++ b/src/composables/ai/useTurnTrace.ts @@ -96,7 +96,7 @@ export function statsTail(s: TurnCallStats): string { * 头部 total=0 显「过程记录」;零值段省略;durationMs 可选(轨迹卡传起止差,分组行不传)。 * 供 MessageList 轨迹卡与 ToolCardList 聚合分组行共用,拼装语义单一出口。 */ -export function aggText(s: TurnCallStats, durationMs?: number): string { +export function turnSummaryText(s: TurnCallStats, durationMs?: number): string { const parts: string[] = [] parts.push(s.total > 0 ? t('ai.trace.calls', { n: s.total }) : t('ai.trace.processOnly')) const tail = statsTail(s) diff --git a/src/composables/useTimerOwnership.ts b/src/composables/useTimerOwnership.ts index 37b96c9..8c240f6 100644 --- a/src/composables/useTimerOwnership.ts +++ b/src/composables/useTimerOwnership.ts @@ -16,7 +16,7 @@ export function useTimerOwnership() { const timeouts = new Set>() const intervals = new Set>() - /** 注册一次性 timeout;触发后自动从注册表移除。返回可交给 clearOwn 的 id。 */ + /** 注册一次性 timeout;触发后自动从注册表移除。返回可交给 clearOwnTimeout 的 id。 */ function setOwnTimeout(fn: () => void, ms: number) { const id = setTimeout(() => { timeouts.delete(id) @@ -34,7 +34,7 @@ export function useTimerOwnership() { } /** 清理指定 timeout(id 为 null/undefined 时 no-op)。 */ - function clearOwn(id?: ReturnType | null) { + function clearOwnTimeout(id?: ReturnType | null) { if (id != null) { clearTimeout(id) timeouts.delete(id) @@ -56,5 +56,5 @@ export function useTimerOwnership() { intervals.clear() }) - return { setOwnTimeout, setOwnInterval, clearOwn, clearOwnInterval } + return { setOwnTimeout, setOwnInterval, clearOwnTimeout, clearOwnInterval } } diff --git a/src/composables/useToast.ts b/src/composables/useToast.ts index ff7df57..a8733c2 100644 --- a/src/composables/useToast.ts +++ b/src/composables/useToast.ts @@ -34,14 +34,14 @@ export function useToast(defaultDurationMs = 3000) { // 会清掉组件 A 的 timer,致 A 的 toast 永不隐藏(跨实例 bug)。 // 已下沉为「每实例独立 timer」:useTimerOwnership 每实例注册表 + onUnmounted 只清自己。 // 不再保留模块级兜底 —— 模块级共享正是串扰根源,每实例自清理即正确语义。 - const { setOwnTimeout, clearOwn } = useTimerOwnership() + const { setOwnTimeout, clearOwnTimeout } = useTimerOwnership() let _timer: ReturnType | null = null function showToast(msg: string, type: ToastType = 'info', durationMs?: number) { toast.msg = msg toast.type = type toast.visible = true - if (_timer) clearOwn(_timer) + if (_timer) clearOwnTimeout(_timer) _timer = setOwnTimeout(() => { _timer = null toast.visible = false @@ -50,7 +50,7 @@ export function useToast(defaultDurationMs = 3000) { function hideToast() { if (_timer) { - clearOwn(_timer) + clearOwnTimeout(_timer) _timer = null } toast.visible = false diff --git a/src/constants/project.ts b/src/constants/project.ts index dbc08b5..7f3a282 100644 --- a/src/constants/project.ts +++ b/src/constants/project.ts @@ -4,6 +4,8 @@ //! 根治此前三套状态映射互相矛盾、任务文案两处不一致的问题。 //! 项目进度不在此处(按 stage 拍百分比已废弃),由任务完成率计算,见 store.projectTaskProgress。 +import type { IdeaStatus } from '@/api/types' + // ── 项目状态 ── // 值为 i18n key,实际文案走 $t('projects.status.')(见 src/i18n/{zh-CN,en}/projects.ts) @@ -221,3 +223,21 @@ export function priorityLabel(p: number): string { export function priorityClass(p: number): string { return PRIORITY_CLASSES[p] ?? 'priority-low' } + +// ── 灵感状态 ── + +// 值为 i18n key,实际文案走 $t(见 src/i18n/{zh-CN,en}/ideas.ts)。 +// 单一数据源:Ideas.vue 列表 badge/确认文案 与 IdeaDetail.vue 详情 badge/状态下拉共用 +// (原 Ideas.vue 自持 + 经 props 透传 IdeaDetail 双写,已收敛至此;archived 无菜单入口不入表)。 +export const IDEA_STATUS_OPTIONS: { 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;未知状态(含 archived)回退到原值显示 */ +export function ideaStatusLabelKey(status: IdeaStatus): string { + return IDEA_STATUS_OPTIONS.find(s => s.value === status)?.labelKey ?? status +} diff --git a/src/mocks/tauri-shim.ts b/src/mocks/tauri-shim.ts index 990a488..aa6591a 100644 --- a/src/mocks/tauri-shim.ts +++ b/src/mocks/tauri-shim.ts @@ -253,7 +253,7 @@ const SCALAR_COMMANDS: Record unknown> = { settings_get_all: () => ({}), settings_set: () => true, settings_delete: () => true, - get_plan_execution: () => false, + is_plan_execution_enabled: () => false, get_data_dir: () => 'C:\\devflow-browser-mock-data', ai_is_generating: () => false, list_tool_executions: () => ({ items: [], total: 0, has_more: false }), diff --git a/src/utils/time.ts b/src/utils/time.ts index 531d4ef..7ab48ea 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -44,8 +44,19 @@ export function formatDate(v: string | number | null | undefined): string { return d.toLocaleDateString(locale) + ' ' + d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }) } -/** 相对时间(走 i18n:justNow/minutesAgo/hoursAgo/dayAgo;en/zh 双语);无效返回 '—' */ -export function formatRelative(v: string | number | null | undefined): string { +/** 相对时间分档差异参数(formatRelative 选项;不传 = 原默认阶梯) */ +export interface FormatRelativeOpts { + /** 开启「昨天」档:24~48h 显示 common.yesterday(如 GitChanges 提交时间),默认关 */ + yesterday?: boolean + /** 超过 N 天回退绝对日期;缺省不截断,一直显示「N 天前」 */ + absAfterDays?: number + /** 绝对日期回退样式:'ago' = 「{date}前」(i18n,默认);'iso' = YYYY-MM-DD(定宽,列表对齐用) */ + absStyle?: 'ago' | 'iso' +} + +/** 相对时间(走 i18n:justNow/minutesAgo/hoursAgo/dayAgo;en/zh 双语);无效返回 '—'。 + * 分档差异(yesterday 档 / N 天截断回退绝对日期)经 opts 表达,不传保持原行为 */ +export function formatRelative(v: string | number | null | undefined, opts?: FormatRelativeOpts): string { const ms = parseTs(v) if (ms == null) return '—' const diffMin = Math.floor((Date.now() - ms) / 60000) @@ -54,5 +65,31 @@ export function formatRelative(v: string | number | null | undefined): string { const diffH = Math.floor(diffMin / 60) if (diffH < 24) return t('common.hoursAgo', { n: diffH }) const diffD = Math.floor(diffH / 24) + if (opts?.yesterday && diffD < 2) return t('common.yesterday') + if (opts?.absAfterDays != null && diffD >= opts.absAfterDays) { + const d = new Date(ms) + if (opts.absStyle === 'iso') { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` + } + return t('common.ago', { time: d.toLocaleDateString() }) + } return t('common.dayAgo', { n: diffD }) } + +// ── 审批等待时长(ToolCard / ApprovalPopup 双写收敛) ── + +export type WaitLevel = 'normal' | 'long' | 'urgent' + +/** 秒 → MM:SS(分秒两位补零,审批挂起时长展示) */ +export 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')}` +} + +/** 等待分级阈值(与后端 APPROVAL_TIMEOUT_MS 5min 静默拒绝对齐):≥300s urgent / ≥120s long */ +export function waitLevelOf(secs: number): WaitLevel { + if (secs >= 300) return 'urgent' + if (secs >= 120) return 'long' + return 'normal' +} diff --git a/src/views/ApprovalPopup.vue b/src/views/ApprovalPopup.vue index f3597b7..fef93f8 100644 --- a/src/views/ApprovalPopup.vue +++ b/src/views/ApprovalPopup.vue @@ -55,7 +55,7 @@ -

    {{ descPreviews.get(idea.id) || '' }}

    @@ -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 | 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 }