修复: 前端UI审查问题(项目状态badge+confirm替换+加载态+CSS修正)
- ProjectDetail: 加载态/不存在态 + stage badge CSS 类名修正(7个实际状态) - Tasks: confirm()替换为useConfirm + 死代码清理 + priority CSS去重 - Knowledge: 列表加载态 - Projects: CSS变量名拼写修正(--df-mono→--df-font-mono) - EmptyState: opacity从容器移到图标(对比度修复) - Ideas: 5处内联style替换为modal-field class - ProjectDetail: 删除注释stages死代码
This commit is contained in:
@@ -72,9 +72,8 @@ const examplePrompts = [
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ai-empty-icon { margin-bottom: 4px; }
|
||||
.ai-empty-icon { margin-bottom: 4px; opacity: 0.4; }
|
||||
.ai-empty-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
projectDetail: {
|
||||
// Header
|
||||
backToList: '← Projects',
|
||||
notFound: 'Project not found or deleted',
|
||||
sync: '🔄 Sync',
|
||||
delete: '🗑 Delete',
|
||||
newTask: '+ New Task',
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
projectDetail: {
|
||||
// 头部
|
||||
backToList: '← 项目列表',
|
||||
notFound: '项目不存在或已删除',
|
||||
sync: '🔄 同步',
|
||||
delete: '🗑 删除',
|
||||
newTask: '+ 新任务',
|
||||
|
||||
@@ -108,21 +108,31 @@
|
||||
<div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false">
|
||||
<div class="modal-box">
|
||||
<h3>{{ $t('ideas.captureTitle') }}</h3>
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldTitle') }}</label>
|
||||
<input v-model="newIdeaTitle" :placeholder="$t('ideas.titlePlaceholder')" @keyup.enter="confirmCapture" />
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldDesc') }}</label>
|
||||
<textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea>
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldTags') }}</label>
|
||||
<input v-model="newIdeaTags" :placeholder="$t('ideas.tagsPlaceholder')" />
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldPriority') }}</label>
|
||||
<select v-model.number="newIdeaPriority" class="modal-select">
|
||||
<option :value="0">{{ $t('ideas.priorityCritical') }} (P0)</option>
|
||||
<option :value="1">{{ $t('ideas.priorityHigh') }} (P1)</option>
|
||||
<option :value="2">{{ $t('ideas.priorityMedium') }} (P2)</option>
|
||||
<option :value="3">{{ $t('ideas.priorityLow') }} (P3)</option>
|
||||
</select>
|
||||
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldSource') }}</label>
|
||||
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('ideas.fieldTitle') }}</label>
|
||||
<input v-model="newIdeaTitle" :placeholder="$t('ideas.titlePlaceholder')" @keyup.enter="confirmCapture" />
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('ideas.fieldDesc') }}</label>
|
||||
<textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('ideas.fieldTags') }}</label>
|
||||
<input v-model="newIdeaTags" :placeholder="$t('ideas.tagsPlaceholder')" />
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('ideas.fieldPriority') }}</label>
|
||||
<select v-model.number="newIdeaPriority" class="modal-select">
|
||||
<option :value="0">{{ $t('ideas.priorityCritical') }} (P0)</option>
|
||||
<option :value="1">{{ $t('ideas.priorityHigh') }} (P1)</option>
|
||||
<option :value="2">{{ $t('ideas.priorityMedium') }} (P2)</option>
|
||||
<option :value="3">{{ $t('ideas.priorityLow') }} (P3)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('ideas.fieldSource') }}</label>
|
||||
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn-confirm" :disabled="creating" @click="confirmCapture">
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div class="list-cards">
|
||||
<div v-if="store.loading" class="list-empty">{{ $t('common.loading') }}</div>
|
||||
<div
|
||||
v-for="item in listItems"
|
||||
:key="item.id"
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<template>
|
||||
<div class="project-detail">
|
||||
<!-- 加载态 -->
|
||||
<div v-if="loading" class="loading-state">{{ $t('common.loading') }}</div>
|
||||
<!-- 项目不存在 -->
|
||||
<div v-else-if="!currentProject" class="empty-state">
|
||||
<p>{{ $t('projectDetail.notFound') }}</p>
|
||||
<router-link to="/projects" class="btn btn-primary">{{ $t('projectDetail.backToList') }}</router-link>
|
||||
</div>
|
||||
<!-- 项目详情 -->
|
||||
<template v-else>
|
||||
<!-- 页面头部 -->
|
||||
<header class="page-header">
|
||||
<div class="header-left">
|
||||
<router-link to="/projects" class="back-link">{{ $t('projectDetail.backToList') }}</router-link>
|
||||
<h1>{{ currentProject?.name ?? '...' }}</h1>
|
||||
<span class="stage-badge" :class="'stage-' + stageKey">{{ $t(statusLabel) }}</span>
|
||||
<h1>{{ currentProject.name }}</h1>
|
||||
<span v-if="statusLabel" class="stage-badge" :class="'stage-' + stageKey">{{ $t(statusLabel) }}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost" @click="handleSync">{{ $t('projectDetail.sync') }}</button>
|
||||
@@ -253,6 +262,7 @@
|
||||
|
||||
<!-- 确认弹层(删除/重定位确认,替代原生 window.confirm/alert) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -290,14 +300,7 @@ const activeTab = ref<'overview' | 'files' | 'graph'>('overview')
|
||||
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
// ── 阶段定义(暂时隐藏,阶段进度条模板已注释,后续接入真实状态机后启用) ──
|
||||
// const stages = [
|
||||
// { key: 'idea', labelKey: 'stageIdea' },
|
||||
// { key: 'requirement', labelKey: 'stageRequirement' },
|
||||
// { key: 'coding', labelKey: 'stageCoding' },
|
||||
// { key: 'testing', labelKey: 'stageTesting' },
|
||||
// { key: 'release', labelKey: 'stageRelease' },
|
||||
// ]
|
||||
|
||||
|
||||
// ── 当前项目 ──
|
||||
// 状态文案/阶段进度统一走 ../constants/project(与 Projects/Tasks/Dashboard 一致,
|
||||
@@ -306,6 +309,7 @@ const projectId = computed(() => route.params.id as string)
|
||||
const currentProject = computed(() =>
|
||||
store.projects.find(p => p.id === projectId.value)
|
||||
)
|
||||
const loading = computed(() => store.projects.length === 0 && !store.error)
|
||||
|
||||
// 状态文案/阶段进度统一走 ../constants/project
|
||||
// B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
|
||||
@@ -553,6 +557,10 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
/* ... 现有样式 ... */
|
||||
|
||||
/* 加载/空态(模板顶部使用) */
|
||||
.loading-state { text-align: center; padding: 40px; color: var(--df-text-dim); }
|
||||
.empty-state { text-align: center; padding: 40px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
|
||||
/* ===== 项目信息样式 ===== */
|
||||
.project-info {
|
||||
display: flex;
|
||||
@@ -736,11 +744,13 @@ onUnmounted(() => {
|
||||
border-radius: var(--df-radius-lg);
|
||||
font-weight: 500;
|
||||
}
|
||||
.stage-idea { background: rgba(100,181,246,0.15); color: var(--df-info); }
|
||||
.stage-requirement { background: rgba(255,217,61,0.15); color: var(--df-warning); }
|
||||
.stage-coding { background: rgba(108,99,255,0.15); color: var(--df-accent); }
|
||||
.stage-testing { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
||||
.stage-release { background: rgba(255,107,107,0.15); color: #ff9800; }
|
||||
.stage-planning { background: rgba(100,181,246,0.15); color: #64b5f6; }
|
||||
.stage-in_progress { background: rgba(108,99,255,0.15); color: #6c63ff; }
|
||||
.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; }
|
||||
.stage-paused { background: rgba(158,158,158,0.15); color: #9e9e9e; }
|
||||
.stage-cancelled { background: rgba(244,67,54,0.15); color: #f44336; }
|
||||
|
||||
.header-actions { display: flex; gap: 10px; }
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ async function runImport() {
|
||||
.import-table tbody td { padding: 8px 10px; border-bottom: 0.5px solid var(--df-border); vertical-align: top; color: var(--df-text-secondary); }
|
||||
.import-table tbody tr:last-child td { border-bottom: none; }
|
||||
.import-table .col-check { width: 32px; text-align: center; }
|
||||
.import-table .col-path { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--df-mono, monospace); font-size: 11px; }
|
||||
.import-table .col-path { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--df-font-mono, monospace); font-size: 11px; }
|
||||
.row-disabled { opacity: 0.5; }
|
||||
.imp-name { font-weight: 500; color: var(--df-text); }
|
||||
.mono-tag { margin-left: 6px; font-size: 10px; padding: 1px 6px; border-radius: var(--df-radius-xs); background: rgba(255,217,61,0.15); color: var(--df-warning); border: 0.5px solid var(--df-border); }
|
||||
|
||||
@@ -155,11 +155,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹层(删除任务,替代原生 window.confirm) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, reactive } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
@@ -168,10 +171,13 @@ import { taskStatusLabel as statusLabel, taskStatusClass, priorityLabel, priorit
|
||||
import { taskApi } from '@/api'
|
||||
import type { TaskRecord, TaskQuery, ProjectId } from '@/api/types'
|
||||
import Paginator from '../components/Paginator.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useProjectStore()
|
||||
const { t } = useI18n()
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
const activeProject = ref('all')
|
||||
const activeStatus = ref('all')
|
||||
@@ -222,7 +228,7 @@ async function quickPriority(id: string, priority: number) {
|
||||
}
|
||||
async function quickDelete(task: TaskRecord) {
|
||||
quickMenuId.value = null
|
||||
if (!confirm(t('tasks.confirmDelete', { title: task.title }))) return
|
||||
if (!await confirmDialog(t('tasks.confirmDelete', { title: task.title }))) return
|
||||
try {
|
||||
await store.deleteTask(task.id)
|
||||
} catch (e) { console.error('删除失败:', e) }
|
||||
@@ -343,17 +349,6 @@ function buildTaskQuery(): TaskQuery | undefined {
|
||||
return query
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
await store.loadTasks(buildTaskQuery())
|
||||
totalTasks.value = store.tasks.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
void refresh // 保留:将来可能由下拉刷新/手动刷新按钮触发
|
||||
|
||||
function openCreateModal() {
|
||||
newTaskProjectId.value = store.projects.length > 0 ? store.projects[0].id : ''
|
||||
newTaskTitle.value = ''
|
||||
@@ -439,7 +434,6 @@ onMounted(async () => {
|
||||
document.addEventListener('click', closeQuickMenu)
|
||||
})
|
||||
|
||||
import { onUnmounted } from 'vue'
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
document.removeEventListener('click', closeQuickMenu)
|
||||
@@ -697,10 +691,6 @@ onUnmounted(() => {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.quick-menu-danger { color: var(--df-danger); }
|
||||
.priority-critical { color: var(--df-danger); }
|
||||
.priority-high { color: #ff9800; }
|
||||
.priority-medium { color: var(--df-info); }
|
||||
.priority-low { color: var(--df-text-dim); }
|
||||
|
||||
/* 空态 */
|
||||
.empty-state {
|
||||
|
||||
Reference in New Issue
Block a user