重构: tool_registry拆分及多批改进
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
<input v-model="connForm.user" class="setting-input" :placeholder="$t('settings.phUser')" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" @click="saveConn">{{ $t('common.save') }}</button>
|
||||
<button class="btn btn-primary" :disabled="submitting" @click="saveConn">{{ $t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -106,6 +106,9 @@ const connForm = reactive({
|
||||
user: '',
|
||||
})
|
||||
|
||||
// 异步操作禁用态(防双击重复提交):saveConn 保存连接期间禁用按钮
|
||||
const submitting = ref(false)
|
||||
|
||||
const CONN_STORAGE_KEY = 'df-connections'
|
||||
|
||||
function loadConnections() {
|
||||
@@ -151,22 +154,27 @@ function saveConn() {
|
||||
emit('toast', t('settings.toastConnIncomplete'), 'warning')
|
||||
return
|
||||
}
|
||||
const record: ConnRecord = {
|
||||
id: connForm.editId || Date.now().toString(36),
|
||||
name: connForm.name,
|
||||
type: connForm.type,
|
||||
host: connForm.host,
|
||||
port: connForm.port,
|
||||
user: connForm.user,
|
||||
submitting.value = true
|
||||
try {
|
||||
const record: ConnRecord = {
|
||||
id: connForm.editId || Date.now().toString(36),
|
||||
name: connForm.name,
|
||||
type: connForm.type,
|
||||
host: connForm.host,
|
||||
port: connForm.port,
|
||||
user: connForm.user,
|
||||
}
|
||||
if (connForm.editId) {
|
||||
const idx = connections.value.findIndex(c => c.id === connForm.editId)
|
||||
if (idx >= 0) connections.value[idx] = record
|
||||
} else {
|
||||
connections.value.push(record)
|
||||
}
|
||||
persistConnections()
|
||||
connForm.visible = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
if (connForm.editId) {
|
||||
const idx = connections.value.findIndex(c => c.id === connForm.editId)
|
||||
if (idx >= 0) connections.value[idx] = record
|
||||
} else {
|
||||
connections.value.push(record)
|
||||
}
|
||||
persistConnections()
|
||||
connForm.visible = false
|
||||
}
|
||||
|
||||
async function removeConn(id: string) {
|
||||
|
||||
@@ -46,7 +46,7 @@ function applyUiState(s: any) {
|
||||
}
|
||||
|
||||
// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局(模块级单例 state)
|
||||
export function restoreUiState() {
|
||||
function restoreUiState() {
|
||||
applyUiState(appSettings.get<any>('df-ai-ui', null))
|
||||
}
|
||||
restoreUiState()
|
||||
|
||||
@@ -22,13 +22,6 @@ export interface ConfirmState {
|
||||
resolve: null | ((v: boolean) => void)
|
||||
}
|
||||
|
||||
/** 弹出确认对话框,返回 Promise;resolve(true) 表示用户确认,resolve(false) 表示取消。
|
||||
* dangerLabel 可选:覆盖危险按钮文案(如「清空」),不传则回退组件兜底 common.confirm,保持旧行为。 */
|
||||
export type ConfirmFn = (msg: string, dangerLabel?: string) => Promise<boolean>
|
||||
|
||||
/** 关闭弹层并回传结果 */
|
||||
export type AnswerFn = (ok: boolean) => void
|
||||
|
||||
export function useConfirm() {
|
||||
const confirmState = ref<ConfirmState>({
|
||||
visible: false,
|
||||
|
||||
@@ -132,8 +132,6 @@ export function useMarkdown() {
|
||||
escapeFallback,
|
||||
getMarked: () => _marked,
|
||||
getPurify: () => _purify,
|
||||
// 仅 AiChat 流式块级 memo 用(独立缓存,不与历史 _mdCache 混)
|
||||
clearHistoryCache: () => _mdCache.clear(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ export default {
|
||||
noIdeas: 'No ideas yet, click above to capture',
|
||||
noData: 'No data',
|
||||
},
|
||||
// Error fallback (refresh/onMounted load failures surfaced to user)
|
||||
err: {
|
||||
loadFailed: 'Failed to load data, please retry later',
|
||||
},
|
||||
stage: {
|
||||
planning: 'Planning',
|
||||
coding: 'Coding',
|
||||
|
||||
@@ -16,6 +16,11 @@ export default {
|
||||
|
||||
// Empty state
|
||||
emptyState: 'Select an idea to view details',
|
||||
// List empty / async button placeholders
|
||||
listEmpty: '📭 No ideas yet. Click "Capture Idea" at the top right to start.',
|
||||
creating: 'Creating…',
|
||||
deleting: 'Deleting…',
|
||||
promoting: 'Promoting…',
|
||||
|
||||
// Adversarial evaluation
|
||||
adversarialTitle: '⚖️ Adversarial Evaluation',
|
||||
|
||||
@@ -32,6 +32,10 @@ export default {
|
||||
noIdeas: '暂无灵感,点击上方按钮捕捉',
|
||||
noData: '暂无数据',
|
||||
},
|
||||
// 错误回退(refresh/onMounted 加载失败对用户可见)
|
||||
err: {
|
||||
loadFailed: '加载数据失败,请稍后重试',
|
||||
},
|
||||
stage: {
|
||||
planning: '规划中',
|
||||
coding: '编码中',
|
||||
|
||||
@@ -16,6 +16,11 @@ export default {
|
||||
|
||||
// 空状态
|
||||
emptyState: '选择一个灵感查看详情',
|
||||
// 列表空态/异步按钮占位
|
||||
listEmpty: '📭 还没有灵感,点击右上角「捕捉灵感」开始记录',
|
||||
creating: '创建中…',
|
||||
deleting: '删除中…',
|
||||
promoting: '立项中…',
|
||||
|
||||
// 对抗式评估
|
||||
adversarialTitle: '⚖️ 对抗式评估',
|
||||
|
||||
@@ -16,7 +16,7 @@ export function stripMd(text: string | null | undefined): string {
|
||||
return text
|
||||
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') // 图片 alt
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // 链接文本
|
||||
.replace(/```[a-zA-Z]*\n?/g, '') // 代码块围栏开
|
||||
.replace(/```[^\n]*\n?/g, '') // 代码块围栏开(含任意 infostring 至换行,如 rust,norun)
|
||||
.replace(/```/g, '') // 代码块围栏闭
|
||||
.replace(/`([^`]+)`/g, '$1') // 行内代码
|
||||
.replace(/^\s{0,3}>\s?/gm, '') // 引用
|
||||
|
||||
@@ -41,7 +41,8 @@ export function formatDate(v: string | number | null | undefined): string {
|
||||
const ms = parseTs(v)
|
||||
if (ms == null) return '—'
|
||||
const d = new Date(ms)
|
||||
return d.toLocaleDateString('zh-CN') + ' ' + d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
const locale = (i18n as any).global.locale.value === 'en' ? 'en-US' : 'zh-CN'
|
||||
return d.toLocaleDateString(locale) + ' ' + d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
/** 中文相对时间 '刚刚 / X 分钟前 / X 小时前 / X 天前';无效返回 '—' */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<span class="dash-subtitle">{{ $t('dashboard.subtitle') }}</span>
|
||||
</div>
|
||||
<div class="dash-header-right">
|
||||
<button class="df-btn df-btn--ghost" @click="refresh">
|
||||
<button class="df-btn df-btn--ghost" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
<button class="df-btn df-btn--primary" @click="quickCapture">
|
||||
@@ -17,6 +17,12 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(对齐 Knowledge error-banner) ═══ -->
|
||||
<div v-if="errorMsg" class="error-banner">
|
||||
<span class="error-text">{{ errorMsg }}</span>
|
||||
<button class="error-dismiss" @click="errorMsg = ''">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Stat Cards ═══ -->
|
||||
<div class="stat-row">
|
||||
<div
|
||||
@@ -104,7 +110,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
@@ -114,6 +120,10 @@ const router = useRouter()
|
||||
const store = useProjectStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 加载态(refresh/load 期间 true,驱动刷新按钮禁用) + 错误态(加载失败对用户可见)
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
const stats = computed(() => [
|
||||
{ key: 'ideas', icon: '\u{1F4A1}', label: t('dashboard.stats.ideas'), value: store.stats.ideas, trend: 0,
|
||||
iconBg: 'rgba(123,111,240,0.12)' },
|
||||
@@ -179,24 +189,32 @@ function ideaStatusLabel(status: string): string {
|
||||
return t('dashboard.ideaStatus.' + status)
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
// 统一加载入口(projects/tasks/ideas 并行),管理 loading + errorMsg
|
||||
// refresh 与 onMounted 共用,避免两处 catch 逻辑重复
|
||||
async function loadAll() {
|
||||
if (loading.value) return // 防重复触发
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
try {
|
||||
await Promise.all([store.loadProjects(), store.loadTasks(), store.loadIdeas()])
|
||||
} catch (e) {
|
||||
console.error('Dashboard refresh failed:', e)
|
||||
} catch (e: any) {
|
||||
console.error('Dashboard load failed:', e)
|
||||
errorMsg.value = e?.toString() ?? t('dashboard.err.loadFailed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
function quickCapture() {
|
||||
router.push('/ideas')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await Promise.all([store.loadProjects(), store.loadTasks(), store.loadIdeas()])
|
||||
} catch (e) {
|
||||
console.error('Dashboard load failed:', e)
|
||||
}
|
||||
onMounted(() => {
|
||||
loadAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -271,6 +289,21 @@ onMounted(async () => {
|
||||
border-color: var(--df-border-strong);
|
||||
}
|
||||
.df-btn--xs { padding: 4px 10px; font-size: 11px; }
|
||||
.df-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* ═══ 错误条(对齐 Knowledge error-banner) ═══ */
|
||||
.error-banner {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 8px 12px; margin-bottom: 14px;
|
||||
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
|
||||
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
|
||||
}
|
||||
.error-text { word-break: break-word; }
|
||||
.error-dismiss {
|
||||
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
|
||||
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
|
||||
}
|
||||
.error-dismiss:hover { opacity: 0.7; }
|
||||
|
||||
/* ═══ Stat Cards ═══ */
|
||||
.stat-row {
|
||||
|
||||
@@ -33,23 +33,29 @@
|
||||
<!-- 左侧:灵感列表 -->
|
||||
<section class="idea-list-panel">
|
||||
<div class="idea-list">
|
||||
<div
|
||||
v-for="idea in filteredIdeas"
|
||||
:key="idea.id"
|
||||
class="idea-card"
|
||||
:class="{ selected: selectedId === idea.id }"
|
||||
@click="selectedId = idea.id"
|
||||
>
|
||||
<div class="idea-card-header">
|
||||
<span class="idea-title">{{ idea.title }}</span>
|
||||
<span class="idea-score" :class="scoreClass(idea.score)">{{ idea.score ?? '-' }}</span>
|
||||
<!-- 三态:加载/错误/空(对齐 Tasks.vue:43-45) -->
|
||||
<div v-if="store.loading" class="empty-state">{{ $t('common.loading') }}</div>
|
||||
<div v-else-if="store.error" class="empty-state">{{ store.error }}</div>
|
||||
<div v-else-if="filteredIdeas.length === 0" class="empty-state">{{ $t('ideas.listEmpty') }}</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="idea in filteredIdeas"
|
||||
:key="idea.id"
|
||||
class="idea-card"
|
||||
:class="{ selected: selectedId === idea.id }"
|
||||
@click="selectedId = idea.id"
|
||||
>
|
||||
<div class="idea-card-header">
|
||||
<span class="idea-title">{{ idea.title }}</span>
|
||||
<span class="idea-score" :class="scoreClass(idea.score)">{{ idea.score ?? '-' }}</span>
|
||||
</div>
|
||||
<p class="idea-desc-preview">{{ stripMd(idea.description).slice(0, 60) }}{{ stripMd(idea.description).length > 60 ? '...' : '' }}</p>
|
||||
<div class="idea-card-footer">
|
||||
<span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
|
||||
<span class="idea-date">{{ formatDate(idea.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="idea-desc-preview">{{ stripMd(idea.description).slice(0, 60) }}{{ stripMd(idea.description).length > 60 ? '...' : '' }}</p>
|
||||
<div class="idea-card-footer">
|
||||
<span class="status-tag" :class="'status-' + idea.status">{{ $t(statusLabelKey(idea.status)) }}</span>
|
||||
<span class="idea-date">{{ formatDate(idea.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -167,11 +173,14 @@
|
||||
<button
|
||||
v-if="currentIdea.status === 'approved' && !currentIdea.promoted_to"
|
||||
class="btn btn-primary"
|
||||
:disabled="promoting"
|
||||
@click="promoteToProject"
|
||||
>
|
||||
{{ $t('ideas.promoteToProject') }}
|
||||
{{ promoting ? $t('ideas.promoting') : $t('ideas.promoteToProject') }}
|
||||
</button>
|
||||
<button class="btn btn-ghost" :disabled="deleting" @click="deleteCurrentIdea">
|
||||
{{ deleting ? $t('ideas.deleting') : $t('ideas.deleteIdea') }}
|
||||
</button>
|
||||
<button class="btn btn-ghost" @click="deleteCurrentIdea">{{ $t('ideas.deleteIdea') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -195,7 +204,9 @@
|
||||
<textarea v-model="newIdeaDesc" :placeholder="$t('ideas.descPlaceholder')" rows="3" style="resize:vertical"></textarea>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" @click="showCaptureModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn-confirm" @click="confirmCapture">{{ $t('common.confirm') }}</button>
|
||||
<button class="btn-confirm" :disabled="creating" @click="confirmCapture">
|
||||
{{ creating ? $t('ideas.creating') : $t('common.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,6 +249,11 @@ const showCaptureModal = ref(false)
|
||||
const newIdeaTitle = ref('')
|
||||
const newIdeaDesc = ref('')
|
||||
|
||||
// ── 异步按钮禁用态(防双击重复提交,对齐 ProviderPanel.vue saving ref) ──
|
||||
const creating = ref(false)
|
||||
const deleting = ref(false)
|
||||
const promoting = ref(false)
|
||||
|
||||
const filters: { key: FilterKey; labelKey: string; icon: string }[] = [
|
||||
{ key: 'all', labelKey: 'ideas.filter.all', icon: '📋' },
|
||||
{ key: 'hot', labelKey: 'ideas.filter.hot', icon: '🔥' },
|
||||
@@ -383,23 +399,34 @@ function openCaptureModal() {
|
||||
}
|
||||
|
||||
async function confirmCapture() {
|
||||
if (!newIdeaTitle.value.trim()) return
|
||||
await store.createIdea({
|
||||
title: newIdeaTitle.value.trim(),
|
||||
description: newIdeaDesc.value.trim() || undefined,
|
||||
})
|
||||
showCaptureModal.value = false
|
||||
if (!newIdeaTitle.value.trim() || creating.value) return
|
||||
creating.value = true
|
||||
try {
|
||||
await store.createIdea({
|
||||
title: newIdeaTitle.value.trim(),
|
||||
description: newIdeaDesc.value.trim() || undefined,
|
||||
})
|
||||
showCaptureModal.value = false
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentIdea() {
|
||||
if (!currentIdea.value) return
|
||||
if (!currentIdea.value || deleting.value) return
|
||||
if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return
|
||||
await store.deleteIdea(currentIdea.value.id)
|
||||
selectedId.value = null
|
||||
deleting.value = true
|
||||
try {
|
||||
await store.deleteIdea(currentIdea.value.id)
|
||||
selectedId.value = null
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function promoteToProject() {
|
||||
if (!currentIdea.value) return
|
||||
if (!currentIdea.value || promoting.value) return
|
||||
promoting.value = true
|
||||
try {
|
||||
const res = await store.promoteIdea(currentIdea.value.id)
|
||||
router.push(`/projects/${res.project_id}`)
|
||||
@@ -407,6 +434,8 @@ async function promoteToProject() {
|
||||
const msg = e?.toString() ?? t('ideas.promoteFailed')
|
||||
console.error(t('ideas.promoteFailed'), e)
|
||||
Message.error(msg)
|
||||
} finally {
|
||||
promoting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,14 +96,14 @@
|
||||
<div class="detail-actions" v-if="!editing">
|
||||
<button class="btn btn-ghost btn-sm" @click="startEdit">{{ $t('knowledge.edit') }}</button>
|
||||
<template v-if="topTab === 'inbox'">
|
||||
<button class="btn btn-primary btn-sm" @click="publishCurrent">{{ $t('knowledge.publish') }}</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="rejectCurrent">{{ $t('knowledge.reject') }}</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="submitting" @click="publishCurrent">{{ $t('knowledge.publish') }}</button>
|
||||
<button class="btn btn-ghost btn-sm" :disabled="submitting" @click="rejectCurrent">{{ $t('knowledge.reject') }}</button>
|
||||
</template>
|
||||
<button v-else class="btn btn-ghost btn-sm" @click="archiveCurrent">{{ $t('knowledge.archive') }}</button>
|
||||
<button v-else class="btn btn-ghost btn-sm" :disabled="submitting" @click="archiveCurrent">{{ $t('knowledge.archive') }}</button>
|
||||
</div>
|
||||
<div class="detail-actions" v-else>
|
||||
<button class="btn btn-ghost btn-sm" @click="cancelEdit">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary btn-sm" @click="saveEdit" :disabled="!editForm.title.trim()">{{ $t('common.save') }}</button>
|
||||
<button class="btn btn-primary btn-sm" @click="saveEdit" :disabled="submitting || !editForm.title.trim()">{{ $t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="closeCreateModal">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submitCreate" :disabled="!form.title.trim()">{{ $t('knowledge.create') }}</button>
|
||||
<button class="btn btn-primary" @click="submitCreate" :disabled="submitting || !form.title.trim()">{{ $t('knowledge.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,6 +349,9 @@ watch(listItems, items => {
|
||||
const editing = ref(false)
|
||||
const editForm = ref({ title: '', content: '', tagsInput: '', confidence: '' })
|
||||
|
||||
// 异步操作禁用态(IPC 期间防双击重复提交):saveEdit/publishCurrent/rejectCurrent/archiveCurrent/submitCreate 共用
|
||||
const submitting = ref(false)
|
||||
|
||||
function startEdit() {
|
||||
if (!detail.value) return
|
||||
const k = detail.value.knowledge
|
||||
@@ -367,38 +370,58 @@ function cancelEdit() {
|
||||
|
||||
async function saveEdit() {
|
||||
if (!detail.value || !editForm.value.title.trim()) return
|
||||
const tags = editForm.value.tagsInput.split(',').map(t => t.trim()).filter(Boolean)
|
||||
await store.update(detail.value.knowledge.id, {
|
||||
title: editForm.value.title.trim(),
|
||||
content: editForm.value.content,
|
||||
tags: JSON.stringify(tags),
|
||||
confidence: editForm.value.confidence, // 空串=清空(后端映射 None),非 undefined
|
||||
})
|
||||
editing.value = false
|
||||
// 重新拉详情
|
||||
await selectKnowledge(detail.value.knowledge.id)
|
||||
submitting.value = true
|
||||
try {
|
||||
const tags = editForm.value.tagsInput.split(',').map(t => t.trim()).filter(Boolean)
|
||||
await store.update(detail.value.knowledge.id, {
|
||||
title: editForm.value.title.trim(),
|
||||
content: editForm.value.content,
|
||||
tags: JSON.stringify(tags),
|
||||
confidence: editForm.value.confidence, // 空串=清空(后端映射 None),非 undefined
|
||||
})
|
||||
editing.value = false
|
||||
// 重新拉详情
|
||||
await selectKnowledge(detail.value.knowledge.id)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 审核操作(收件箱) =====
|
||||
async function publishCurrent() {
|
||||
if (!detail.value) return
|
||||
await store.updateStatus(detail.value.knowledge.id, 'published')
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.updateStatus(detail.value.knowledge.id, 'published')
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectCurrent() {
|
||||
if (!detail.value) return
|
||||
await store.archive(detail.value.knowledge.id)
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.archive(detail.value.knowledge.id)
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveCurrent() {
|
||||
if (!detail.value) return
|
||||
await store.archive(detail.value.knowledge.id)
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.archive(detail.value.knowledge.id)
|
||||
selectedId.value = null
|
||||
detail.value = null
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 新增对话框 =====
|
||||
@@ -413,16 +436,21 @@ function closeCreateModal() {
|
||||
showCreateModal.value = false
|
||||
}
|
||||
async function submitCreate() {
|
||||
const tags = form.value.tagsInput.split(',').map(t => t.trim()).filter(Boolean)
|
||||
await store.create({
|
||||
kind: form.value.kind,
|
||||
title: form.value.title.trim(),
|
||||
content: form.value.content.trim(),
|
||||
tags: JSON.stringify(tags),
|
||||
confidence: form.value.confidence || undefined,
|
||||
})
|
||||
showCreateModal.value = false
|
||||
switchTopTab('inbox')
|
||||
submitting.value = true
|
||||
try {
|
||||
const tags = form.value.tagsInput.split(',').map(t => t.trim()).filter(Boolean)
|
||||
await store.create({
|
||||
kind: form.value.kind,
|
||||
title: form.value.title.trim(),
|
||||
content: form.value.content.trim(),
|
||||
tags: JSON.stringify(tags),
|
||||
confidence: form.value.confidence || undefined,
|
||||
})
|
||||
showCreateModal.value = false
|
||||
switchTopTab('inbox')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 事件解析辅助 =====
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="showNewTaskModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submitNewTask" :disabled="!newTaskTitle.trim()">{{ $t('projectDetail.confirmCreate') }}</button>
|
||||
<button class="btn btn-primary" @click="submitNewTask" :disabled="submitting || !newTaskTitle.trim()">{{ $t('projectDetail.confirmCreate') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,6 +208,7 @@
|
||||
:key="idx"
|
||||
class="btn"
|
||||
:class="idx === 0 ? 'btn-primary' : 'btn-ghost'"
|
||||
:disabled="submitting"
|
||||
@click="handleApproval(option)"
|
||||
>
|
||||
{{ option }}
|
||||
@@ -319,19 +320,27 @@ const newTaskTitle = ref('')
|
||||
const newTaskDesc = ref('')
|
||||
const newTaskBranch = ref('')
|
||||
|
||||
// 异步操作禁用态(IPC 期间防双击重复提交):submitNewTask 创建任务期间禁用确认按钮
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submitNewTask() {
|
||||
if (!newTaskTitle.value.trim()) return
|
||||
const r = await store.createTask({
|
||||
project_id: projectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
description: newTaskDesc.value.trim(),
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showNewTaskModal.value = false
|
||||
newTaskTitle.value = ''
|
||||
newTaskDesc.value = ''
|
||||
newTaskBranch.value = ''
|
||||
submitting.value = true
|
||||
try {
|
||||
const r = await store.createTask({
|
||||
project_id: projectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
description: newTaskDesc.value.trim(),
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showNewTaskModal.value = false
|
||||
newTaskTitle.value = ''
|
||||
newTaskDesc.value = ''
|
||||
newTaskBranch.value = ''
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 同步 ──
|
||||
@@ -406,8 +415,13 @@ async function handleImportDir() {
|
||||
|
||||
// ── 审批处理 ──
|
||||
async function handleApproval(decision: string) {
|
||||
await store.approveHumanApproval([decision])
|
||||
showApprovalDialog.value = false
|
||||
submitting.value = true
|
||||
try {
|
||||
await store.approveHumanApproval([decision])
|
||||
showApprovalDialog.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// F-260615-01: 多选审批
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 错误条:消费 store.error(加载/创建/删除失败对用户可见,对齐 Knowledge error-banner) -->
|
||||
<div v-if="store.error" class="error-banner">
|
||||
<span class="error-text">{{ store.error }}</span>
|
||||
<button class="error-dismiss" @click="store.clearError()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 新建项目模态框 -->
|
||||
<div v-if="showCreateModal" class="modal-overlay" @click.self="showCreateModal = false">
|
||||
<div class="modal-box">
|
||||
@@ -42,7 +48,7 @@
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-ghost" @click="showCreateModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submitCreate" :disabled="!newName.trim()">{{ $t('projects.confirmCreate') }}</button>
|
||||
<button class="btn btn-primary" @click="submitCreate" :disabled="submitting || !newName.trim()">{{ $t('projects.confirmCreate') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,6 +221,8 @@ const pathWarning = ref('')
|
||||
const scanning = ref(false)
|
||||
const scanError = ref('')
|
||||
const scanProjectType = ref('')
|
||||
// 创建提交中(防双击重复提交,对齐 ProviderPanel.saving 模式)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 选择目录 → 探测技术栈 + 查重
|
||||
async function pickDir() {
|
||||
@@ -267,13 +275,19 @@ async function aiScan() {
|
||||
async function submitCreate() {
|
||||
if (!newName.value.trim()) return
|
||||
if (pathWarning.value) return // 目录冲突,禁止创建
|
||||
const stack = detectedStack.value.length ? JSON.stringify(detectedStack.value) : undefined
|
||||
const r = await store.createProject(newName.value.trim(), newDesc.value.trim(), undefined, newPath.value || undefined, stack)
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showCreateModal.value = false
|
||||
newName.value = ''
|
||||
newDesc.value = ''
|
||||
clearDir()
|
||||
if (submitting.value) return // 防双击重复提交
|
||||
submitting.value = true
|
||||
try {
|
||||
const stack = detectedStack.value.length ? JSON.stringify(detectedStack.value) : undefined
|
||||
const r = await store.createProject(newName.value.trim(), newDesc.value.trim(), undefined, newPath.value || undefined, stack)
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showCreateModal.value = false
|
||||
newName.value = ''
|
||||
newDesc.value = ''
|
||||
clearDir()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 删除 / 回收站 ──
|
||||
@@ -603,6 +617,20 @@ async function runImport() {
|
||||
font-size: 14px; color: var(--df-text-dim);
|
||||
}
|
||||
|
||||
/* ===== 错误条(消费 store.error,对齐 Knowledge error-banner) ===== */
|
||||
.error-banner {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 8px 12px; margin-bottom: var(--df-gap-page);
|
||||
background: rgba(255,107,107,0.12); border: 0.5px solid var(--df-danger);
|
||||
border-radius: var(--df-radius-sm); color: var(--df-danger); font-size: 12px;
|
||||
}
|
||||
.error-text { word-break: break-word; }
|
||||
.error-dismiss {
|
||||
flex-shrink: 0; background: transparent; border: none; color: var(--df-danger);
|
||||
font-size: 14px; cursor: pointer; line-height: 1; padding: 0 2px;
|
||||
}
|
||||
.error-dismiss:hover { opacity: 0.7; }
|
||||
|
||||
/* ===== 卡片删除按钮 + 回收站 ===== */
|
||||
.card-top-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.card-del-btn {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</select>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" @click="showCreateModal = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn-confirm" @click="confirmCreate">{{ $t('common.confirm') }}</button>
|
||||
<button class="btn-confirm" @click="confirmCreate" :disabled="submitting || !newTaskTitle.trim()">{{ $t('common.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +133,9 @@ const newTaskDesc = ref('')
|
||||
const newTaskBranch = ref('')
|
||||
const newTaskPriority = ref(2) // 默认 medium
|
||||
|
||||
// 异步操作禁用态(IPC 期间防双击重复提交):confirmCreate 创建任务期间禁用确认按钮
|
||||
const submitting = ref(false)
|
||||
|
||||
interface TaskGroup {
|
||||
projectName: string
|
||||
icon: string
|
||||
@@ -217,15 +220,20 @@ function openCreateModal() {
|
||||
|
||||
async function confirmCreate() {
|
||||
if (!newTaskTitle.value.trim() || !newTaskProjectId.value) return
|
||||
const r = await store.createTask({
|
||||
project_id: newTaskProjectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
description: newTaskDesc.value.trim(),
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
priority: newTaskPriority.value,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showCreateModal.value = false
|
||||
submitting.value = true
|
||||
try {
|
||||
const r = await store.createTask({
|
||||
project_id: newTaskProjectId.value,
|
||||
title: newTaskTitle.value.trim(),
|
||||
description: newTaskDesc.value.trim(),
|
||||
branch_name: newTaskBranch.value.trim() || undefined,
|
||||
priority: newTaskPriority.value,
|
||||
})
|
||||
if (!r) return // 失败已 toast,保持弹窗不关
|
||||
showCreateModal.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// B-260615-29: 切换项目筛选时按 projectId 重载(避免仅前端 filter,跨项目视图不同步)
|
||||
|
||||
Reference in New Issue
Block a user