重构: 前端DRY(style全局沉淀 + ts/js公共沉淀)
style-dry: - 新增 src/styles/components.css: .info-item*/.panel/.panel-header/.tech-tag/.card-tags 全局 - main.ts 注册 import - 删重复: ProjectDetail/TaskDetail/TaskOutputCard/ProjectCard/Projects(scoped删重复, 全局对子组件生效) - 未使用清理: Projects .card-progress dead(1处保守核对) tsjs-dry: - aiShared.ts: resolveAiLang(合并useAiSend.resolveLang + useAiContext.resolveLanguage) - stores/knowledge.ts: kindText/kindLabel/kindIcon/knowledgeStatusLabel/knowledgeConfidenceLabel(5公共) - 删重复: useAiSend/useAiContext(resolveLang) + Knowledge/KnowledgeDetail(4+5 helper) + ActiveProjectsPanel(formatLastActivity→formatRelativeZh复用) strategy: DRY收口, 公共沉淀(utils/store/shared); relativeTime非纯重复保留; .btn/.empty-hint局部override WATCH未动
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { parseTs } from '@/utils/time'
|
||||
import { formatRelativeZh } from '@/utils/time'
|
||||
|
||||
const store = useProjectStore()
|
||||
const { t } = useI18n()
|
||||
@@ -59,17 +59,9 @@ function getProjectTaskCount(projectId: string): number {
|
||||
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length
|
||||
}
|
||||
|
||||
function formatLastActivity(iso: string): string {
|
||||
const ms = parseTs(iso)
|
||||
if (ms == null) return '—'
|
||||
const diffMin = Math.floor((Date.now() - ms) / 60000)
|
||||
if (diffMin < 1) return t('common.justNow')
|
||||
if (diffMin < 60) return t('common.minutesAgo', { n: diffMin })
|
||||
const diffH = Math.floor(diffMin / 60)
|
||||
if (diffH < 24) return t('common.hoursAgo', { n: diffH })
|
||||
const diffD = Math.floor(diffH / 24)
|
||||
return t('common.dayAgo', { n: diffD })
|
||||
}
|
||||
// 相对时间复用 utils/time.formatRelativeZh(与 Tasks/AuditLog/MessageList 等同源,根治 NaN)
|
||||
// 原 formatLastActivity 是其逐行复制,提取为单一来源。
|
||||
const formatLastActivity = formatRelativeZh
|
||||
|
||||
const displayProjects = computed(() =>
|
||||
store.projects.map(p => {
|
||||
|
||||
@@ -107,7 +107,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { KNOWLEDGE_KINDS, parseTags } from '@/stores/knowledge'
|
||||
import {
|
||||
parseTags,
|
||||
kindLabel,
|
||||
kindIcon,
|
||||
knowledgeStatusLabel as statusLabel,
|
||||
knowledgeConfidenceLabel as confidenceLabel,
|
||||
} from '@/stores/knowledge'
|
||||
import { useRendered } from '@/composables/useMarkdown'
|
||||
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types'
|
||||
|
||||
@@ -244,27 +250,7 @@ const timelineNodes = computed(() => {
|
||||
return nodes
|
||||
})
|
||||
|
||||
// ===== 标签/时间辅助(从 Knowledge.vue 迁移,行为一致)=====
|
||||
function kindLabel(key: string): string {
|
||||
const k = KNOWLEDGE_KINDS.find(c => c.key === key)
|
||||
return k ? `${k.icon} ${kindText(key)}` : key
|
||||
}
|
||||
function kindText(key: string): string {
|
||||
return t(`knowledge.kind.${key}`)
|
||||
}
|
||||
function kindIcon(key: string): string {
|
||||
return KNOWLEDGE_KINDS.find(c => c.key === key)?.icon ?? '📄'
|
||||
}
|
||||
function statusLabel(status: string): string {
|
||||
const key = `knowledge.status.${status}`
|
||||
const msg = t(key)
|
||||
return msg === key ? status : msg
|
||||
}
|
||||
function confidenceLabel(c: string): string {
|
||||
const key = `knowledge.confidence.${c}`
|
||||
const msg = t(key)
|
||||
return msg === key ? c : msg
|
||||
}
|
||||
// ===== 标签/时间辅助(kind*/status/confidence label 由 @/stores/knowledge 共用,相对时间本组件自留 30 天截止)=====
|
||||
// 相对时间:复用 common.* 既有 key(justNow/minutesAgo/hoursAgo/dayAgo),30 天以上回退绝对值
|
||||
function relativeTime(millisStr: string): string {
|
||||
const ms = Number(millisStr)
|
||||
|
||||
@@ -107,20 +107,6 @@ const router = useRouter()
|
||||
}
|
||||
.stat-icon { font-size: 13px; }
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tech-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(108, 99, 255, 0.08);
|
||||
color: var(--df-text-secondary);
|
||||
border: 0.5px solid var(--df-border);
|
||||
}
|
||||
|
||||
.card-top-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.card-del-btn {
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
|
||||
@@ -87,27 +87,9 @@ const reviewSuggestions = computed<string[]>(() => {
|
||||
|
||||
<style scoped>
|
||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||
/* 基础布局 .info-item / .info-block / .label / .value:Vue scoped 不穿透子组件后代 DOM,
|
||||
父级 TaskDetail.vue 的 .info-item .label 等后代选择器不会作用到本子组件内部元素,
|
||||
故此处复制等价基础布局(与 TaskDetail.vue L559-577 一致),保持视觉零变化。 */
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
.info-item.info-block {
|
||||
flex-direction: column;
|
||||
}
|
||||
.info-item .label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 88px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info-item .value { font-size: 14px; color: var(--df-text); flex: 1; min-width: 0; }
|
||||
/* 基础布局 .info-item / .info-block / .label / .value 已收敛至全局 components.css
|
||||
(DRY 收口 B-260619)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||
故本子组件无需复制等价基础布局,仅保留本组件特有的 output/review 样式。 */
|
||||
|
||||
.output-block .output-body {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user