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未动
120 lines
3.8 KiB
Vue
120 lines
3.8 KiB
Vue
<template>
|
|
<div class="project-card" @click="router.push('/projects/' + project.id)">
|
|
<div class="card-top">
|
|
<div class="card-title-row">
|
|
<h2 class="card-name">{{ project.name }}</h2>
|
|
</div>
|
|
<div class="card-top-actions">
|
|
<span class="stage-badge" :class="stageClass(project.status)">{{ $t(statusLabel(project.status)) }}</span>
|
|
<button class="card-del-btn" :title="$t('projects.deleteTitle')" @click.stop="emit('delete', project)">🗑</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="card-desc">{{ stripMd(project.description) }}</p>
|
|
|
|
<!-- 底部信息 -->
|
|
<div class="card-footer">
|
|
<div class="footer-stat">
|
|
<span class="stat-icon">📅</span>
|
|
<span>{{ formatDate(project.created_at) }}</span>
|
|
</div>
|
|
<div class="footer-stat">
|
|
<span class="stat-icon">🕐</span>
|
|
<span>{{ formatDate(project.updated_at) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 技术栈标签 -->
|
|
<div class="card-tags" v-if="parseStack(project.stack).length">
|
|
<span class="tech-tag" v-for="t in parseStack(project.stack)" :key="t">{{ t }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
// 项目列表卡片 — 从 Projects.vue 抽出(单卡片渲染 + 导航 + 删除触发)。
|
|
// 依赖均为无副作用纯函数 + 共享常量;删除走 emit 交回父组件处理(confirm 弹层 + store 调用)。
|
|
import { useRouter } from 'vue-router'
|
|
import { formatDate } from '@/utils/time'
|
|
import { parseStack } from '@/utils/project'
|
|
import { stripMd } from '@/utils/markdown'
|
|
import { projectStatusLabel as statusLabel, projectBadgeClass as stageClass } from '@/constants/project'
|
|
import type { ProjectRecord } from '@/api/types'
|
|
|
|
defineProps<{ project: ProjectRecord }>()
|
|
const emit = defineEmits<{
|
|
(e: 'delete', project: ProjectRecord): void
|
|
}>()
|
|
|
|
const router = useRouter()
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 仅迁入卡片自身样式;列表网格/页面壳样式留 Projects.vue(网格布局属列表职责) */
|
|
.project-card {
|
|
background: var(--df-bg-card);
|
|
border: 0.5px solid var(--df-border);
|
|
border-radius: var(--df-radius-lg);
|
|
padding: var(--df-pad-panel);
|
|
transition: all 0.15s;
|
|
cursor: pointer;
|
|
}
|
|
.project-card:hover {
|
|
border-color: var(--df-accent);
|
|
}
|
|
|
|
.card-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
.card-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.card-name { font-size: 17px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
.stage-badge {
|
|
font-size: 12px; font-weight: 500;
|
|
padding: 3px 10px;
|
|
border-radius: var(--df-radius-sm);
|
|
}
|
|
.stage-design { background: rgba(100,181,246,0.15); color: var(--df-info); }
|
|
.stage-coding { background: rgba(108,99,255,0.15); color: var(--df-accent); }
|
|
.stage-testing { background: rgba(255,217,61,0.15); color: var(--df-warning); }
|
|
.stage-release { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
|
|
|
.card-desc {
|
|
font-size: 13px;
|
|
color: var(--df-text-secondary);
|
|
line-height: 1.5;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.card-footer {
|
|
display: flex;
|
|
gap: var(--df-gap-grid);
|
|
margin-bottom: 14px;
|
|
}
|
|
.footer-stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
color: var(--df-text-dim);
|
|
}
|
|
.stat-icon { font-size: 13px; }
|
|
|
|
.card-top-actions { display: flex; align-items: center; gap: 8px; }
|
|
.card-del-btn {
|
|
background: transparent; border: none; cursor: pointer;
|
|
font-size: 14px; padding: 2px 4px; opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
/* hover 态依赖 .project-card 父级 — scoped 下 :deep 不适用于兄弟伪类,改用 group 思路:卡片 hover 时按钮可见 */
|
|
.project-card:hover .card-del-btn { opacity: 0.55; }
|
|
.card-del-btn:hover { opacity: 1 !important; }
|
|
</style>
|