重构: 前端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>
|
||||
|
||||
Reference in New Issue
Block a user