优化: 前端扫描文案 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:
2026-06-14 14:22:57 +08:00
parent d5a641797c
commit 02ff88fc61
8 changed files with 126 additions and 36 deletions

View File

@@ -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)
}