重构: 前端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 { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import { parseTs } from '@/utils/time'
|
import { formatRelativeZh } from '@/utils/time'
|
||||||
|
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const { t } = useI18n()
|
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
|
return store.tasks.filter(t => t.project_id === projectId && t.status === 'in_progress').length
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLastActivity(iso: string): string {
|
// 相对时间复用 utils/time.formatRelativeZh(与 Tasks/AuditLog/MessageList 等同源,根治 NaN)
|
||||||
const ms = parseTs(iso)
|
// 原 formatLastActivity 是其逐行复制,提取为单一来源。
|
||||||
if (ms == null) return '—'
|
const formatLastActivity = formatRelativeZh
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayProjects = computed(() =>
|
const displayProjects = computed(() =>
|
||||||
store.projects.map(p => {
|
store.projects.map(p => {
|
||||||
|
|||||||
@@ -107,7 +107,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 { useRendered } from '@/composables/useMarkdown'
|
||||||
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types'
|
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types'
|
||||||
|
|
||||||
@@ -244,27 +250,7 @@ const timelineNodes = computed(() => {
|
|||||||
return nodes
|
return nodes
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 标签/时间辅助(从 Knowledge.vue 迁移,行为一致)=====
|
// ===== 标签/时间辅助(kind*/status/confidence label 由 @/stores/knowledge 共用,相对时间本组件自留 30 天截止)=====
|
||||||
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
|
|
||||||
}
|
|
||||||
// 相对时间:复用 common.* 既有 key(justNow/minutesAgo/hoursAgo/dayAgo),30 天以上回退绝对值
|
// 相对时间:复用 common.* 既有 key(justNow/minutesAgo/hoursAgo/dayAgo),30 天以上回退绝对值
|
||||||
function relativeTime(millisStr: string): string {
|
function relativeTime(millisStr: string): string {
|
||||||
const ms = Number(millisStr)
|
const ms = Number(millisStr)
|
||||||
|
|||||||
@@ -107,20 +107,6 @@ const router = useRouter()
|
|||||||
}
|
}
|
||||||
.stat-icon { font-size: 13px; }
|
.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-top-actions { display: flex; align-items: center; gap: 8px; }
|
||||||
.card-del-btn {
|
.card-del-btn {
|
||||||
background: transparent; border: none; cursor: pointer;
|
background: transparent; border: none; cursor: pointer;
|
||||||
|
|||||||
@@ -87,27 +87,9 @@ const reviewSuggestions = computed<string[]>(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||||
/* 基础布局 .info-item / .info-block / .label / .value:Vue scoped 不穿透子组件后代 DOM,
|
/* 基础布局 .info-item / .info-block / .label / .value 已收敛至全局 components.css
|
||||||
父级 TaskDetail.vue 的 .info-item .label 等后代选择器不会作用到本子组件内部元素,
|
(DRY 收口 B-260619)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||||
故此处复制等价基础布局(与 TaskDetail.vue L559-577 一致),保持视觉零变化。 */
|
故本子组件无需复制等价基础布局,仅保留本组件特有的 output/review 样式。 */
|
||||||
.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; }
|
|
||||||
|
|
||||||
.output-block .output-body {
|
.output-block .output-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -12,11 +12,30 @@
|
|||||||
|
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import { state } from '@/stores/ai'
|
import { state } from '@/stores/ai'
|
||||||
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import type { AiToolCallInfo } from '@/api/types'
|
import type { AiToolCallInfo } from '@/api/types'
|
||||||
|
|
||||||
|
const appSettings = useAppSettingsStore()
|
||||||
|
|
||||||
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。
|
||||||
|
*
|
||||||
|
* df-ai-language='auto' → 回落到 df-language(应用主语言);否则用 df-ai-language 指定值;
|
||||||
|
* 兜底 'zh-CN'。
|
||||||
|
*
|
||||||
|
* 原分别在 useAiSend.resolveLang(三元)与 useAiContext.resolveLanguage(if/else)各一份,
|
||||||
|
* 语义相同仅写法不同,提取至此共用。
|
||||||
|
*/
|
||||||
|
export function resolveAiLang(): string {
|
||||||
|
const raw = appSettings.get<string>('df-ai-language', 'auto')
|
||||||
|
return raw === 'auto'
|
||||||
|
? appSettings.get<string>('df-language', 'zh-CN')
|
||||||
|
: raw
|
||||||
|
}
|
||||||
|
|
||||||
/** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */
|
/** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */
|
||||||
let _msgCounter = 0
|
let _msgCounter = 0
|
||||||
|
|
||||||
|
|||||||
@@ -24,15 +24,13 @@
|
|||||||
//! 耦合:
|
//! 耦合:
|
||||||
//! - aiApi.clearContext / aiApi.compressContext(IPC)
|
//! - aiApi.clearContext / aiApi.compressContext(IPC)
|
||||||
//! - state.activeConversationId(读当前对话)
|
//! - state.activeConversationId(读当前对话)
|
||||||
//! - appSettings.get(df-ai-language/df-language)(压缩语言;与 useAiSend.sendMessage 同源)
|
//! - resolveAiLang(压缩语言;与 useAiSend.sendMessage 同源,实现在 aiShared)
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
||||||
import { state } from '@/stores/ai'
|
import { state } from '@/stores/ai'
|
||||||
|
import { resolveAiLang } from './aiShared'
|
||||||
const appSettings = useAppSettingsStore()
|
|
||||||
|
|
||||||
/// 压缩中 loading(防重复点击;AiCompressing→true / AiCompressed|AiError→false)
|
/// 压缩中 loading(防重复点击;AiCompressing→true / AiCompressed|AiError→false)
|
||||||
const isCompressing = ref(false)
|
const isCompressing = ref(false)
|
||||||
@@ -108,20 +106,6 @@ function stopContextListener(): void {
|
|||||||
_callbacks = {}
|
_callbacks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析压缩语言(与 useAiSend.sendMessage 同源逻辑):
|
|
||||||
* - df-ai-language='auto' → df-language(应用主语言)
|
|
||||||
* - 否则用 df-ai-language 指定值
|
|
||||||
* - 兜底 'zh-CN'
|
|
||||||
*/
|
|
||||||
function resolveLanguage(): string {
|
|
||||||
const raw = appSettings.get<string>('df-ai-language', 'auto')
|
|
||||||
if (raw === 'auto') {
|
|
||||||
return appSettings.get<string>('df-language', 'zh-CN')
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 压缩当前对话上下文。
|
* 压缩当前对话上下文。
|
||||||
*
|
*
|
||||||
@@ -136,7 +120,7 @@ async function compressContext(): Promise<void> {
|
|||||||
if (isCompressing.value) return // 防重复点击
|
if (isCompressing.value) return // 防重复点击
|
||||||
isCompressing.value = true
|
isCompressing.value = true
|
||||||
try {
|
try {
|
||||||
await aiApi.compressContext(convId, resolveLanguage())
|
await aiApi.compressContext(convId, resolveAiLang())
|
||||||
// 后端 IPC 同步等到 LLM 压缩完成才 resolve(非 spawn 异步),成功即复位 isCompressing;
|
// 后端 IPC 同步等到 LLM 压缩完成才 resolve(非 spawn 异步),成功即复位 isCompressing;
|
||||||
// case 'AiCompressed' 事件也复位(双保险,防事件丢失致按钮永久禁用)
|
// case 'AiCompressed' 事件也复位(双保险,防事件丢失致按钮永久禁用)
|
||||||
isCompressing.value = false
|
isCompressing.value = false
|
||||||
|
|||||||
@@ -19,31 +19,15 @@ import { listen } from '@tauri-apps/api/event'
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
||||||
import { state } from '@/stores/ai'
|
import { state } from '@/stores/ai'
|
||||||
import type { ContentPart, AiMessage } from '@/api/types'
|
import type { ContentPart, AiMessage } from '@/api/types'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||||
import { startListener } from './useAiEvents'
|
import { startListener } from './useAiEvents'
|
||||||
import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers } from './aiShared'
|
import { nextMsgId, findToolCall, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang } from './aiShared'
|
||||||
|
|
||||||
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
||||||
|
|
||||||
const appSettings = useAppSettingsStore()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* B-260617-03 DRY: 解析 AI 回复语言。
|
|
||||||
*
|
|
||||||
* df-ai-language='auto' → 回落到 df-language;否则用 df-ai-language 指定值。
|
|
||||||
* doSend/regenerate/editMessage 三处复用(原各内联同一段表达式)。
|
|
||||||
*/
|
|
||||||
function resolveLang(): string {
|
|
||||||
const raw = appSettings.get<string>('df-ai-language', 'auto')
|
|
||||||
return raw === 'auto'
|
|
||||||
? appSettings.get<string>('df-language', 'zh-CN')
|
|
||||||
: raw
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 待发送队列上限(超过抛错提示用户)
|
/// 待发送队列上限(超过抛错提示用户)
|
||||||
const QUEUE_LIMIT = 10
|
const QUEUE_LIMIT = 10
|
||||||
|
|
||||||
@@ -114,7 +98,7 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
|
|||||||
resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底
|
resetStreamWatchdog() // 启动流式看门狗,无数据超时兜底
|
||||||
|
|
||||||
await startListener()
|
await startListener()
|
||||||
const lang = resolveLang()
|
const lang = resolveAiLang()
|
||||||
try {
|
try {
|
||||||
// force=true 走 force_send IPC(复位残留 generating),否则走正常 send
|
// force=true 走 force_send IPC(复位残留 generating),否则走正常 send
|
||||||
// F-01 阶段6: 透传 modelOverride(主对话专用,后端兜底校验池内才用)
|
// F-01 阶段6: 透传 modelOverride(主对话专用,后端兜底校验池内才用)
|
||||||
@@ -170,7 +154,7 @@ async function regenerate() {
|
|||||||
resetStreamWatchdog()
|
resetStreamWatchdog()
|
||||||
|
|
||||||
await startListener()
|
await startListener()
|
||||||
const lang = resolveLang()
|
const lang = resolveAiLang()
|
||||||
const convId = state.activeConversationId
|
const convId = state.activeConversationId
|
||||||
if (!convId) {
|
if (!convId) {
|
||||||
// 无活跃对话:回滚占位,报错
|
// 无活跃对话:回滚占位,报错
|
||||||
@@ -240,7 +224,7 @@ async function editMessage(newMessage: string) {
|
|||||||
resetStreamWatchdog()
|
resetStreamWatchdog()
|
||||||
|
|
||||||
await startListener()
|
await startListener()
|
||||||
const lang = resolveLang()
|
const lang = resolveAiLang()
|
||||||
const convId = state.activeConversationId
|
const convId = state.activeConversationId
|
||||||
if (!convId) {
|
if (!convId) {
|
||||||
state.streaming = false
|
state.streaming = false
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import router from "./router";
|
|||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
import "./styles/global.css";
|
import "./styles/global.css";
|
||||||
import "./styles/ai-md.css";
|
import "./styles/ai-md.css";
|
||||||
|
import "./styles/components.css";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,40 @@ export function parseTags(tags: string | null): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 知识 label 文案辅助(列表 Knowledge.vue 与详情 KnowledgeDetail.vue 共用) ──
|
||||||
|
// 原两文件各内联一份重复实现(SW-260618-20 详情迁移时复制),提取至此单一来源。
|
||||||
|
// 全部依赖 i18n,故用模块级 t(同文件 parseTags 之上的全局 t)。
|
||||||
|
|
||||||
|
/** 知识类型文案(无 icon):${icon} ${text} 组合的文本部分,亦供下拉选项 */
|
||||||
|
export function kindText(key: string): string {
|
||||||
|
return t(`knowledge.kind.${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 知识类型带 icon 文案(列表/详情卡片);未知 key 回退原值 */
|
||||||
|
export function kindLabel(key: string): string {
|
||||||
|
const k = KNOWLEDGE_KINDS.find(c => c.key === key)
|
||||||
|
return k ? `${k.icon} ${kindText(key)}` : key
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 知识类型 icon;未知回退通用图标 */
|
||||||
|
export function kindIcon(key: string): string {
|
||||||
|
return KNOWLEDGE_KINDS.find(c => c.key === key)?.icon ?? '📄'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 知识状态文案;$t(key) 命中失败(无该 status key)回退原值,避免裸露 key */
|
||||||
|
export function knowledgeStatusLabel(status: string): string {
|
||||||
|
const key = `knowledge.status.${status}`
|
||||||
|
const msg = t(key)
|
||||||
|
return msg === key ? status : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 知识置信度文案;$t(key) 命中失败回退原值 */
|
||||||
|
export function knowledgeConfidenceLabel(c: string): string {
|
||||||
|
const key = `knowledge.confidence.${c}`
|
||||||
|
const msg = t(key)
|
||||||
|
return msg === key ? c : msg
|
||||||
|
}
|
||||||
|
|
||||||
export function useKnowledgeStore() {
|
export function useKnowledgeStore() {
|
||||||
// ── 加载列表(library:默认仅 published,F-260616-02 决策 a) ──
|
// ── 加载列表(library:默认仅 published,F-260616-02 决策 a) ──
|
||||||
// 后端 knowledge_list 默认收窄为 published,无需前端传 status。
|
// 后端 knowledge_list 默认收窄为 published,无需前端传 status。
|
||||||
|
|||||||
79
src/styles/components.css
Normal file
79
src/styles/components.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
DevFlow 公共组件样式(全局,非 scoped)— DRY 收口
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
背景:多个视图/组件 <style scoped> 重复定义相同规则块,且 Vue scoped
|
||||||
|
不跨组件渗透(子组件内部 DOM 拿不到父级 scoped 后代选择器),被迫逐处复制。
|
||||||
|
本文件提取高频重复且**完全一致**的样式为全局类,各组件删除 scoped 重复块。
|
||||||
|
全局类与各页 template class 名一致,零侵入。
|
||||||
|
|
||||||
|
提取原则:
|
||||||
|
- 仅提取 ≥2 处逐字相同的规则(已 grep 比对一致)
|
||||||
|
- scoped 故意 override 全局值的(如 .empty-hint padding 24px vs 全局 12px)
|
||||||
|
不提取,保留各组件 scoped 覆盖(零视觉变化)
|
||||||
|
- global.css 已有的 .btn/.modal-*/.page-header/.empty-hint/.empty-state
|
||||||
|
不在此重复,仅补充尚未全局化的类
|
||||||
|
|
||||||
|
落地批:DRY 收口(B-260619)
|
||||||
|
- .info-item / .info-item.info-block / .info-item .label / .info-item .value
|
||||||
|
← ProjectDetail.vue / TaskDetail.vue / TaskOutputCard.vue 三处复制
|
||||||
|
- .panel / .panel-header ← ProjectDetail.vue / TaskDetail.vue 两处复制
|
||||||
|
- .tech-tag / .card-tags ← ProjectCard.vue / Projects.vue 两处复制
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* — Info Item:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状 —
|
||||||
|
原 ProjectDetail.vue L439-456 / TaskDetail.vue L560-577 /
|
||||||
|
TaskOutputCard.vue L93-110 三处逐字一致(后者注释说明 scoped 不渗透故复制)。 */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
/* — Panel:内嵌面板容器(卡片底 + 头部行)—
|
||||||
|
原 ProjectDetail.vue L617-630 / TaskDetail.vue L539-551 两处逐字一致。
|
||||||
|
.task-count 为 ProjectDetail 特有,留组件 scoped。 */
|
||||||
|
.panel {
|
||||||
|
background: var(--df-bg-card);
|
||||||
|
border: 0.5px solid var(--df-border);
|
||||||
|
border-radius: var(--df-radius-lg);
|
||||||
|
padding: var(--df-pad-panel);
|
||||||
|
}
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--df-gap-head);
|
||||||
|
}
|
||||||
|
.panel-header h2 { font-size: 15px; font-weight: 500; }
|
||||||
|
|
||||||
|
/* — Tech Tags:技术栈标签行 + 单标签 —
|
||||||
|
原 ProjectCard.vue L110-122 / Projects.vue L471-483 两处逐字一致。
|
||||||
|
ProjectCard 为 scoped 组件,Projects.vue 为创建/导入模态复用;统一全局后
|
||||||
|
两处均生效(scoped 下全局类对当前组件根/子 DOM 仍生效,仅 scoped 属性选择器
|
||||||
|
不渗透到子组件内部 — 此处标签是当前组件自身 DOM,无渗透问题)。 */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
@@ -145,7 +145,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useKnowledgeStore, KNOWLEDGE_KINDS } from '@/stores/knowledge'
|
import {
|
||||||
|
useKnowledgeStore,
|
||||||
|
KNOWLEDGE_KINDS,
|
||||||
|
kindText,
|
||||||
|
kindLabel,
|
||||||
|
knowledgeStatusLabel as statusLabel,
|
||||||
|
knowledgeConfidenceLabel as confidenceLabel,
|
||||||
|
} from '@/stores/knowledge'
|
||||||
import { useRendered } from '@/composables/useMarkdown'
|
import { useRendered } from '@/composables/useMarkdown'
|
||||||
import { stripMd } from '@/utils/markdown'
|
import { stripMd } from '@/utils/markdown'
|
||||||
import KnowledgeDetail from '@/components/knowledge/KnowledgeDetail.vue'
|
import KnowledgeDetail from '@/components/knowledge/KnowledgeDetail.vue'
|
||||||
@@ -165,10 +172,6 @@ const categories = [
|
|||||||
function catLabel(key: string): string {
|
function catLabel(key: string): string {
|
||||||
return key === 'all' ? t('knowledge.categoryAll') : kindText(key)
|
return key === 'all' ? t('knowledge.categoryAll') : kindText(key)
|
||||||
}
|
}
|
||||||
// 纯类型文案(无 icon),用于下拉/分类
|
|
||||||
function kindText(key: string): string {
|
|
||||||
return t(`knowledge.kind.${key}`)
|
|
||||||
}
|
|
||||||
const activeKind = ref('all')
|
const activeKind = ref('all')
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -331,20 +334,7 @@ async function submitCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 列表卡片标签辅助(详情相关 label/时间解析已迁移至 KnowledgeDetail 子组件)=====
|
// ===== 列表卡片标签辅助(详情相关 label/时间解析已迁移至 KnowledgeDetail 子组件)=====
|
||||||
function kindLabel(key: string): string {
|
// kindText/kindLabel/statusLabel/confidenceLabel 由 @/stores/knowledge 提供(列表/详情共用,单一来源)
|
||||||
const k = KNOWLEDGE_KINDS.find(c => c.key === key)
|
|
||||||
return k ? `${k.icon} ${kindText(key)}` : key
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
|
||||||
|
|||||||
@@ -435,26 +435,9 @@ onUnmounted(() => {
|
|||||||
gap: var(--df-gap-grid);
|
gap: var(--df-gap-grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* B-260615-31:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状 */
|
/* B-260615-31:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状)
|
||||||
.info-item {
|
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
|
||||||
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 a.idea-link {
|
.info-item a.idea-link {
|
||||||
color: var(--df-accent);
|
color: var(--df-accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -489,11 +472,8 @@ onUnmounted(() => {
|
|||||||
.path-ok { color: var(--df-success); font-size: 12px; }
|
.path-ok { color: var(--df-success); font-size: 12px; }
|
||||||
.path-missing { color: var(--df-danger); font-size: 12px; }
|
.path-missing { color: var(--df-danger); font-size: 12px; }
|
||||||
.info-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
.info-tags { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
.info-tags .tech-tag {
|
/* .info-tags .tech-tag 与全局 .tech-tag(components.css)逐字一致,移除局部冗余,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 项目描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
/* ===== 项目描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
||||||
.description.ai-md { font-size: 14px; color: var(--df-text); line-height: 1.6; }
|
.description.ai-md { font-size: 14px; color: var(--df-text); line-height: 1.6; }
|
||||||
@@ -614,19 +594,8 @@ onUnmounted(() => {
|
|||||||
.right-column { display: flex; flex-direction: column; gap: var(--df-gap-grid); }
|
.right-column { display: flex; flex-direction: column; gap: var(--df-gap-grid); }
|
||||||
|
|
||||||
/* ===== 面板 ===== */
|
/* ===== 面板 ===== */
|
||||||
.panel {
|
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||||
background: var(--df-bg-card);
|
此处仅保留本组件特有 .task-count。 */
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
padding: var(--df-pad-panel);
|
|
||||||
}
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-head);
|
|
||||||
}
|
|
||||||
.panel-header h2 { font-size: 15px; font-weight: 500; }
|
|
||||||
.task-count { font-size: 12px; color: var(--df-text-dim); }
|
.task-count { font-size: 12px; color: var(--df-text-dim); }
|
||||||
|
|
||||||
/* ===== 任务卡片 ===== */
|
/* ===== 任务卡片 ===== */
|
||||||
|
|||||||
@@ -446,7 +446,6 @@ async function runImport() {
|
|||||||
|
|
||||||
/* ===== 进度条 ===== */
|
/* ===== 进度条 ===== */
|
||||||
.card-progress { margin-bottom: 16px; }
|
.card-progress { margin-bottom: 16px; }
|
||||||
.card-progress { margin-bottom: 16px; }
|
|
||||||
.progress-label-row {
|
.progress-label-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -467,20 +466,8 @@ async function runImport() {
|
|||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 技术栈标签(创建/导入模态复用,保留;卡片自身样式随 ProjectCard.vue 迁出) ===== */
|
/* ===== 技术栈标签:基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||||
.card-tags {
|
创建/导入模态与本页 ProjectCard 渲染的标签均走全局 .tech-tag/.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 目录选择行 ===== */
|
/* ===== 目录选择行 ===== */
|
||||||
.dir-row { display: flex; gap: 8px; align-items: center; }
|
.dir-row { display: flex; gap: 8px; align-items: center; }
|
||||||
|
|||||||
@@ -536,19 +536,7 @@ onBeforeUnmount(() => {
|
|||||||
/* ===== 面板 ===== */
|
/* ===== 面板 ===== */
|
||||||
.detail-grid { display: grid; grid-template-columns: 1fr; gap: var(--df-gap-grid); }
|
.detail-grid { display: grid; grid-template-columns: 1fr; gap: var(--df-gap-grid); }
|
||||||
|
|
||||||
.panel {
|
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619)。 */
|
||||||
background: var(--df-bg-card);
|
|
||||||
border: 0.5px solid var(--df-border);
|
|
||||||
border-radius: var(--df-radius-lg);
|
|
||||||
padding: var(--df-pad-panel);
|
|
||||||
}
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--df-gap-head);
|
|
||||||
}
|
|
||||||
.panel-header h2 { font-size: 15px; font-weight: 500; }
|
|
||||||
|
|
||||||
/* ===== 信息列表 ===== */
|
/* ===== 信息列表 ===== */
|
||||||
.task-info {
|
.task-info {
|
||||||
@@ -556,25 +544,9 @@ onBeforeUnmount(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--df-gap-grid);
|
gap: var(--df-gap-grid);
|
||||||
}
|
}
|
||||||
/* B-260615-31:字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状 */
|
/* B-260615-31:字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状)
|
||||||
.info-item {
|
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
|
||||||
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; }
|
|
||||||
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li) */
|
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li) */
|
||||||
.info-item .value.description { line-height: 1.5; }
|
.info-item .value.description { line-height: 1.5; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user