修复: 前端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;
|
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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export default {
|
|||||||
projectDetail: {
|
projectDetail: {
|
||||||
// 头部
|
// 头部
|
||||||
backToList: '← 项目列表',
|
backToList: '← 项目列表',
|
||||||
|
notFound: '项目不存在或已删除',
|
||||||
sync: '🔄 同步',
|
sync: '🔄 同步',
|
||||||
delete: '🗑 删除',
|
delete: '🗑 删除',
|
||||||
newTask: '+ 新任务',
|
newTask: '+ 新任务',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user