优化: 工程实体化(description/stack/status)+ 概览工程列表

- project_modules 加 description/status 列(V40 迁移,stack V34 已有)+ models/repo/INSERT/UPDATE/SELECT 映射

- FileExplorer 工程 CRUD 弹窗加 description(textarea)/stack(input)/status(select)输入,工程有完整身份

- ProjectDetail 概览工程列表升级:name+status badge+path + description 段(2行省略)+ stack tag

- 老工程兼容(None=active),update 部分更新语义,status 归一(active/archived)
This commit is contained in:
lxy
2026-08-05 22:10:07 +08:00
parent 8e4c7c68cf
commit ec9f0bf1ea
9 changed files with 551 additions and 47 deletions
+12
View File
@@ -19,6 +19,10 @@ export interface ProjectModuleRecord {
sort_order: number
created_at: string
updated_at: string
/** 工程职责描述(如"前端 web 工程""后端 API 服务")。V40 加,老工程为 null。 */
description?: string | null
/** 工程状态 active/archived。V40 加,老工程为 null(应用层视 null 为 active)。 */
status?: string | null
}
/** 文件树单条目(单层;前端点击文件夹再懒加载下一层)。 */
@@ -98,6 +102,10 @@ export const moduleApi = {
path: string
gitUrl?: string | null
stack?: string | null
/** 工程职责描述(V40 加) */
description?: string | null
/** 工程状态 active/archived(V40 加,默认 active) */
status?: string | null
}): Promise<ProjectModuleRecord> {
return invoke('add_project_module', { input })
},
@@ -155,6 +163,10 @@ export const moduleApi = {
path?: string
gitUrl?: string | null
stack?: string | null
/** 工程职责描述(V40 加,部分更新:仅传入时覆盖) */
description?: string | null
/** 工程状态 active/archived(V40 加,部分更新:仅传入时覆盖) */
status?: string | null
}): Promise<ProjectModuleRecord> {
return invoke('update_project_module', { input })
},
+40 -4
View File
@@ -138,9 +138,28 @@
<input v-model="moduleFormPath" :placeholder="$t('fileExplorer.modulePathPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleGitUrl') }} <span class="opt-label">{{ $t('common.optional') }}</span></label>
<label>{{ $t('fileExplorer.moduleGitUrl') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<input v-model="moduleFormGitUrl" :placeholder="$t('fileExplorer.moduleGitUrlPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleDescription') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<textarea
v-model="moduleFormDescription"
:placeholder="$t('fileExplorer.moduleDescriptionPlaceholder')"
rows="2"
></textarea>
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleStack') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<input v-model="moduleFormStack" :placeholder="$t('fileExplorer.moduleStackPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleStatus') }}</label>
<select v-model="moduleFormStatus">
<option value="active">{{ $t('fileExplorer.moduleStatusActive') }}</option>
<option value="archived">{{ $t('fileExplorer.moduleStatusArchived') }}</option>
</select>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" @click="showModuleModal = false">{{ $t('common.cancel') }}</button>
<button class="btn btn-primary" :disabled="savingModule || !moduleFormName.trim() || !moduleFormPath.trim()" @click="onSaveModule">
@@ -206,6 +225,10 @@ const savingModule = ref(false)
const moduleFormName = ref('')
const moduleFormPath = ref('')
const moduleFormGitUrl = ref('')
// V40 工程实体化:职责描述 / 技术栈(逗号分隔) / 状态(active/archived)
const moduleFormDescription = ref('')
const moduleFormStack = ref('')
const moduleFormStatus = ref<'active' | 'archived'>('active')
// 删除确认
const showRemoveConfirm = ref(false)
@@ -409,6 +432,9 @@ function openAddModule() {
moduleFormName.value = ''
moduleFormPath.value = ''
moduleFormGitUrl.value = ''
moduleFormDescription.value = ''
moduleFormStack.value = ''
moduleFormStatus.value = 'active'
showModuleModal.value = true
}
@@ -419,6 +445,10 @@ function openEditModule() {
moduleFormName.value = currentModule.value.name
moduleFormPath.value = currentModule.value.path
moduleFormGitUrl.value = currentModule.value.git_url ?? ''
moduleFormDescription.value = currentModule.value.description ?? ''
moduleFormStack.value = currentModule.value.stack ?? ''
// 老工程 status 为 null 时视作 active
moduleFormStatus.value = (currentModule.value.status === 'archived' ? 'archived' : 'active')
showModuleModal.value = true
}
@@ -439,6 +469,9 @@ async function onSaveModule() {
name: moduleFormName.value.trim(),
path: moduleFormPath.value.trim(),
gitUrl: moduleFormGitUrl.value.trim() || null,
description: moduleFormDescription.value.trim() || null,
stack: moduleFormStack.value.trim() || null,
status: moduleFormStatus.value,
})
} else {
await moduleApi.addProjectModule({
@@ -446,6 +479,9 @@ async function onSaveModule() {
name: moduleFormName.value.trim(),
path: moduleFormPath.value.trim(),
gitUrl: moduleFormGitUrl.value.trim() || null,
description: moduleFormDescription.value.trim() || null,
stack: moduleFormStack.value.trim() || null,
status: moduleFormStatus.value,
})
}
showModuleModal.value = false
@@ -594,7 +630,7 @@ async function onRemoveModule() {
.crumb {
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -682,11 +718,11 @@ async function onRemoveModule() {
min-width: 14px;
height: 14px;
padding: 0 4px;
border-radius: 7px;
border-radius: 6px;
background: var(--df-accent);
color: #fff;
font-size: 9px;
font-weight: 700;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
+9
View File
@@ -34,6 +34,15 @@ export default {
modulePathPlaceholder: 'e.g. D:/projects/my-app/frontend',
moduleGitUrl: 'Git URL',
moduleGitUrlPlaceholder: 'https://github.com/… (optional)',
// V40 module entity: description / stack / status
optSuffix: '(optional)',
moduleDescription: 'Description',
moduleDescriptionPlaceholder: 'e.g. Frontend web app / Backend API service / Infrastructure',
moduleStack: 'Tech Stack',
moduleStackPlaceholder: 'comma-separated, e.g. Vue, TypeScript, Vite',
moduleStatus: 'Status',
moduleStatusActive: 'Active',
moduleStatusArchived: 'Archived',
editModule: 'Edit Module',
removeModule: 'Remove Module',
removeConfirm: 'Are you sure to remove module "{name}"? This action cannot be undone.',
+10
View File
@@ -70,6 +70,16 @@ export default {
techStackLabel: 'Tech Stack',
// Module list (multi-module read-only display)
modulesLabel: 'Modules ({n})',
// V40 module entity: status badge (legacy status=null treated as active)
moduleStatusActive: 'Active',
moduleStatusArchived: 'Archived',
// Overview module management (CRUD + scan)
modulesEmptyHint: 'A project can contain multiple sub-modules (frontend / backend / infra). Add a module, or scan the bound directory for sub-repos to auto-discover.',
scanFound: 'Found {n} new module(s)',
scanNone: 'No new modules found',
moduleAddSuccess: 'Module added',
moduleUpdateSuccess: 'Module updated',
moduleRemoved: 'Module removed',
// Tab navigation
tabOverview: '📋 Overview',
},
+9
View File
@@ -34,6 +34,15 @@ export default {
modulePathPlaceholder: '例如 D:/projects/my-app/frontend',
moduleGitUrl: 'Git 地址',
moduleGitUrlPlaceholder: 'https://github.com/…(可选)',
// V40 工程实体化:描述 / 技术栈 / 状态(可选后缀统一用 optSuffix,替代缺失的 common.optional)
optSuffix: '(可选)',
moduleDescription: '职责描述',
moduleDescriptionPlaceholder: '例如 前端 web 工程 / 后端 API 服务 / 基础设施',
moduleStack: '技术栈',
moduleStackPlaceholder: '逗号分隔,例如 Vue, TypeScript, Vite',
moduleStatus: '状态',
moduleStatusActive: '活跃',
moduleStatusArchived: '归档',
editModule: '编辑工程',
removeModule: '删除工程',
removeConfirm: '确定删除工程「{name}」吗?此操作不可撤销。',
+10
View File
@@ -70,6 +70,16 @@ export default {
techStackLabel: '技术栈',
// 工程列表(多工程只读展示)
modulesLabel: '工程({n})',
// V40 工程实体化:状态 badge(老工程 status=null 视作 active)
moduleStatusActive: '活跃',
moduleStatusArchived: '归档',
// 概览工程管理(增删改 + 扫描)
modulesEmptyHint: '一个项目可包含多个子工程(前端 / 后端 / 基础设施…)。添加工程,或扫描绑定目录下的子仓库自动发现。',
scanFound: '扫描到 {n} 个新工程',
scanNone: '未发现新工程',
moduleAddSuccess: '工程已添加',
moduleUpdateSuccess: '工程已更新',
moduleRemoved: '工程已删除',
// Tab 导航
tabOverview: '📋 概览',
},
+346 -15
View File
@@ -117,13 +117,67 @@
</span>
</div>
<!-- 工程列表(多工程只读展示;CRUD 在文件 Tab) -->
<div class="info-item" v-if="modules.length">
<!-- 工程管理(概览内 CRUD:新增/编辑/删除/扫描;复用 moduleApi + ConfirmDialog + arco Message
切到文件 Tab FileExplorer v-if 重挂载自动重拉,无需手动同步) -->
<div class="info-item info-block">
<span class="label">{{ $t('projectDetail.modulesLabel', { n: modules.length }) }}</span>
<div class="module-list-overview">
<div v-for="m in modules" :key="m.id" class="module-row-overview">
<span class="module-name-overview">{{ m.name }}</span>
<span class="module-path-overview" :title="m.path">{{ m.path }}</span>
<div class="module-manager-card">
<!-- 标题行操作入口 -->
<div class="module-manager-header">
<button class="btn btn-primary btn-sm" type="button" @click="openAddModule">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
{{ $t('fileExplorer.addModule') }}
</button>
<button
v-if="currentProject?.path"
class="btn btn-ghost btn-sm"
type="button"
:disabled="scanning"
@click="onScanSubmodules"
>
<span v-if="scanning" class="spinner"></span>
{{ $t('fileExplorer.scanSubmodules') }}
</button>
</div>
<!-- 空态引导(无工程不隐藏,显卡片 + CTA) -->
<div v-if="modules.length === 0" class="module-empty">
<div class="module-empty-icon">📦</div>
<p class="module-empty-hint">{{ $t('projectDetail.modulesEmptyHint') }}</p>
<div class="module-empty-actions">
<button class="btn btn-primary btn-sm" type="button" @click="openAddModule">{{ $t('fileExplorer.addModule') }}</button>
<button
v-if="currentProject?.path"
class="btn btn-ghost btn-sm"
type="button"
:disabled="scanning"
@click="onScanSubmodules"
>{{ $t('fileExplorer.scanSubmodules') }}</button>
</div>
</div>
<!-- 工程列表(hover 显行内操作) -->
<div v-else class="module-list-overview">
<div v-for="m in modules" :key="m.id" class="module-row-overview">
<div class="module-row-head">
<span class="module-name-overview">{{ m.name }}</span>
<span v-if="moduleStatusOf(m) === 'archived'" class="module-status-badge module-status-archived">{{ $t('projectDetail.moduleStatusArchived') }}</span>
<span v-else class="module-status-badge module-status-active">{{ $t('projectDetail.moduleStatusActive') }}</span>
<span class="module-path-overview" :title="m.path">{{ m.path }}</span>
<span class="module-row-actions">
<button class="ai-btn-icon" :title="$t('fileExplorer.editModule')" @click="openEditModule(m)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="ai-btn-icon module-row-action-danger" :title="$t('fileExplorer.deleteCurrentModule')" @click="confirmRemoveModule(m)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</span>
</div>
<p v-if="m.description" class="module-desc-overview">{{ m.description }}</p>
<div v-if="parseStack(m.stack).length" class="module-stack-overview">
<span class="tech-tag" v-for="tag in parseStack(m.stack)" :key="tag">{{ tag }}</span>
</div>
</div>
</div>
</div>
</div>
@@ -289,7 +343,48 @@
/>
<!-- 确认弹层删除/重定位确认替代原生 window.confirm/alert -->
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" :danger-label="confirmState.dangerLabel" @result="answerConfirm" />
<!-- 新增/编辑工程弹窗(概览内 CRUD,对标 FileExplorer 工程 modal) -->
<div v-if="showModuleModal" class="modal-overlay" @click.self="showModuleModal = false">
<div class="modal-box">
<h3 class="modal-title">{{ editingModule ? $t('fileExplorer.editModule') : $t('fileExplorer.addModule') }}</h3>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleName') }}</label>
<input v-model="moduleFormName" :placeholder="$t('fileExplorer.moduleNamePlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.modulePath') }}</label>
<input v-model="moduleFormPath" :placeholder="$t('fileExplorer.modulePathPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleGitUrl') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<input v-model="moduleFormGitUrl" :placeholder="$t('fileExplorer.moduleGitUrlPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleDescription') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<textarea v-model="moduleFormDescription" :placeholder="$t('fileExplorer.moduleDescriptionPlaceholder')" rows="2"></textarea>
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleStack') }} <span class="opt-label">{{ $t('fileExplorer.optSuffix') }}</span></label>
<input v-model="moduleFormStack" :placeholder="$t('fileExplorer.moduleStackPlaceholder')" />
</div>
<div class="modal-field">
<label>{{ $t('fileExplorer.moduleStatus') }}</label>
<select v-model="moduleFormStatus">
<option value="active">{{ $t('fileExplorer.moduleStatusActive') }}</option>
<option value="archived">{{ $t('fileExplorer.moduleStatusArchived') }}</option>
</select>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" @click="showModuleModal = false">{{ $t('common.cancel') }}</button>
<button class="btn btn-primary" :disabled="savingModule || !moduleFormName.trim() || !moduleFormPath.trim()" @click="onSaveModule">
<span v-if="savingModule" class="spinner"></span>
{{ savingModule ? $t('common.loading') : (editingModule ? $t('common.save') : $t('fileExplorer.addModule')) }}
</button>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -431,9 +526,9 @@ async function loadProjectTasks() {
}
// taskStatusLabel / taskStatusClass 由 ../constants/project 提供
// ── 工程列表(只读展示)──
// 多工程(monorepo/多模块)在 FileExplorer 文件 Tab 内 CRUD,概览仅按 project_id 拉取展示。
// 0 工程 v-if 隐藏,单工程也显(与多工程同列表)
// ── 工程列表(概览内 CRUD)──
// 多工程(monorepo/多模块)在概览内增删改 + 扫描;复用 moduleApi + ConfirmDialog(删除确认)
// + arco Message(反馈)。切到文件 Tab 时 FileExplorer 因 v-if 重挂载自动重拉,无需手动同步
const modules = ref<ProjectModuleRecord[]>([])
async function loadModules() {
try {
@@ -443,6 +538,112 @@ async function loadModules() {
}
}
/**
* 工程状态归一:老工程 status 为 null 视作 active(默认活跃)。
* 仅返回 active / archived 两值,供模板按状态显 badge。
*/
function moduleStatusOf(m: ProjectModuleRecord): 'active' | 'archived' {
return m.status === 'archived' ? 'archived' : 'active'
}
// 工程 CRUD 表单状态(对标 FileExplorer 工程 modal)
const showModuleModal = ref(false)
const editingModule = ref<ProjectModuleRecord | null>(null)
const savingModule = ref(false)
const scanning = ref(false)
const moduleFormName = ref('')
const moduleFormPath = ref('')
const moduleFormGitUrl = ref('')
const moduleFormDescription = ref('')
const moduleFormStack = ref('')
const moduleFormStatus = ref<'active' | 'archived'>('active')
/** 打开新增工程弹窗(清空表单)。 */
function openAddModule() {
editingModule.value = null
moduleFormName.value = ''
moduleFormPath.value = ''
moduleFormGitUrl.value = ''
moduleFormDescription.value = ''
moduleFormStack.value = ''
moduleFormStatus.value = 'active'
showModuleModal.value = true
}
/** 打开编辑工程弹窗(回填当前工程字段;老工程 status=null 视作 active)。 */
function openEditModule(m: ProjectModuleRecord) {
editingModule.value = m
moduleFormName.value = m.name
moduleFormPath.value = m.path
moduleFormGitUrl.value = m.git_url ?? ''
moduleFormDescription.value = m.description ?? ''
moduleFormStack.value = m.stack ?? ''
moduleFormStatus.value = m.status === 'archived' ? 'archived' : 'active'
showModuleModal.value = true
}
/** 保存工程(新增/编辑统一入口;成功 toast + 关弹窗 + 重拉列表)。 */
async function onSaveModule() {
if (savingModule.value) return
if (!moduleFormName.value.trim() || !moduleFormPath.value.trim()) return
savingModule.value = true
try {
const payload = {
name: moduleFormName.value.trim(),
path: moduleFormPath.value.trim(),
gitUrl: moduleFormGitUrl.value.trim() || null,
description: moduleFormDescription.value.trim() || null,
stack: moduleFormStack.value.trim() || null,
status: moduleFormStatus.value,
}
if (editingModule.value) {
await moduleApi.updateProjectModule({ id: editingModule.value.id, ...payload })
Message.success(t('projectDetail.moduleUpdateSuccess'))
} else {
await moduleApi.addProjectModule({ projectId: projectId.value, ...payload })
Message.success(t('projectDetail.moduleAddSuccess'))
}
showModuleModal.value = false
await loadModules()
} catch (e) {
Message.error(t('common.unknownError'))
console.error('[ProjectDetail] 保存工程失败:', e)
} finally {
savingModule.value = false
}
}
/** 删除工程(二次确认后执行;复用 useConfirm 的 Promise 化 confirmDialog)。 */
async function confirmRemoveModule(m: ProjectModuleRecord) {
const ok = await confirmDialog(t('fileExplorer.removeConfirm', { name: m.name }), t('common.delete'))
if (!ok) return
try {
await moduleApi.removeProjectModule(m.id)
Message.info(t('projectDetail.moduleRemoved'))
await loadModules()
} catch (e) {
Message.error(t('common.unknownError'))
console.error('[ProjectDetail] 删除工程失败:', e)
}
}
/** 扫描项目绑定目录下的子仓库,自动创建工程记录(幂等)。 */
async function onScanSubmodules() {
if (scanning.value) return
scanning.value = true
try {
const n = await moduleApi.scanProjectModules(projectId.value)
await loadModules()
if (n > 0) Message.success(t('projectDetail.scanFound', { n }))
else Message.info(t('projectDetail.scanNone'))
} catch (e) {
Message.error(t('common.unknownError'))
console.error('[ProjectDetail] 扫描子仓库失败:', e)
} finally {
scanning.value = false
}
}
// ── 工作流 ──
// demoDag + runDemoWorkflow 已下线(R-PD-2):"script" 节点不再注册到 NodeRegistry
// 前端构造含 script 节点的 DagDef 会在后端 build_dag 失败报错。
@@ -677,7 +878,88 @@ onUnmounted(() => {
font-style: italic;
}
/* ===== 工程列表(概览只读展示;对齐 .info-item label固定宽 + value) ===== */
/* ===== 工程管理卡片(概览内 CRUD) ===== */
.module-manager-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
}
.module-manager-header {
display: flex;
gap: 8px;
align-items: center;
}
/* 工程行:hover 显操作 + 背景反馈(原 .module-row-overview padding/border-bottom 保留) */
.module-row-overview {
position: relative;
border-radius: var(--df-radius-sm);
transition: background 0.15s var(--df-ease);
}
.module-row-overview:hover {
background: var(--df-bg-card-hover);
}
.module-row-actions {
display: inline-flex;
align-items: center;
gap: 2px;
margin-left: auto;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s var(--df-ease);
}
.module-row-overview:hover .module-row-actions {
opacity: 1;
}
/* 行内图标钮缩到 22×22(紧凑协调,覆盖全局 .ai-btn-icon 26×26) */
.module-row-actions .ai-btn-icon {
width: 22px;
height: 22px;
}
.module-row-action-danger:hover {
color: var(--df-danger);
}
/* 无 hover 设备(触屏):操作常显,可达性兜底 */
@media (hover: none) {
.module-row-actions { opacity: 1; }
}
/* 空态引导 */
.module-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px 12px;
text-align: center;
}
.module-empty-icon {
font-size: 28px;
opacity: 0.6;
line-height: 1;
}
.module-empty-hint {
margin: 0;
font-size: 12px;
color: var(--df-text-dim);
line-height: 1.6;
max-width: 320px;
}
.module-empty-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
/* modal 内「(可选)」小字标签(对标 FileExplorer) */
.opt-label {
font-size: 11px;
color: var(--df-text-dim);
font-weight: 400;
}
/* ===== 工程列表 ===== */
.module-list-overview {
display: flex;
flex-direction: column;
@@ -686,10 +968,22 @@ onUnmounted(() => {
min-width: 0;
}
.module-row-overview {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 0;
border-bottom: 1px solid var(--df-border, rgba(0, 0, 0, 0.06));
min-width: 0;
}
.module-row-overview:last-child {
border-bottom: none;
}
.module-row-head {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
flex-wrap: wrap;
}
.module-name-overview {
font-size: 13px;
@@ -705,6 +999,43 @@ onUnmounted(() => {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex: 1;
}
.module-desc-overview {
margin: 0;
font-size: 12px;
color: var(--df-text-secondary);
line-height: 1.5;
/* 限制两行,过长省略;描述通常一句话,两行覆盖 99% 场景 */
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.module-stack-overview {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.module-stack-overview .tech-tag {
font-size: 11px;
padding: 1px 6px;
}
.module-status-badge {
font-size: 11px;
padding: 1px 6px;
border-radius: 8px;
font-weight: 500;
flex-shrink: 0;
}
.module-status-active {
color: var(--df-success, #16a34a);
background: rgba(22, 163, 74, 0.1);
}
.module-status-archived {
color: var(--df-text-dim, #888);
background: rgba(136, 136, 136, 0.12);
}
.status-badge {
@@ -713,7 +1044,7 @@ onUnmounted(() => {
border-radius: var(--df-radius-lg);
font-size: 11px;
font-weight: 500;
background: rgba(108,99,255,0.1);
background: var(--df-accent-bg);
color: var(--df-accent);
}
@@ -843,7 +1174,7 @@ onUnmounted(() => {
font-weight: 500;
}
.stage-planning { background: rgba(100,181,246,0.15); color: #64b5f6; }
.stage-in_progress { background: rgba(108,99,255,0.15); color: #6c63ff; }
.stage-in_progress { background: var(--df-accent-soft); color: var(--df-accent); }
.stage-testing { background: rgba(255,193,7,0.15); color: #ffc107; }
.stage-releasing { background: rgba(156,39,176,0.15); color: #ce93d8; }
.stage-completed { background: rgba(100,200,100,0.15); color: #64c864; }
@@ -890,7 +1221,7 @@ onUnmounted(() => {
.task-title { font-size: 14px; font-weight: 500; color: var(--df-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.task-status { font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs); }
.status-done { background: rgba(100,255,218,0.15); color: var(--df-success); }
.status-progress { background: rgba(108,99,255,0.15); color: var(--df-accent); }
.status-progress { background: var(--df-accent-soft); color: var(--df-accent); }
.status-review { background: rgba(255,217,61,0.15); color: var(--df-warning); }
.status-todo { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
@@ -905,7 +1236,7 @@ onUnmounted(() => {
font-size: 12px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--df-text-secondary);
background: rgba(108,99,255,0.1);
background: var(--df-accent-bg);
padding: 1px 6px;
border-radius: var(--df-radius-sm);
}