diff --git a/src/i18n/en/projectDetail.ts b/src/i18n/en/projectDetail.ts index 5b559b9..db6d13a 100644 --- a/src/i18n/en/projectDetail.ts +++ b/src/i18n/en/projectDetail.ts @@ -48,6 +48,13 @@ export default { relocateDir: 'Relocate', bindDir: 'Bind Directory', dirStatusLabel: 'Directory Status', + dirChecking: 'Checking…', + dirExists: '✓ Directory exists', + dirMissing: '⚠ Directory moved, recommend relocate', + dirConflict: 'This directory is already bound to project "{name}"', + 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?', + relocateFailed: 'Relocate failed: {msg}', // Tech stack techStackLabel: 'Tech Stack', }, diff --git a/src/i18n/en/projects.ts b/src/i18n/en/projects.ts index 885a7b4..b26d243 100644 --- a/src/i18n/en/projects.ts +++ b/src/i18n/en/projects.ts @@ -26,9 +26,16 @@ export default { confirmPurge: 'Permanently delete project "{name}"? Tasks/branches/releases will be physically deleted and cannot be recovered!', // New fields codeDirLabel: 'Code Directory', + codeDirOptionalHint: '(optional, auto-detects tech stack once bound)', codeDirPlaceholder: 'Click the button on the right to select project directory', selectDir: 'Select Directory', clearDir: 'Clear', + // AI scan + aiScanning: 'AI scanning…', + aiScanFill: '🤖 AI scan & autofill', + aiScanNoDesc: 'AI could not generate a description, please fill manually', + aiScanFailed: 'AI scan failed', + dirConflict: 'This directory is already bound to project "{name}"', // Status labels (PROJECT_STATUS_LABELS values in constants/project.ts use these keys) status: { planning: '📐 Planning', diff --git a/src/i18n/zh-CN/projectDetail.ts b/src/i18n/zh-CN/projectDetail.ts index 2d1cfd1..2ee8537 100644 --- a/src/i18n/zh-CN/projectDetail.ts +++ b/src/i18n/zh-CN/projectDetail.ts @@ -48,6 +48,13 @@ export default { relocateDir: '重定位', bindDir: '绑定目录', dirStatusLabel: '目录状态', + dirChecking: '检测中…', + dirExists: '✓ 目录存在', + dirMissing: '⚠ 目录已移走,建议重定位', + dirConflict: '该目录已被项目「{name}」绑定', + relocateConfirmRelocate: '将项目目录重定位到:\n{dir}\n\n技术栈将重新探测。确认?', + relocateConfirmBind: '将项目目录绑定到:\n{dir}\n\n技术栈将重新探测。确认?', + relocateFailed: '重定位失败: {msg}', // 技术栈 techStackLabel: '技术栈', }, diff --git a/src/i18n/zh-CN/projects.ts b/src/i18n/zh-CN/projects.ts index 6d765bf..67e2d57 100644 --- a/src/i18n/zh-CN/projects.ts +++ b/src/i18n/zh-CN/projects.ts @@ -11,10 +11,17 @@ export default { descLabel: '项目描述', descPlaceholder: '简要描述项目目标', codeDirLabel: '代码目录', + codeDirOptionalHint: '(可选,绑定后自动识别技术栈)', codeDirPlaceholder: '点击右侧选择项目所在目录', selectDir: '选择目录', clearDir: '清除', confirmCreate: '确认创建', + // AI 扫描 + aiScanning: 'AI 扫描中…', + aiScanFill: '🤖 AI 扫描填信息', + aiScanNoDesc: 'AI 未能生成描述,可手动填写', + aiScanFailed: 'AI 扫描失败', + dirConflict: '该目录已被项目「{name}」绑定', // 回收站模态框 trashTitle: '🗑 回收站', trashEmpty: '回收站为空', diff --git a/src/utils/project.ts b/src/utils/project.ts new file mode 100644 index 0000000..d8659fa --- /dev/null +++ b/src/utils/project.ts @@ -0,0 +1,20 @@ +//! 项目相关纯工具函数(无副作用、无依赖,便于跨视图复用) +//! +//! parseStack 此前在 Projects.vue 与 ProjectDetail.vue 各有一份重复实现, +//! 现抽到此处统一维护。 + +/** + * 解析技术栈 JSON 数组字符串(防崩)。 + * + * 后端将技术栈以 `JSON.stringify(["vue","rust"])` 形式存于 project.stack 字段; + * 解析失败或非数组时返回空数组,避免渲染崩溃。 + */ +export function parseStack(stack: string | null | undefined): string[] { + if (!stack) return [] + try { + const arr = JSON.parse(stack) + return Array.isArray(arr) ? arr : [] + } catch { + return [] + } +} diff --git a/src/views/Ideas.vue b/src/views/Ideas.vue index 265531c..1543f8a 100644 --- a/src/views/Ideas.vue +++ b/src/views/Ideas.vue @@ -193,6 +193,9 @@ + + + @@ -200,14 +203,31 @@ import { ref, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' +import { Message } from '@arco-design/web-vue' import { useProjectStore } from '../stores/project' import { formatDate } from '../utils/time' +import ConfirmDialog from '../components/ConfirmDialog.vue' import type { IdeaRecord } from '../api/types' const { t } = useI18n() const router = useRouter() const store = useProjectStore() +// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ── +const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) }) +function confirmDialog(msg: string): Promise { + return new Promise(resolve => { + confirmState.value.msg = msg + confirmState.value.visible = true + confirmState.value.resolve = resolve + }) +} +function answerConfirm(ok: boolean) { + confirmState.value.visible = false + confirmState.value.resolve?.(ok) + confirmState.value.resolve = null +} + type FilterKey = 'all' | 'hot' | 'pending' | 'promoted' const activeFilter = ref('all') @@ -375,7 +395,7 @@ async function confirmCapture() { async function deleteCurrentIdea() { if (!currentIdea.value) return - if (!confirm(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return + if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return await store.deleteIdea(currentIdea.value.id) selectedId.value = null } @@ -388,7 +408,7 @@ async function promoteToProject() { } catch (e: any) { const msg = e?.toString() ?? t('ideas.promoteFailed') console.error(t('ideas.promoteFailed'), e) - alert(msg) + Message.error(msg) } } diff --git a/src/views/ProjectDetail.vue b/src/views/ProjectDetail.vue index e3d61b9..ec17bb7 100644 --- a/src/views/ProjectDetail.vue +++ b/src/views/ProjectDetail.vue @@ -90,7 +90,7 @@
{{ $t('projectDetail.dirStatusLabel') }} - {{ pathExists === null ? '检测中…' : pathExists ? '✓ 目录存在' : '⚠ 目录已移走,建议重定位' }} + {{ pathExists === null ? $t('projectDetail.dirChecking') : pathExists ? $t('projectDetail.dirExists') : $t('projectDetail.dirMissing') }}
@@ -193,6 +193,9 @@ + + + @@ -200,11 +203,14 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' +import { Message } from '@arco-design/web-vue' import { open } from '@tauri-apps/plugin-dialog' import { useProjectStore } from '../stores/project' import { projectApi } from '../api' import { formatDate } from '../utils/time' +import { parseStack } from '../utils/project' import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project' +import ConfirmDialog from '../components/ConfirmDialog.vue' import type { WorkflowEventPayload } from '../api/types' const route = useRoute() @@ -215,6 +221,21 @@ const logListRef = ref(null) const showApprovalDialog = ref(false) let unlisten: (() => void) | null = null +// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ── +const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) }) +function confirmDialog(msg: string): Promise { + return new Promise(resolve => { + confirmState.value.msg = msg + confirmState.value.visible = true + confirmState.value.resolve = resolve + }) +} +function answerConfirm(ok: boolean) { + confirmState.value.visible = false + confirmState.value.resolve?.(ok) + confirmState.value.resolve = null +} + // ── 阶段定义(labelKey 对应 i18n projectDetail.stage*) ── const stages = [ { key: 'idea', labelKey: 'stageIdea' }, @@ -317,17 +338,6 @@ async function handleSync() { // ── 代码目录绑定 ── const pathExists = ref(null) -// 解析技术栈 JSON 数组字符串(防崩) -function parseStack(stack: string | null): string[] { - if (!stack) return [] - try { - const arr = JSON.parse(stack) - return Array.isArray(arr) ? arr : [] - } catch { - return [] - } -} - // 检查绑定目录是否存在(详情页「目录是否还在」) async function checkPath() { const p = currentProject.value @@ -351,16 +361,19 @@ async function relocateDir() { try { const conflict = await projectApi.checkBinding(dir, p.id) if (conflict) { - alert(`该目录已被项目「${conflict.name}」绑定`) + Message.warning(t('projectDetail.dirConflict', { name: conflict.name })) return } } catch { /* ignore */ } - if (!confirm(`将项目目录${currentProject.value?.path ? '重定位' : '绑定'}到:\n${dir}\n\n技术栈将重新探测。确认?`)) return + const confirmMsg = p.path + ? t('projectDetail.relocateConfirmRelocate', { dir }) + : t('projectDetail.relocateConfirmBind', { dir }) + if (!await confirmDialog(confirmMsg)) return await store.relocateProjectPath(p.id, dir) await checkPath() } catch (e: any) { console.error('重定位失败:', e) - alert('重定位失败: ' + (e?.toString() ?? '未知错误')) + Message.error(t('projectDetail.relocateFailed', { msg: e?.toString() ?? '未知错误' })) } } @@ -374,7 +387,7 @@ async function handleApproval(decision: string) { async function handleDelete() { const p = currentProject.value if (!p) return - if (!confirm(t('projectDetail.confirmDelete', { name: p.name }))) return + if (!await confirmDialog(t('projectDetail.confirmDelete', { name: p.name }))) return await store.deleteProject(p.id) router.push('/projects') } diff --git a/src/views/Projects.vue b/src/views/Projects.vue index dd50443..7217348 100644 --- a/src/views/Projects.vue +++ b/src/views/Projects.vue @@ -21,7 +21,7 @@