优化: 前端扫描文案 i18n + parseStack 抽公共 + alert/confirm 换组件
- 扫描/目录状态文案走 $t(Projects/ProjectDetail,zh-CN+en 同步) - parseStack 抽公共到 src/utils/project.ts(删两份本地实现) - 8 处原生 alert/confirm 全换(confirm→ConfirmDialog / alert→Arco Message),控制流语义不变 Sprint 20 审查 ⑥⑦⑧。vue-tsc 0 error,验证 safe 0 issue
This commit is contained in:
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '技术栈',
|
||||
},
|
||||
|
||||
@@ -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: '回收站为空',
|
||||
|
||||
20
src/utils/project.ts
Normal file
20
src/utils/project.ts
Normal file
@@ -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 []
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹层(删除想法,替代原生 window.confirm) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<FilterKey>('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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="info-item" v-if="currentProject.path">
|
||||
<span class="label">{{ $t('projectDetail.dirStatusLabel') }}</span>
|
||||
<span :class="pathExists === false ? 'path-missing' : 'path-ok'">
|
||||
{{ pathExists === null ? '检测中…' : pathExists ? '✓ 目录存在' : '⚠ 目录已移走,建议重定位' }}
|
||||
{{ pathExists === null ? $t('projectDetail.dirChecking') : pathExists ? $t('projectDetail.dirExists') : $t('projectDetail.dirMissing') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -193,6 +193,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹层(删除/重定位确认,替代原生 window.confirm/alert) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<HTMLElement | null>(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<boolean> {
|
||||
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<boolean | null>(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')
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<textarea v-model="newDesc" :placeholder="$t('projects.descPlaceholder')" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label>{{ $t('projects.codeDirLabel') }}(可选,绑定后自动识别技术栈)</label>
|
||||
<label>{{ $t('projects.codeDirLabel') }}{{ $t('projects.codeDirOptionalHint') }}</label>
|
||||
<div class="dir-row">
|
||||
<input v-model="newPath" :placeholder="$t('projects.codeDirPlaceholder')" readonly />
|
||||
<button class="btn btn-ghost btn-sm" type="button" @click="pickDir">{{ $t('projects.selectDir') }}</button>
|
||||
@@ -30,7 +30,7 @@
|
||||
<div v-if="pathWarning" class="path-warning">⚠ {{ pathWarning }}</div>
|
||||
<div v-if="newPath" class="scan-row">
|
||||
<button class="btn btn-ghost btn-sm" type="button" @click="aiScan" :disabled="scanning">
|
||||
{{ scanning ? 'AI 扫描中…' : '🤖 AI 扫描填信息' }}
|
||||
{{ scanning ? $t('projects.aiScanning') : $t('projects.aiScanFill') }}
|
||||
</button>
|
||||
<span v-if="scanProjectType" class="tech-tag">{{ scanProjectType }}</span>
|
||||
</div>
|
||||
@@ -107,6 +107,9 @@
|
||||
<div v-if="!store.loading && store.projects.length === 0" class="empty-state">
|
||||
{{ $t('projects.empty') }}
|
||||
</div>
|
||||
|
||||
<!-- 确认弹层(删除/彻底删除,替代原生 window.confirm) -->
|
||||
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,7 +121,9 @@ 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 as statusLabel, projectBadgeClass as stageClass } from '../constants/project'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
import type { ProjectRecord } from '../api/types'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -128,6 +133,21 @@ const { t } = useI18n()
|
||||
// 状态映射统一走 ../constants/project(与 ProjectDetail/Tasks/Dashboard 一致)
|
||||
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
|
||||
|
||||
// ── 确认弹层(替代原生 window.confirm,后者在 Tauri webview 带 localhost 来源信息无法去除) ──
|
||||
const confirmState = ref({ visible: false, msg: '', resolve: null as null | ((v: boolean) => void) })
|
||||
function confirmDialog(msg: string): Promise<boolean> {
|
||||
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
|
||||
}
|
||||
|
||||
// ── 新建项目 ──
|
||||
const showCreateModal = ref(false)
|
||||
const newName = ref('')
|
||||
@@ -139,17 +159,6 @@ const scanning = ref(false)
|
||||
const scanError = ref('')
|
||||
const scanProjectType = ref('')
|
||||
|
||||
// 解析技术栈 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 pickDir() {
|
||||
try {
|
||||
@@ -163,7 +172,7 @@ async function pickDir() {
|
||||
// 查重(已被其他项目绑定则警告,禁止创建)
|
||||
try {
|
||||
const conflict = await projectApi.checkBinding(selected as string)
|
||||
if (conflict) pathWarning.value = `该目录已被项目「${conflict.name}」绑定`
|
||||
if (conflict) pathWarning.value = t('projects.dirConflict', { name: conflict.name })
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) {
|
||||
console.error('选择目录失败:', e)
|
||||
@@ -190,9 +199,9 @@ async function aiScan() {
|
||||
detectedStack.value = result.stack
|
||||
if (result.description) newDesc.value = result.description
|
||||
scanProjectType.value = result.project_type ?? ''
|
||||
if (!result.description) scanError.value = 'AI 未能生成描述,可手动填写'
|
||||
if (!result.description) scanError.value = t('projects.aiScanNoDesc')
|
||||
} catch (e: any) {
|
||||
scanError.value = e?.toString() ?? 'AI 扫描失败'
|
||||
scanError.value = e?.toString() ?? t('projects.aiScanFailed')
|
||||
} finally {
|
||||
scanning.value = false
|
||||
}
|
||||
@@ -214,7 +223,7 @@ async function submitCreate() {
|
||||
const showTrashModal = ref(false)
|
||||
|
||||
async function handleDelete(project: ProjectRecord) {
|
||||
if (!confirm(t('projects.confirmDelete', { name: project.name }))) return
|
||||
if (!await confirmDialog(t('projects.confirmDelete', { name: project.name }))) return
|
||||
await store.deleteProject(project.id)
|
||||
}
|
||||
|
||||
@@ -228,7 +237,7 @@ async function handleRestore(id: string) {
|
||||
}
|
||||
|
||||
async function handlePurge(project: ProjectRecord) {
|
||||
if (!confirm(t('projects.confirmPurge', { name: project.name }))) return
|
||||
if (!await confirmDialog(t('projects.confirmPurge', { name: project.name }))) return
|
||||
await store.purgeProject(project.id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user