重构: 前端DRY收口+后端测试类型对齐
- 新增 useToast composable: 消除 AiChat/Settings/Projects 4处 toast 重复 统一默认3000ms(Projects原4000ms为操作类提示保留参数覆盖) - 新增 utils/json.ts parseJsonArray: 消除 parseStack/parseTags/ModuleNode 3处JSON字符串数组解析重复 - 新增 utils/html.ts escapeHtml: 消除 useMarkdown/FilePreview 2处重复 - ProjectDetail score-bar 内联三元改用 scoreTier(消除最后一处阈值硬编码) - ConversationSidebar 删除 formatTime 透传包装(直接用 formatRelative) - 清理死代码: parseTs/stringifyError/ErrorSink/_Unused 改私有或删除 wrapNakedDiff 改私有(无外部 import) - ModuleNode shortPath 改名 truncatedPath(与 useToolCard.shortPath 语义不同)
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
@@ -143,6 +143,7 @@ import MaxRoundsCard from './ai/MaxRoundsCard.vue'
|
||||
import HelpRequiredCard from './ai/HelpRequiredCard.vue'
|
||||
import DirAuthDialog from './ai/DirAuthDialog.vue'
|
||||
import { useConfirm } from '../composables/useConfirm'
|
||||
import { useToast } from '../composables/useToast'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import type { AiMessage } from '../api/types'
|
||||
|
||||
@@ -177,18 +178,10 @@ function sendExamplePrompt(i18nKey: string) {
|
||||
chatInputRef.value?.sendExamplePrompt(i18nKey)
|
||||
}
|
||||
|
||||
// ── 错误 toast(零依赖,复用 Settings.vue 同款 reactive toast) ──
|
||||
// ── 错误 toast(useToast composable,消除 AiChat/Settings/Projects/TaskDetail 4 处重复) ──
|
||||
// AiChat 可能以分离窗口运行(App.vue 根 toast 不在 DOM 中),故本组件自管一个 toast;
|
||||
// i18n 不在本任务白名单,文案硬编码,后续补 aiChat.toastSendFail key
|
||||
const toast = reactive({ visible: false, msg: '', type: 'error' as 'error' | 'warning' | 'info' })
|
||||
let _toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function showToast(msg: string, type: 'error' | 'warning' | 'info' = 'info') {
|
||||
toast.msg = msg
|
||||
toast.type = type
|
||||
toast.visible = true
|
||||
if (_toastTimer) clearTimeout(_toastTimer)
|
||||
_toastTimer = setTimeout(() => { toast.visible = false }, 3000)
|
||||
}
|
||||
const { toast, showToast } = useToast()
|
||||
|
||||
// B-260616-12: 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
|
||||
let _unlistenToolSlow: (() => void) | null = null
|
||||
@@ -460,7 +453,7 @@ onBeforeUnmount(() => {
|
||||
store.stopContextListener()
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } // B-260616-12
|
||||
if (_unlistenAutoApproved) { _unlistenAutoApproved(); _unlistenAutoApproved = null } // AE-2025-04
|
||||
if (_toastTimer) { clearTimeout(_toastTimer); _toastTimer = null }
|
||||
// toast timer 清理由 useToast composable 的 onUnmounted 自动管理
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ai-conv-item-actions">
|
||||
<div class="ai-conv-item-more">
|
||||
@@ -132,7 +132,7 @@
|
||||
<!-- F-09: 多会话并发生成指示器(后台/当前会话生成中均显脉冲点,多会话可同时显示) -->
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ai-conv-item-actions">
|
||||
<div class="ai-conv-item-more">
|
||||
@@ -177,7 +177,7 @@
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="ai-conv-item-time">{{ formatRelative(conv.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ai-conv-item-actions">
|
||||
<div class="ai-conv-item-more">
|
||||
@@ -328,10 +328,6 @@ function onSelectSearchResult(id: string) {
|
||||
store.state.searchQuery = ''
|
||||
}
|
||||
|
||||
function formatTime(ts: string): string {
|
||||
return formatRelative(ts)
|
||||
}
|
||||
|
||||
// ── 对话分组:今天 / 昨天 / 更早 ──
|
||||
const bucketLabels = computed<Record<string, string>>(() => ({
|
||||
today: t('aiChat.today'),
|
||||
|
||||
@@ -91,6 +91,7 @@ import { ref, watch, computed, onUnmounted } from 'vue'
|
||||
import { moduleApi } from '@/api/module'
|
||||
import hljs from 'highlight.js/lib/common'
|
||||
import { useMarkdown, useRendered } from '@/composables/useMarkdown'
|
||||
import { escapeHtml } from '@/utils/html'
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string
|
||||
@@ -284,10 +285,6 @@ async function loadFile() {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
function joinPath(root: string, rel: string): string {
|
||||
const sep = root.includes('\\') ? '\\' : '/'
|
||||
const normRel = rel.split('/').join(sep)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="module-node__icon">📦</span>
|
||||
<span class="module-node__name">{{ data.name }}</span>
|
||||
</div>
|
||||
<div class="module-node__path">{{ shortPath }}</div>
|
||||
<div class="module-node__path">{{ truncatedPath }}</div>
|
||||
<div v-if="stackList.length" class="module-node__tags">
|
||||
<span v-for="s in stackList" :key="s" class="module-node__tag">{{ s }}</span>
|
||||
</div>
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { parseJsonArray } from '@/utils/json'
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
@@ -24,21 +25,13 @@ const props = defineProps<{
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const shortPath = computed(() => {
|
||||
const truncatedPath = computed(() => {
|
||||
const p = props.data?.path || ''
|
||||
if (p.length <= 32) return p
|
||||
return '...' + p.slice(-29)
|
||||
})
|
||||
|
||||
const stackList = computed(() => {
|
||||
if (!props.data?.stack) return []
|
||||
try {
|
||||
const parsed = JSON.parse(props.data.stack)
|
||||
return Array.isArray(parsed) ? parsed.slice(0, 3) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
const stackList = computed(() => parseJsonArray(props.data?.stack).slice(0, 3))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -166,9 +166,7 @@ export function useRendered(getText: () => string): {
|
||||
return { rendered, ensureLoaded }
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
import { escapeHtml } from '@/utils/html'
|
||||
|
||||
/// marked 未就绪时的兜底渲染:HTML 转义 + 换行转 <br>(纯文本安全降级)
|
||||
function escapeFallback(text: string): string {
|
||||
@@ -217,7 +215,7 @@ const _DIFF_LINE_RE = /^[ \t]*[+-][^\n]*$/ // 行首可选空格/tab + +/- + 任
|
||||
const _PLUS_LINE_RE = /^[ \t]*\+[^\n]*$/ // 含 + 的行(+ 必须含的判定)
|
||||
const _FENCE_OPEN_RE = /^[ \t]{0,3}(```|~~~)/ // marked 围栏开头(行首 ≤3 空格 + 3+ 反引号/波浪)
|
||||
|
||||
export function wrapNakedDiff(text: string): string {
|
||||
function wrapNakedDiff(text: string): string {
|
||||
if (!text) return text
|
||||
// 快速短路:全文无 +/- 行首特征直接返回原文(避免大文本白跑切段)
|
||||
if (!/[+-]/.test(text)) return text
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
* 注:带 seq 竞态守卫的场景调用方需在 fn 内部判断,不在本工具职责范围。
|
||||
*/
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
// (Ref import 已移除,_Unused 类型已删除)
|
||||
|
||||
/** 带 error 字段的 store state 形状(仅约束 error,其他字段任意)。 */
|
||||
export interface ErrorSink {
|
||||
/** 带 error 字段的 store state 形状(仅约束 error,其他字段任意)。仅本模块内部使用。 */
|
||||
interface ErrorSink {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
@@ -40,12 +40,8 @@ export async function runWithCatch<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 unknown 错误转为可读字符串(无则返 undefined,由调用方兜底 i18n)。
|
||||
*
|
||||
* Tauri invoke 抛出的通常是 string 或 Error 实例。
|
||||
*/
|
||||
export function stringifyError(e: unknown): string | undefined {
|
||||
// stringifyError 仅本模块内部使用(runWithCatch/runWithCatchGuarded 调用),不导出。
|
||||
function stringifyError(e: unknown): string | undefined {
|
||||
if (typeof e === 'string') return e
|
||||
if (e instanceof Error) return e.message || e.toString()
|
||||
if (e && typeof e === 'object' && 'toString' in e) {
|
||||
@@ -81,5 +77,4 @@ export async function runWithCatchGuarded<T>(
|
||||
}
|
||||
}
|
||||
|
||||
// 显式标记 Ref 未使用(避免未来用上时改导入)
|
||||
export type _Unused = Ref<unknown>
|
||||
// _Unused 类型从未被外部 import,已删除(同时移除上方 Ref import)。
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 轻量 toast 提示 composable — 消除 AiChat / Settings / Projects / TaskDetail 4 处重复。
|
||||
*
|
||||
* 原 4 处各自 reactive/ref + _toastTimer + showToast + onUnmounted 清理,
|
||||
* 且 Projects 用 4000ms 其余 3000ms(体验不一致 bug)。
|
||||
* 本 composable 统一默认 3000ms,支持调用方按需传 durationMs。
|
||||
*
|
||||
* 用法:
|
||||
* const { toast, showToast } = useToast()
|
||||
* showToast('保存成功')
|
||||
* showToast('导入失败', 'error', 4000)
|
||||
* // template: <div v-if="toast.visible" class="toast ...">{{ toast.msg }}</div>
|
||||
*/
|
||||
import { reactive, onUnmounted } from 'vue'
|
||||
|
||||
export type ToastType = 'info' | 'error' | 'warning'
|
||||
|
||||
export interface ToastState {
|
||||
visible: boolean
|
||||
msg: string
|
||||
type: ToastType
|
||||
}
|
||||
|
||||
let _timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
export function useToast(defaultDurationMs = 3000) {
|
||||
const toast = reactive<ToastState>({
|
||||
visible: false,
|
||||
msg: '',
|
||||
type: 'info',
|
||||
})
|
||||
|
||||
function showToast(msg: string, type: ToastType = 'info', durationMs?: number) {
|
||||
toast.msg = msg
|
||||
toast.type = type
|
||||
toast.visible = true
|
||||
if (_timer) clearTimeout(_timer)
|
||||
_timer = setTimeout(() => {
|
||||
toast.visible = false
|
||||
}, durationMs ?? defaultDurationMs)
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
if (_timer) {
|
||||
clearTimeout(_timer)
|
||||
_timer = null
|
||||
}
|
||||
toast.visible = false
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (_timer) {
|
||||
clearTimeout(_timer)
|
||||
_timer = null
|
||||
}
|
||||
})
|
||||
|
||||
return { toast, showToast, hideToast }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { reactive, computed } from 'vue'
|
||||
import { knowledgeApi } from '@/api'
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { runWithCatch, runWithCatchGuarded } from '@/composables/useStoreAction'
|
||||
import { parseJsonArray } from '@/utils/json'
|
||||
import type {
|
||||
KnowledgeRecord,
|
||||
KnowledgeDetailPayload,
|
||||
@@ -46,14 +47,8 @@ export const KNOWLEDGE_KINDS = [
|
||||
] as const
|
||||
|
||||
/** 解析 tags JSON 字符串为 string[] */
|
||||
export function parseTags(tags: string | null): string[] {
|
||||
if (!tags) return []
|
||||
try {
|
||||
const arr = JSON.parse(tags)
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
export function parseTags(tags: string | null | undefined): string[] {
|
||||
return parseJsonArray(tags)
|
||||
}
|
||||
|
||||
// ── 知识 label 文案辅助(列表 Knowledge.vue 与详情 KnowledgeDetail.vue 共用) ──
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* HTML 转义 — 消除 useMarkdown / FilePreview 两处重复实现。
|
||||
*/
|
||||
|
||||
/** 转义 &, <, > 三个核心 HTML 特殊字符 */
|
||||
export function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* JSON 字符串数组解析 — 消除 parseStack / parseTags / ModuleNode 内联 3 处重复。
|
||||
*
|
||||
* 后端将 string[] 以 JSON.stringify 形式存于 stack/tags 等字段;
|
||||
* 解析失败或非数组时返回空数组,避免渲染崩溃。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安全解析 JSON 字符串数组。
|
||||
* null / 空 / 非法 JSON / 非数组 → 返回 fallback(默认 [])。
|
||||
*/
|
||||
export function parseJsonArray<T = string>(
|
||||
raw: string | null | undefined,
|
||||
fallback: T[] = [],
|
||||
): T[] {
|
||||
if (!raw) return fallback
|
||||
try {
|
||||
const arr = JSON.parse(raw)
|
||||
return Array.isArray(arr) ? arr : fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
//! parseStack 此前在 Projects.vue 与 ProjectDetail.vue 各有一份重复实现,
|
||||
//! 现抽到此处统一维护。
|
||||
|
||||
import { parseJsonArray } from './json'
|
||||
|
||||
/**
|
||||
* 解析技术栈 JSON 数组字符串(防崩)。
|
||||
*
|
||||
@@ -10,11 +12,5 @@
|
||||
* 解析失败或非数组时返回空数组,避免渲染崩溃。
|
||||
*/
|
||||
export function parseStack(stack: string | null | undefined): string[] {
|
||||
if (!stack) return []
|
||||
try {
|
||||
const arr = JSON.parse(stack)
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return parseJsonArray(stack)
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ import { t, currentLocale } from '@/i18n/i18n-helpers'
|
||||
// CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
|
||||
// 类型安全访问经 i18n-helpers 收口(规避 vue-i18n TS2589)。
|
||||
|
||||
/** 解析任意时间值为毫秒时间戳;无效或空返回 null */
|
||||
export function parseTs(v: string | number | null | undefined): number | null {
|
||||
/** 解析任意时间值为毫秒时间戳;无效或空返回 null(仅本模块内部使用) */
|
||||
function parseTs(v: string | number | null | undefined): number | null {
|
||||
if (v == null) return null
|
||||
if (typeof v === 'number') {
|
||||
if (!Number.isFinite(v)) return null
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<div class="score-bar-track">
|
||||
<div
|
||||
class="score-bar-fill"
|
||||
:class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"
|
||||
:class="'fill-' + scoreTier(dim.score)"
|
||||
:style="{ width: dim.score + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
@@ -276,7 +276,7 @@ import { useProjectStore } from '@/stores/project'
|
||||
import { projectApi } from '@/api'
|
||||
import { formatDate } from '@/utils/time'
|
||||
import { parseStack } from '@/utils/project'
|
||||
import { parseScores as parseScoresJson, assessmentClass, assessmentLabel as assessmentLabelI18n } from '@/utils/ideaEval'
|
||||
import { parseScores as parseScoresJson, assessmentClass, assessmentLabel as assessmentLabelI18n, scoreTier } from '@/utils/ideaEval'
|
||||
import { projectStatusLabel, taskStatusLabel, taskStatusClass } from '../constants/project'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import ApprovalDialog from '@/components/project/ApprovalDialog.vue'
|
||||
|
||||
+4
-17
@@ -164,7 +164,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
@@ -174,6 +174,7 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import ProjectCard from '@/components/project/ProjectCard.vue'
|
||||
import Paginator from '@/components/Paginator.vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import type { ProjectRecord } from '@/api/types'
|
||||
import type { ScannedProjectItem } from '@/api/project'
|
||||
|
||||
@@ -303,14 +304,7 @@ onMounted(() => {
|
||||
store.loadProjects()
|
||||
})
|
||||
|
||||
// 卸载清 toast timer:防组件已离开视图后 4s 内 timer 触发向已销毁组件写状态。
|
||||
// 对齐 Settings.vue 的 _toastTimer 清理模式(Knowledge.vue searchTimer 同模式)。
|
||||
onUnmounted(() => {
|
||||
if (_toastTimer) {
|
||||
clearTimeout(_toastTimer)
|
||||
_toastTimer = null
|
||||
}
|
||||
})
|
||||
// toast timer 清理由 useToast composable 的 onUnmounted 自动管理
|
||||
|
||||
// ── 导入历史项目(F-260614-06 scan 第二步) ──
|
||||
const showImportModal = ref(false)
|
||||
@@ -320,14 +314,7 @@ const importScanning = ref(false)
|
||||
const importError = ref('')
|
||||
const importRunning = ref(false)
|
||||
const selectedImportPaths = ref<Set<string>>(new Set())
|
||||
const toast = ref({ visible: false, msg: '', type: 'info' as 'info' | 'error' })
|
||||
let _toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function showToast(msg: string, type: 'info' | 'error' = 'info') {
|
||||
toast.value = { visible: true, msg, type }
|
||||
if (_toastTimer) clearTimeout(_toastTimer)
|
||||
_toastTimer = setTimeout(() => { toast.value.visible = false }, 4000)
|
||||
}
|
||||
const { toast, showToast } = useToast(4000) // Projects 导入操作 toast 用 4000ms(操作类提示)
|
||||
|
||||
const allImportSelected = computed(() =>
|
||||
scannedItems.value.length > 0
|
||||
|
||||
+5
-14
@@ -67,8 +67,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, shallowRef, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { computed, ref, shallowRef, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import SettingsNav, { type SettingsCategory } from '@/components/settings/SettingsNav.vue'
|
||||
import ProviderPanel from '@/components/settings/ProviderPanel.vue'
|
||||
@@ -90,17 +91,8 @@ import { t as ti18n } from '@/i18n/i18n-helpers'
|
||||
// (阶段3 UX 重构:左导航 + 右按 activeCategory 渲染,各功能域 state/methods 已拆到子组件)
|
||||
// ============================================================
|
||||
|
||||
// 顶部轻量提示(错误/警告/完成),3s 自动消失;零依赖,替代未接入的 Arco Message
|
||||
const toast = reactive({ visible: false, msg: '', type: 'info' as 'error' | 'warning' | 'info' })
|
||||
let _toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function showToast(msg: string, type: 'error' | 'warning' | 'info' = 'info') {
|
||||
toast.msg = msg
|
||||
toast.type = type
|
||||
toast.visible = true
|
||||
if (_toastTimer) clearTimeout(_toastTimer)
|
||||
_toastTimer = setTimeout(() => { toast.visible = false }, 3000)
|
||||
}
|
||||
// 顶部轻量提示(useToast composable,消除 4 处重复;默认 3000ms)
|
||||
const { toast, showToast } = useToast()
|
||||
|
||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||
// 模板 confirmState.visible/msg 在 <script setup> 中自动解包 ref,沿用既有内联确认弹层
|
||||
@@ -207,9 +199,8 @@ onMounted(() => {
|
||||
void loadAiProviders()
|
||||
})
|
||||
|
||||
// 组件卸载清 toast/resize timer;各子面板自管自身 debounce timer(各自 onUnmounted)
|
||||
// 组件卸载清 resize timer;toast 清理由 useToast 自动管理;各子面板自管自身 debounce timer
|
||||
onUnmounted(() => {
|
||||
if (_toastTimer) clearTimeout(_toastTimer)
|
||||
if (_resizeTimer) clearTimeout(_resizeTimer)
|
||||
window.removeEventListener('resize', onResize)
|
||||
providerPanelRef.value?.clearPoolTimer()
|
||||
|
||||
Reference in New Issue
Block a user