修复: 前端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:
2026-07-02 12:12:29 +08:00
parent acf0ed091d
commit c3b47b5771
8 changed files with 63 additions and 51 deletions

View File

@@ -72,9 +72,8 @@ const examplePrompts = [
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
opacity: 0.5;
} }
.ai-empty-icon { margin-bottom: 4px; } .ai-empty-icon { margin-bottom: 4px; opacity: 0.4; }
.ai-empty-text { .ai-empty-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

View File

@@ -2,6 +2,7 @@ export default {
projectDetail: { projectDetail: {
// Header // Header
backToList: '← Projects', backToList: '← Projects',
notFound: 'Project not found or deleted',
sync: '🔄 Sync', sync: '🔄 Sync',
delete: '🗑 Delete', delete: '🗑 Delete',
newTask: '+ New Task', newTask: '+ New Task',

View File

@@ -2,6 +2,7 @@ export default {
projectDetail: { projectDetail: {
// 头部 // 头部
backToList: '← 项目列表', backToList: '← 项目列表',
notFound: '项目不存在或已删除',
sync: '🔄 同步', sync: '🔄 同步',
delete: '🗑 删除', delete: '🗑 删除',
newTask: '+ 新任务', newTask: '+ 新任务',

View File

@@ -108,21 +108,31 @@
<div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false"> <div class="modal-overlay" v-if="showCaptureModal" @click.self="showCaptureModal = false">
<div class="modal-box"> <div class="modal-box">
<h3>{{ $t('ideas.captureTitle') }}</h3> <h3>{{ $t('ideas.captureTitle') }}</h3>
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldTitle') }}</label> <div class="modal-field">
<label>{{ $t('ideas.fieldTitle') }}</label>
<input v-model="newIdeaTitle" :placeholder="$t('ideas.titlePlaceholder')" @keyup.enter="confirmCapture" /> <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> </div>
<div class="modal-field">
<label>{{ $t('ideas.fieldDesc') }}</label>
<textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea> <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> </div>
<div class="modal-field">
<label>{{ $t('ideas.fieldTags') }}</label>
<input v-model="newIdeaTags" :placeholder="$t('ideas.tagsPlaceholder')" /> <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> </div>
<div class="modal-field">
<label>{{ $t('ideas.fieldPriority') }}</label>
<select v-model.number="newIdeaPriority" class="modal-select"> <select v-model.number="newIdeaPriority" class="modal-select">
<option :value="0">{{ $t('ideas.priorityCritical') }} (P0)</option> <option :value="0">{{ $t('ideas.priorityCritical') }} (P0)</option>
<option :value="1">{{ $t('ideas.priorityHigh') }} (P1)</option> <option :value="1">{{ $t('ideas.priorityHigh') }} (P1)</option>
<option :value="2">{{ $t('ideas.priorityMedium') }} (P2)</option> <option :value="2">{{ $t('ideas.priorityMedium') }} (P2)</option>
<option :value="3">{{ $t('ideas.priorityLow') }} (P3)</option> <option :value="3">{{ $t('ideas.priorityLow') }} (P3)</option>
</select> </select>
<label style="font-size:12px;color:var(--df-text-secondary);margin-bottom:4px;display:block">{{ $t('ideas.fieldSource') }}</label> </div>
<div class="modal-field">
<label>{{ $t('ideas.fieldSource') }}</label>
<input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" /> <input v-model="newIdeaSource" :placeholder="$t('ideas.sourcePlaceholder')" />
</div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button> <button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
<button class="btn-confirm" :disabled="creating" @click="confirmCapture"> <button class="btn-confirm" :disabled="creating" @click="confirmCapture">

View File

@@ -57,6 +57,7 @@
<!-- 卡片列表 --> <!-- 卡片列表 -->
<div class="list-cards"> <div class="list-cards">
<div v-if="store.loading" class="list-empty">{{ $t('common.loading') }}</div>
<div <div
v-for="item in listItems" v-for="item in listItems"
:key="item.id" :key="item.id"

View File

@@ -1,11 +1,20 @@
<template> <template>
<div class="project-detail"> <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"> <header class="page-header">
<div class="header-left"> <div class="header-left">
<router-link to="/projects" class="back-link">{{ $t('projectDetail.backToList') }}</router-link> <router-link to="/projects" class="back-link">{{ $t('projectDetail.backToList') }}</router-link>
<h1>{{ currentProject?.name ?? '...' }}</h1> <h1>{{ currentProject.name }}</h1>
<span class="stage-badge" :class="'stage-' + stageKey">{{ $t(statusLabel) }}</span> <span v-if="statusLabel" class="stage-badge" :class="'stage-' + stageKey">{{ $t(statusLabel) }}</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-ghost" @click="handleSync">{{ $t('projectDetail.sync') }}</button> <button class="btn btn-ghost" @click="handleSync">{{ $t('projectDetail.sync') }}</button>
@@ -253,6 +262,7 @@
<!-- 确认弹层删除/重定位确认替代原生 window.confirm/alert --> <!-- 确认弹层删除/重定位确认替代原生 window.confirm/alert -->
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" /> <ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
</template>
</div> </div>
</template> </template>
@@ -290,14 +300,7 @@ const activeTab = ref<'overview' | 'files' | 'graph'>('overview')
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings) // 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
const { confirmState, confirmDialog, answerConfirm } = useConfirm() 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 一致, // 状态文案/阶段进度统一走 ../constants/project(与 Projects/Tasks/Dashboard 一致,
@@ -306,6 +309,7 @@ const projectId = computed(() => route.params.id as string)
const currentProject = computed(() => const currentProject = computed(() =>
store.projects.find(p => p.id === projectId.value) store.projects.find(p => p.id === projectId.value)
) )
const loading = computed(() => store.projects.length === 0 && !store.error)
// 状态文案/阶段进度统一走 ../constants/project // 状态文案/阶段进度统一走 ../constants/project
// B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例), // B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
@@ -553,6 +557,10 @@ onUnmounted(() => {
<style scoped> <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 { .project-info {
display: flex; display: flex;
@@ -736,11 +744,13 @@ onUnmounted(() => {
border-radius: var(--df-radius-lg); border-radius: var(--df-radius-lg);
font-weight: 500; font-weight: 500;
} }
.stage-idea { background: rgba(100,181,246,0.15); color: var(--df-info); } .stage-planning { background: rgba(100,181,246,0.15); color: #64b5f6; }
.stage-requirement { background: rgba(255,217,61,0.15); color: var(--df-warning); } .stage-in_progress { background: rgba(108,99,255,0.15); color: #6c63ff; }
.stage-coding { background: rgba(108,99,255,0.15); color: var(--df-accent); } .stage-testing { background: rgba(255,193,7,0.15); color: #ffc107; }
.stage-testing { background: rgba(100,255,218,0.15); color: var(--df-success); } .stage-releasing { background: rgba(156,39,176,0.15); color: #ce93d8; }
.stage-release { background: rgba(255,107,107,0.15); color: #ff9800; } .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; } .header-actions { display: flex; gap: 10px; }

View File

@@ -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 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 tbody tr:last-child td { border-bottom: none; }
.import-table .col-check { width: 32px; text-align: center; } .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; } .row-disabled { opacity: 0.5; }
.imp-name { font-weight: 500; color: var(--df-text); } .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); } .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); }

View File

@@ -155,11 +155,14 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 确认弹层(删除任务,替代原生 window.confirm) -->
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
</div> </div>
</template> </template>
<script setup lang="ts"> <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 { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useProjectStore } from '@/stores/project' import { useProjectStore } from '@/stores/project'
@@ -168,10 +171,13 @@ import { taskStatusLabel as statusLabel, taskStatusClass, priorityLabel, priorit
import { taskApi } from '@/api' import { taskApi } from '@/api'
import type { TaskRecord, TaskQuery, ProjectId } from '@/api/types' import type { TaskRecord, TaskQuery, ProjectId } from '@/api/types'
import Paginator from '../components/Paginator.vue' import Paginator from '../components/Paginator.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { useConfirm } from '@/composables/useConfirm'
const router = useRouter() const router = useRouter()
const store = useProjectStore() const store = useProjectStore()
const { t } = useI18n() const { t } = useI18n()
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
const activeProject = ref('all') const activeProject = ref('all')
const activeStatus = ref('all') const activeStatus = ref('all')
@@ -222,7 +228,7 @@ async function quickPriority(id: string, priority: number) {
} }
async function quickDelete(task: TaskRecord) { async function quickDelete(task: TaskRecord) {
quickMenuId.value = null quickMenuId.value = null
if (!confirm(t('tasks.confirmDelete', { title: task.title }))) return if (!await confirmDialog(t('tasks.confirmDelete', { title: task.title }))) return
try { try {
await store.deleteTask(task.id) await store.deleteTask(task.id)
} catch (e) { console.error('删除失败:', e) } } catch (e) { console.error('删除失败:', e) }
@@ -343,17 +349,6 @@ function buildTaskQuery(): TaskQuery | undefined {
return query 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() { function openCreateModal() {
newTaskProjectId.value = store.projects.length > 0 ? store.projects[0].id : '' newTaskProjectId.value = store.projects.length > 0 ? store.projects[0].id : ''
newTaskTitle.value = '' newTaskTitle.value = ''
@@ -439,7 +434,6 @@ onMounted(async () => {
document.addEventListener('click', closeQuickMenu) document.addEventListener('click', closeQuickMenu)
}) })
import { onUnmounted } from 'vue'
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', onKeydown) window.removeEventListener('keydown', onKeydown)
document.removeEventListener('click', closeQuickMenu) document.removeEventListener('click', closeQuickMenu)
@@ -697,10 +691,6 @@ onUnmounted(() => {
margin: 4px 0; margin: 4px 0;
} }
.quick-menu-danger { color: var(--df-danger); } .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 { .empty-state {