优化: CR-08 i18n batch2全量(ProjectDetail5处+TaskDetail16key+AiChat3处+store error fallback 23处+useAiSend 3处)+@/i18n路径统一+vite别名
This commit is contained in:
@@ -104,7 +104,7 @@
|
|||||||
<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="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
<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="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- 清空对话(真删 DB messages,带二次确认防误删) -->
|
<!-- 清空对话(真删 DB messages,带二次确认防误删) -->
|
||||||
<button class="ai-btn-icon" @click="confirmClearChat" title="清空">
|
<button class="ai-btn-icon" @click="confirmClearChat" :title="$t('aiChat.clearChat')">
|
||||||
<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 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>
|
<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 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- 嵌入模式:最大化/还原 + 分离 + 关闭 -->
|
<!-- 嵌入模式:最大化/还原 + 分离 + 关闭 -->
|
||||||
@@ -468,8 +468,7 @@ async function confirmDeleteConversation(id: string) {
|
|||||||
|
|
||||||
/** 清空当前对话(真删 DB messages,带二次确认防误删) */
|
/** 清空当前对话(真删 DB messages,带二次确认防误删) */
|
||||||
async function confirmClearChat() {
|
async function confirmClearChat() {
|
||||||
// i18n 不在本任务白名单:文案硬编码,后续补 aiChat.confirmClearChat key
|
if (!await confirmDialog(t('aiChat.confirmClearChat'))) return
|
||||||
if (!await confirmDialog('确定清空当前对话的所有消息?此操作不可恢复。')) return
|
|
||||||
await store.clearChat()
|
await store.clearChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,7 +684,7 @@ async function handleSend() {
|
|||||||
pendingSkill.value = skill
|
pendingSkill.value = skill
|
||||||
// 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象
|
// 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
showToast(`发送失败:${msg}`, 'error')
|
showToast(t('aiChat.toastSendFail', { msg }), 'error')
|
||||||
}
|
}
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { listen, emit } from '@tauri-apps/api/event'
|
|||||||
import { aiApi } from '../../api'
|
import { aiApi } from '../../api'
|
||||||
import { useAppSettingsStore } from '../../stores/appSettings'
|
import { useAppSettingsStore } from '../../stores/appSettings'
|
||||||
import { state } from '../../stores/ai'
|
import { state } from '../../stores/ai'
|
||||||
import i18n from '../../i18n'
|
import i18n from '@/i18n'
|
||||||
import { nextMsgId } from './aiShared'
|
import { nextMsgId } from './aiShared'
|
||||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||||
import { drainQueue } from './useAiSend'
|
import { drainQueue } from './useAiSend'
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import { invoke } from '@tauri-apps/api/core'
|
|||||||
import { aiApi } from '../../api'
|
import { aiApi } from '../../api'
|
||||||
import { useAppSettingsStore } from '../../stores/appSettings'
|
import { useAppSettingsStore } from '../../stores/appSettings'
|
||||||
import { state } from '../../stores/ai'
|
import { state } from '../../stores/ai'
|
||||||
|
import i18n from '../../i18n'
|
||||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||||
import { startListener, findToolCall } from './useAiEvents'
|
import { startListener, findToolCall } from './useAiEvents'
|
||||||
import { nextMsgId } from './aiShared'
|
import { nextMsgId } from './aiShared'
|
||||||
|
|
||||||
|
const t = ((i18n as any).global.t as (k: string, named?: Record<string, unknown>) => string).bind((i18n as any).global)
|
||||||
|
|
||||||
const appSettings = useAppSettingsStore()
|
const appSettings = useAppSettingsStore()
|
||||||
|
|
||||||
/// 待发送队列上限(超过抛错提示用户)
|
/// 待发送队列上限(超过抛错提示用户)
|
||||||
@@ -46,7 +49,7 @@ async function sendMessage(text: string, skill?: string) {
|
|||||||
}
|
}
|
||||||
if (backendGenerating) {
|
if (backendGenerating) {
|
||||||
if (state.queue.length >= QUEUE_LIMIT) {
|
if (state.queue.length >= QUEUE_LIMIT) {
|
||||||
throw new Error(`待发送队列已满(最多 ${QUEUE_LIMIT} 条)`)
|
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
|
||||||
}
|
}
|
||||||
// 后端真在生成但本地 streaming 已复位(false):状态不同步——对齐本地 streaming=true
|
// 后端真在生成但本地 streaming 已复位(false):状态不同步——对齐本地 streaming=true
|
||||||
// 让用户感知(禁用输入框等),并入队等待 AiCompleted 续发;不入队则消息静默丢失。
|
// 让用户感知(禁用输入框等),并入队等待 AiCompleted 续发;不入队则消息静默丢失。
|
||||||
@@ -62,7 +65,7 @@ async function sendMessage(text: string, skill?: string) {
|
|||||||
// 生成中:进入待发送队列,当前对话完成后(AiCompleted)由 drainQueue 自动续发
|
// 生成中:进入待发送队列,当前对话完成后(AiCompleted)由 drainQueue 自动续发
|
||||||
if (state.streaming) {
|
if (state.streaming) {
|
||||||
if (state.queue.length >= QUEUE_LIMIT) {
|
if (state.queue.length >= QUEUE_LIMIT) {
|
||||||
throw new Error(`待发送队列已满(最多 ${QUEUE_LIMIT} 条)`)
|
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
|
||||||
}
|
}
|
||||||
state.queue.push({ text: text.trim(), skill: skill || undefined })
|
state.queue.push({ text: text.trim(), skill: skill || undefined })
|
||||||
return
|
return
|
||||||
@@ -127,7 +130,7 @@ async function approveToolCall(toolCallId: string, approved: boolean) {
|
|||||||
console.error('[AI] 审批操作未送达后端:', e)
|
console.error('[AI] 审批操作未送达后端:', e)
|
||||||
if (tc) {
|
if (tc) {
|
||||||
tc.status = 'completed'
|
tc.status = 'completed'
|
||||||
tc.result = `审批操作未送达后端:${e instanceof Error ? e.message : String(e)}`
|
tc.result = t('ai.approvalNotDelivered', { error: e instanceof Error ? e.message : String(e) })
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
//! 循环依赖;下沉到 aiShared 后本模块不再 import useAiEvents,环消除)
|
//! 循环依赖;下沉到 aiShared 后本模块不再 import useAiEvents,环消除)
|
||||||
|
|
||||||
import { state } from '../../stores/ai'
|
import { state } from '../../stores/ai'
|
||||||
import i18n from '../../i18n'
|
import i18n from '@/i18n'
|
||||||
import { nextMsgId } from './aiShared'
|
import { nextMsgId } from './aiShared'
|
||||||
|
|
||||||
// composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t 而非 useI18n()。
|
// composable 内非组件上下文(无 setup),用 vue-i18n 全局实例的 t 而非 useI18n()。
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ export default {
|
|||||||
errorNetwork: 'Network connection failed. Please check your network.',
|
errorNetwork: 'Network connection failed. Please check your network.',
|
||||||
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
|
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
|
||||||
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
|
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
|
||||||
|
queueFull: 'Send queue is full (max {limit} items)',
|
||||||
|
approvalNotDelivered: 'Approval operation failed to reach the backend: {error}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,5 +60,8 @@ export default {
|
|||||||
|
|
||||||
// ── Confirm dialog ──
|
// ── Confirm dialog ──
|
||||||
confirmDeleteConversation: 'Delete chat "{title}"? This cannot be undone.',
|
confirmDeleteConversation: 'Delete chat "{title}"? This cannot be undone.',
|
||||||
|
clearChat: 'Clear',
|
||||||
|
confirmClearChat: 'Are you sure you want to clear all messages in this chat? This cannot be undone.',
|
||||||
|
toastSendFail: 'Send failed: {msg}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,5 +76,11 @@ export default {
|
|||||||
confirmDelete: 'Delete idea "{title}"? This cannot be undone.',
|
confirmDelete: 'Delete idea "{title}"? This cannot be undone.',
|
||||||
promoteFailed: 'Promotion failed',
|
promoteFailed: 'Promotion failed',
|
||||||
evalFailed: 'Evaluation failed',
|
evalFailed: 'Evaluation failed',
|
||||||
|
// Error fallbacks (user-visible via state.error)
|
||||||
|
err: {
|
||||||
|
loadFailed: 'Failed to load ideas',
|
||||||
|
createFailed: 'Failed to create idea',
|
||||||
|
deleteFailed: 'Failed to delete idea',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,5 +74,17 @@ export default {
|
|||||||
deployment_note: 'Deployment Note',
|
deployment_note: 'Deployment Note',
|
||||||
workflow_optimization: 'Workflow Optimization',
|
workflow_optimization: 'Workflow Optimization',
|
||||||
},
|
},
|
||||||
|
// Error fallbacks (user-visible via state.error)
|
||||||
|
err: {
|
||||||
|
loadFailed: 'Failed to load knowledge base',
|
||||||
|
loadInboxFailed: 'Failed to load inbox',
|
||||||
|
searchFailed: 'Search failed',
|
||||||
|
createFailed: 'Failed to create knowledge',
|
||||||
|
updateStatusFailed: 'Failed to update status',
|
||||||
|
archiveFailed: 'Failed to archive',
|
||||||
|
loadConfigFailed: 'Failed to load config',
|
||||||
|
saveConfigFailed: 'Failed to save config',
|
||||||
|
extractFailed: 'Extraction failed',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ export default {
|
|||||||
relocateConfirmRelocate: 'Relocate project directory to:\n{dir}\n\nTech stack will be re-detected. Confirm?',
|
relocateConfirmRelocate: 'Relocate project directory to:\n{dir}\n\nTech stack will be re-detected. Confirm?',
|
||||||
relocateConfirmBind: 'Bind project directory to:\n{dir}\n\nTech stack will be re-detected. Confirm?',
|
relocateConfirmBind: 'Bind project directory to:\n{dir}\n\nTech stack will be re-detected. Confirm?',
|
||||||
relocateFailed: 'Relocate failed: {msg}',
|
relocateFailed: 'Relocate failed: {msg}',
|
||||||
|
// Import directory
|
||||||
|
importDir: 'Import Directory',
|
||||||
|
importConfirm: 'Import as new project?\n{dir}\n\nThe directory name will be used as project name, tech stack will be detected, and README first section will fill description.',
|
||||||
|
importSuccess: 'Imported project "{name}"',
|
||||||
|
importFailed: 'Import failed: {msg}',
|
||||||
|
// Approval actions
|
||||||
|
approvalConfirm: 'Confirm ({count})',
|
||||||
|
approvalCancel: 'Cancel',
|
||||||
// Tech stack
|
// Tech stack
|
||||||
techStackLabel: 'Tech Stack',
|
techStackLabel: 'Tech Stack',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,5 +45,16 @@ export default {
|
|||||||
cancelled: '❌ Cancelled',
|
cancelled: '❌ Cancelled',
|
||||||
active: '🚀 In Progress',
|
active: '🚀 In Progress',
|
||||||
},
|
},
|
||||||
|
// Error fallbacks (user-visible via state.error)
|
||||||
|
err: {
|
||||||
|
loadFailed: 'Failed to load projects',
|
||||||
|
createFailed: 'Failed to create project',
|
||||||
|
importFailed: 'Failed to import project',
|
||||||
|
deleteFailed: 'Failed to delete project',
|
||||||
|
loadTrashFailed: 'Failed to load trash',
|
||||||
|
restoreFailed: 'Failed to restore project',
|
||||||
|
purgeFailed: 'Failed to delete permanently',
|
||||||
|
loadWorkflowFailed: 'Failed to load workflow records',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/i18n/en/taskDetail.ts
Normal file
24
src/i18n/en/taskDetail.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
taskDetail: {
|
||||||
|
// Header
|
||||||
|
backToList: '← Back to Tasks',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
// Status hints
|
||||||
|
loading: 'Loading...',
|
||||||
|
loadFailed: 'Failed to load',
|
||||||
|
// Panel
|
||||||
|
infoTitle: 'Task Info',
|
||||||
|
// Field labels
|
||||||
|
title: 'Title',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'Status',
|
||||||
|
priority: 'Priority',
|
||||||
|
project: 'Project',
|
||||||
|
branch: 'Branch',
|
||||||
|
assignee: 'Assignee',
|
||||||
|
baseBranch: 'Base Branch',
|
||||||
|
workflowDef: 'Workflow Definition',
|
||||||
|
createdAt: 'Created At',
|
||||||
|
updatedAt: 'Updated At',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -52,5 +52,11 @@ export default {
|
|||||||
merged: '✅ Merged',
|
merged: '✅ Merged',
|
||||||
abandoned: '🗑️ Abandoned',
|
abandoned: '🗑️ Abandoned',
|
||||||
},
|
},
|
||||||
|
// Error fallbacks (user-visible via state.error)
|
||||||
|
err: {
|
||||||
|
loadFailed: 'Failed to load tasks',
|
||||||
|
createFailed: 'Failed to create task',
|
||||||
|
deleteFailed: 'Failed to delete task',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ export default {
|
|||||||
errorNetwork: '网络连接失败,请检查网络',
|
errorNetwork: '网络连接失败,请检查网络',
|
||||||
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
|
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
|
||||||
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
|
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
|
||||||
|
queueFull: '待发送队列已满(最多 {limit} 条)',
|
||||||
|
approvalNotDelivered: '审批操作未送达后端:{error}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,5 +60,8 @@ export default {
|
|||||||
|
|
||||||
// ── 确认弹层 ──
|
// ── 确认弹层 ──
|
||||||
confirmDeleteConversation: '确定删除对话「{title}」?该操作不可恢复。',
|
confirmDeleteConversation: '确定删除对话「{title}」?该操作不可恢复。',
|
||||||
|
clearChat: '清空',
|
||||||
|
confirmClearChat: '确定清空当前对话的所有消息?此操作不可恢复。',
|
||||||
|
toastSendFail: '发送失败:{msg}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,5 +76,11 @@ export default {
|
|||||||
confirmDelete: '确定删除灵感「{title}」?此操作不可撤销。',
|
confirmDelete: '确定删除灵感「{title}」?此操作不可撤销。',
|
||||||
promoteFailed: '立项失败',
|
promoteFailed: '立项失败',
|
||||||
evalFailed: '评估失败',
|
evalFailed: '评估失败',
|
||||||
|
// 错误回退(state.error 用户可见)
|
||||||
|
err: {
|
||||||
|
loadFailed: '加载灵感失败',
|
||||||
|
createFailed: '创建灵感失败',
|
||||||
|
deleteFailed: '删除灵感失败',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,5 +90,17 @@ export default {
|
|||||||
deployment_note: '部署经验',
|
deployment_note: '部署经验',
|
||||||
workflow_optimization: '工作流优化',
|
workflow_optimization: '工作流优化',
|
||||||
},
|
},
|
||||||
|
// 错误回退(state.error 用户可见)
|
||||||
|
err: {
|
||||||
|
loadFailed: '加载知识库失败',
|
||||||
|
loadInboxFailed: '加载收件箱失败',
|
||||||
|
searchFailed: '检索失败',
|
||||||
|
createFailed: '创建知识失败',
|
||||||
|
updateStatusFailed: '更新状态失败',
|
||||||
|
archiveFailed: '归档失败',
|
||||||
|
loadConfigFailed: '加载配置失败',
|
||||||
|
saveConfigFailed: '保存配置失败',
|
||||||
|
extractFailed: '立即抽取失败',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ export default {
|
|||||||
relocateConfirmRelocate: '将项目目录重定位到:\n{dir}\n\n技术栈将重新探测。确认?',
|
relocateConfirmRelocate: '将项目目录重定位到:\n{dir}\n\n技术栈将重新探测。确认?',
|
||||||
relocateConfirmBind: '将项目目录绑定到:\n{dir}\n\n技术栈将重新探测。确认?',
|
relocateConfirmBind: '将项目目录绑定到:\n{dir}\n\n技术栈将重新探测。确认?',
|
||||||
relocateFailed: '重定位失败: {msg}',
|
relocateFailed: '重定位失败: {msg}',
|
||||||
|
// 导入目录
|
||||||
|
importDir: '导入目录',
|
||||||
|
importConfirm: '导入目录为新项目?\n{dir}\n\n将自动用目录名作项目名,探测技术栈,并读 README 首段填描述。',
|
||||||
|
importSuccess: '已导入项目「{name}」',
|
||||||
|
importFailed: '导入失败: {msg}',
|
||||||
|
// 审批操作
|
||||||
|
approvalConfirm: '确认({count})',
|
||||||
|
approvalCancel: '取消',
|
||||||
// 技术栈
|
// 技术栈
|
||||||
techStackLabel: '技术栈',
|
techStackLabel: '技术栈',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,5 +44,16 @@ export default {
|
|||||||
cancelled: '❌ 已取消',
|
cancelled: '❌ 已取消',
|
||||||
active: '🚀 进行中',
|
active: '🚀 进行中',
|
||||||
},
|
},
|
||||||
|
// 错误回退(state.error 用户可见)
|
||||||
|
err: {
|
||||||
|
loadFailed: '加载项目失败',
|
||||||
|
createFailed: '创建项目失败',
|
||||||
|
importFailed: '导入项目失败',
|
||||||
|
deleteFailed: '删除项目失败',
|
||||||
|
loadTrashFailed: '加载回收站失败',
|
||||||
|
restoreFailed: '恢复项目失败',
|
||||||
|
purgeFailed: '彻底删除失败',
|
||||||
|
loadWorkflowFailed: '加载工作流记录失败',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/i18n/zh-CN/taskDetail.ts
Normal file
24
src/i18n/zh-CN/taskDetail.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
taskDetail: {
|
||||||
|
// 头部
|
||||||
|
backToList: '← 返回任务列表',
|
||||||
|
refresh: '刷新',
|
||||||
|
// 状态提示
|
||||||
|
loading: '加载中...',
|
||||||
|
loadFailed: '加载失败',
|
||||||
|
// 面板
|
||||||
|
infoTitle: '任务信息',
|
||||||
|
// 字段标签
|
||||||
|
title: '标题',
|
||||||
|
description: '描述',
|
||||||
|
status: '状态',
|
||||||
|
priority: '优先级',
|
||||||
|
project: '关联项目',
|
||||||
|
branch: '分支',
|
||||||
|
assignee: '负责人',
|
||||||
|
baseBranch: '基础分支',
|
||||||
|
workflowDef: '工作流定义',
|
||||||
|
createdAt: '创建时间',
|
||||||
|
updatedAt: '更新时间',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -52,5 +52,11 @@ export default {
|
|||||||
merged: '✅ 已合并',
|
merged: '✅ 已合并',
|
||||||
abandoned: '🗑️ 已放弃',
|
abandoned: '🗑️ 已放弃',
|
||||||
},
|
},
|
||||||
|
// 错误回退(state.error 用户可见)
|
||||||
|
err: {
|
||||||
|
loadFailed: '加载任务失败',
|
||||||
|
createFailed: '创建任务失败',
|
||||||
|
deleteFailed: '删除任务失败',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* 正确做法:先转 number 再 new Date。本模块统一处理毫秒字符串 / 秒级 / ISO / number / 空。
|
* 正确做法:先转 number 再 new Date。本模块统一处理毫秒字符串 / 秒级 / ISO / number / 空。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import i18n from '../i18n'
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
// CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
|
// CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
|
||||||
// utils 纯函数无 setup,用 vue-i18n 全局实例(参考 useAiEvents.ts:16-25),any 中转规避 TS2589。
|
// utils 纯函数无 setup,用 vue-i18n 全局实例(参考 useAiEvents.ts:16-25),any 中转规避 TS2589。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</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>
|
||||||
<button class="btn btn-ghost" type="button" @click="handleImportDir">导入目录</button>
|
<button class="btn btn-ghost" type="button" @click="handleImportDir">{{ $t('projectDetail.importDir') }}</button>
|
||||||
<button class="btn btn-danger" @click="handleDelete">{{ $t('projectDetail.delete') }}</button>
|
<button class="btn btn-danger" @click="handleDelete">{{ $t('projectDetail.delete') }}</button>
|
||||||
<button class="btn btn-primary" @click="showNewTaskModal = true">{{ $t('projectDetail.newTask') }}</button>
|
<button class="btn btn-primary" @click="showNewTaskModal = true">{{ $t('projectDetail.newTask') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
:disabled="multiDecisions.length === 0"
|
:disabled="multiDecisions.length === 0"
|
||||||
@click="handleApprovalMulti"
|
@click="handleApprovalMulti"
|
||||||
>
|
>
|
||||||
确认({{ multiDecisions.length }})
|
{{ $t('projectDetail.approvalConfirm', { count: multiDecisions.length }) }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-ghost" @click="handleCancelApproval">取消</button>
|
<button class="btn btn-ghost" @click="handleCancelApproval">{{ $t('projectDetail.approvalCancel') }}</button>
|
||||||
<button class="btn btn-ghost" @click="showApprovalDialog = false">{{ $t('projectDetail.approvalLater') }}</button>
|
<button class="btn btn-ghost" @click="showApprovalDialog = false">{{ $t('projectDetail.approvalLater') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,25 +383,24 @@ async function relocateDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导入历史项目(选已存在目录 → 后端创建实体+绑定+探测栈+读 README 首段一步完成)
|
// 导入历史项目(选已存在目录 → 后端创建实体+绑定+探测栈+读 README 首段一步完成)
|
||||||
// 文案硬编码:projectDetail i18n 不在本次任务白名单,改用内联中文(避免引用不存在的 key)
|
|
||||||
async function handleImportDir() {
|
async function handleImportDir() {
|
||||||
try {
|
try {
|
||||||
const selected = await open({ directory: true, multiple: false })
|
const selected = await open({ directory: true, multiple: false })
|
||||||
if (!selected || Array.isArray(selected)) return
|
if (!selected || Array.isArray(selected)) return
|
||||||
const dir = selected as string
|
const dir = selected as string
|
||||||
if (!await confirmDialog(`导入目录为新项目?\n${dir}\n\n将自动用目录名作项目名,探测技术栈,并读 README 首段填描述。`)) return
|
if (!await confirmDialog(t('projectDetail.importConfirm', { dir }))) return
|
||||||
const record = await store.importProject({ path: dir })
|
const record = await store.importProject({ path: dir })
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// store 已 toast 错误
|
// store 已 toast 错误
|
||||||
if (store.error) Message.error(store.error)
|
if (store.error) Message.error(store.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Message.success(`已导入项目「${record.name}」`)
|
Message.success(t('projectDetail.importSuccess', { name: record.name }))
|
||||||
// 跳转到新导入项目的详情页
|
// 跳转到新导入项目的详情页
|
||||||
router.push(`/projects/${record.id}`)
|
router.push(`/projects/${record.id}`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('导入失败:', e)
|
console.error('导入失败:', e)
|
||||||
Message.error(`导入失败: ${e?.toString() ?? '未知错误'}`)
|
Message.error(t('projectDetail.importFailed', { msg: e?.toString() ?? t('common.unknownError') }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ import { aiApi, knowledgeApi } from '../api'
|
|||||||
import { useAppSettingsStore } from '../stores/appSettings'
|
import { useAppSettingsStore } from '../stores/appSettings'
|
||||||
import { useConfirm } from '../composables/useConfirm'
|
import { useConfirm } from '../composables/useConfirm'
|
||||||
import type { AiProviderConfig } from '../api/types'
|
import type { AiProviderConfig } from '../api/types'
|
||||||
import i18n from '../i18n'
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appSettings = useAppSettingsStore()
|
const appSettings = useAppSettingsStore()
|
||||||
|
|||||||
@@ -3,33 +3,33 @@
|
|||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<router-link to="/tasks" class="back-link">← 返回任务列表</router-link>
|
<router-link to="/tasks" class="back-link">{{ $t('taskDetail.backToList') }}</router-link>
|
||||||
<h1>{{ task?.title ?? '...' }}</h1>
|
<h1>{{ task?.title ?? '...' }}</h1>
|
||||||
<span v-if="task" class="status-tag" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
<span v-if="task" class="status-tag" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
||||||
<span v-if="task" class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
|
<span v-if="task" class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="btn btn-ghost" type="button" @click="refresh">刷新</button>
|
<button class="btn btn-ghost" type="button" @click="refresh">{{ $t('taskDetail.refresh') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 加载/错误态 -->
|
<!-- 加载/错误态 -->
|
||||||
<div v-if="loading" class="empty-hint">加载中...</div>
|
<div v-if="loading" class="empty-hint">{{ $t('taskDetail.loading') }}</div>
|
||||||
<div v-else-if="errorMsg" class="empty-hint error-hint">⚠ {{ errorMsg }}</div>
|
<div v-else-if="errorMsg" class="empty-hint error-hint">⚠ {{ errorMsg }}</div>
|
||||||
|
|
||||||
<!-- 主体 -->
|
<!-- 主体 -->
|
||||||
<div v-else-if="task" class="detail-grid">
|
<div v-else-if="task" class="detail-grid">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>任务信息</h2>
|
<h2>{{ $t('taskDetail.infoTitle') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-info">
|
<div class="task-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">标题</span>
|
<span class="label">{{ $t('taskDetail.title') }}</span>
|
||||||
<span class="value">{{ task.title }}</span>
|
<span class="value">{{ task.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item info-block">
|
<div class="info-item info-block">
|
||||||
<span class="label">描述</span>
|
<span class="label">{{ $t('taskDetail.description') }}</span>
|
||||||
<!-- B-24:任务描述 Markdown 渲染(## 标题 / - 列表 等),v-html 经 useMarkdown DOMPurify sanitize -->
|
<!-- B-24:任务描述 Markdown 渲染(## 标题 / - 列表 等),v-html 经 useMarkdown DOMPurify sanitize -->
|
||||||
<span
|
<span
|
||||||
class="value description ai-md"
|
class="value description ai-md"
|
||||||
@@ -39,19 +39,19 @@
|
|||||||
<span v-else class="value description">—</span>
|
<span v-else class="value description">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">状态</span>
|
<span class="label">{{ $t('taskDetail.status') }}</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<span class="status-tag" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
<span class="status-tag" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">优先级</span>
|
<span class="label">{{ $t('taskDetail.priority') }}</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<span class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
|
<span class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">关联项目</span>
|
<span class="label">{{ $t('taskDetail.project') }}</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<router-link v-if="task.project_id" :to="`/projects/${task.project_id}`" class="project-link">
|
<router-link v-if="task.project_id" :to="`/projects/${task.project_id}`" class="project-link">
|
||||||
{{ projectName }}
|
{{ projectName }}
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">分支</span>
|
<span class="label">{{ $t('taskDetail.branch') }}</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<span v-if="task.branch_name" class="branch-tag">
|
<span v-if="task.branch_name" class="branch-tag">
|
||||||
<span class="branch-icon">⑂</span>{{ task.branch_name }}
|
<span class="branch-icon">⑂</span>{{ task.branch_name }}
|
||||||
@@ -69,23 +69,23 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">负责人</span>
|
<span class="label">{{ $t('taskDetail.assignee') }}</span>
|
||||||
<span class="value">{{ task.assignee ?? '—' }}</span>
|
<span class="value">{{ task.assignee ?? '—' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">基础分支</span>
|
<span class="label">{{ $t('taskDetail.baseBranch') }}</span>
|
||||||
<span class="value">{{ task.base_branch ?? '—' }}</span>
|
<span class="value">{{ task.base_branch ?? '—' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">工作流定义</span>
|
<span class="label">{{ $t('taskDetail.workflowDef') }}</span>
|
||||||
<span class="value mono">{{ task.workflow_def_id ?? '—' }}</span>
|
<span class="value mono">{{ task.workflow_def_id ?? '—' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">创建时间</span>
|
<span class="label">{{ $t('taskDetail.createdAt') }}</span>
|
||||||
<span class="value">{{ formatDate(task.created_at) }}</span>
|
<span class="value">{{ formatDate(task.created_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">更新时间</span>
|
<span class="label">{{ $t('taskDetail.updatedAt') }}</span>
|
||||||
<span class="value">{{ formatDate(task.updated_at) }}</span>
|
<span class="value">{{ formatDate(task.updated_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +96,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { taskApi, projectApi } from '../api'
|
import { taskApi, projectApi } from '../api'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
@@ -108,6 +109,7 @@ import {
|
|||||||
} from '../constants/project'
|
} from '../constants/project'
|
||||||
import type { TaskRecord, ProjectRecord } from '../api/types'
|
import type { TaskRecord, ProjectRecord } from '../api/types'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -141,7 +143,7 @@ async function load() {
|
|||||||
projects.value = ps
|
projects.value = ps
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
task.value = null
|
task.value = null
|
||||||
errorMsg.value = e?.toString() ?? '加载失败'
|
errorMsg.value = e?.toString() ?? t('taskDetail.loadFailed')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user