优化: 前端扫描文案 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',
|
relocateDir: 'Relocate',
|
||||||
bindDir: 'Bind Directory',
|
bindDir: 'Bind Directory',
|
||||||
dirStatusLabel: 'Directory Status',
|
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
|
// Tech stack
|
||||||
techStackLabel: '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!',
|
confirmPurge: 'Permanently delete project "{name}"? Tasks/branches/releases will be physically deleted and cannot be recovered!',
|
||||||
// New fields
|
// New fields
|
||||||
codeDirLabel: 'Code Directory',
|
codeDirLabel: 'Code Directory',
|
||||||
|
codeDirOptionalHint: '(optional, auto-detects tech stack once bound)',
|
||||||
codeDirPlaceholder: 'Click the button on the right to select project directory',
|
codeDirPlaceholder: 'Click the button on the right to select project directory',
|
||||||
selectDir: 'Select Directory',
|
selectDir: 'Select Directory',
|
||||||
clearDir: 'Clear',
|
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 labels (PROJECT_STATUS_LABELS values in constants/project.ts use these keys)
|
||||||
status: {
|
status: {
|
||||||
planning: '📐 Planning',
|
planning: '📐 Planning',
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ export default {
|
|||||||
relocateDir: '重定位',
|
relocateDir: '重定位',
|
||||||
bindDir: '绑定目录',
|
bindDir: '绑定目录',
|
||||||
dirStatusLabel: '目录状态',
|
dirStatusLabel: '目录状态',
|
||||||
|
dirChecking: '检测中…',
|
||||||
|
dirExists: '✓ 目录存在',
|
||||||
|
dirMissing: '⚠ 目录已移走,建议重定位',
|
||||||
|
dirConflict: '该目录已被项目「{name}」绑定',
|
||||||
|
relocateConfirmRelocate: '将项目目录重定位到:\n{dir}\n\n技术栈将重新探测。确认?',
|
||||||
|
relocateConfirmBind: '将项目目录绑定到:\n{dir}\n\n技术栈将重新探测。确认?',
|
||||||
|
relocateFailed: '重定位失败: {msg}',
|
||||||
// 技术栈
|
// 技术栈
|
||||||
techStackLabel: '技术栈',
|
techStackLabel: '技术栈',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,17 @@ export default {
|
|||||||
descLabel: '项目描述',
|
descLabel: '项目描述',
|
||||||
descPlaceholder: '简要描述项目目标',
|
descPlaceholder: '简要描述项目目标',
|
||||||
codeDirLabel: '代码目录',
|
codeDirLabel: '代码目录',
|
||||||
|
codeDirOptionalHint: '(可选,绑定后自动识别技术栈)',
|
||||||
codeDirPlaceholder: '点击右侧选择项目所在目录',
|
codeDirPlaceholder: '点击右侧选择项目所在目录',
|
||||||
selectDir: '选择目录',
|
selectDir: '选择目录',
|
||||||
clearDir: '清除',
|
clearDir: '清除',
|
||||||
confirmCreate: '确认创建',
|
confirmCreate: '确认创建',
|
||||||
|
// AI 扫描
|
||||||
|
aiScanning: 'AI 扫描中…',
|
||||||
|
aiScanFill: '🤖 AI 扫描填信息',
|
||||||
|
aiScanNoDesc: 'AI 未能生成描述,可手动填写',
|
||||||
|
aiScanFailed: 'AI 扫描失败',
|
||||||
|
dirConflict: '该目录已被项目「{name}」绑定',
|
||||||
// 回收站模态框
|
// 回收站模态框
|
||||||
trashTitle: '🗑 回收站',
|
trashTitle: '🗑 回收站',
|
||||||
trashEmpty: '回收站为空',
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认弹层(删除想法,替代原生 window.confirm) -->
|
||||||
|
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,14 +203,31 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import type { IdeaRecord } from '../api/types'
|
import type { IdeaRecord } from '../api/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useProjectStore()
|
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'
|
type FilterKey = 'all' | 'hot' | 'pending' | 'promoted'
|
||||||
|
|
||||||
const activeFilter = ref<FilterKey>('all')
|
const activeFilter = ref<FilterKey>('all')
|
||||||
@@ -375,7 +395,7 @@ async function confirmCapture() {
|
|||||||
|
|
||||||
async function deleteCurrentIdea() {
|
async function deleteCurrentIdea() {
|
||||||
if (!currentIdea.value) return
|
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)
|
await store.deleteIdea(currentIdea.value.id)
|
||||||
selectedId.value = null
|
selectedId.value = null
|
||||||
}
|
}
|
||||||
@@ -388,7 +408,7 @@ async function promoteToProject() {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e?.toString() ?? t('ideas.promoteFailed')
|
const msg = e?.toString() ?? t('ideas.promoteFailed')
|
||||||
console.error(t('ideas.promoteFailed'), e)
|
console.error(t('ideas.promoteFailed'), e)
|
||||||
alert(msg)
|
Message.error(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
<div class="info-item" v-if="currentProject.path">
|
<div class="info-item" v-if="currentProject.path">
|
||||||
<span class="label">{{ $t('projectDetail.dirStatusLabel') }}</span>
|
<span class="label">{{ $t('projectDetail.dirStatusLabel') }}</span>
|
||||||
<span :class="pathExists === false ? 'path-missing' : 'path-ok'">
|
<span :class="pathExists === false ? 'path-missing' : 'path-ok'">
|
||||||
{{ pathExists === null ? '检测中…' : pathExists ? '✓ 目录存在' : '⚠ 目录已移走,建议重定位' }}
|
{{ pathExists === null ? $t('projectDetail.dirChecking') : pathExists ? $t('projectDetail.dirExists') : $t('projectDetail.dirMissing') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,6 +193,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认弹层(删除/重定位确认,替代原生 window.confirm/alert) -->
|
||||||
|
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,11 +203,14 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { projectApi } from '../api'
|
import { projectApi } from '../api'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
|
import { parseStack } from '../utils/project'
|
||||||
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
import { projectStatusLabel, projectStageInfo, taskStatusLabel, taskStatusClass } from '../constants/project'
|
||||||
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import type { WorkflowEventPayload } from '../api/types'
|
import type { WorkflowEventPayload } from '../api/types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -215,6 +221,21 @@ const logListRef = ref<HTMLElement | null>(null)
|
|||||||
const showApprovalDialog = ref(false)
|
const showApprovalDialog = ref(false)
|
||||||
let unlisten: (() => void) | null = null
|
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*) ──
|
// ── 阶段定义(labelKey 对应 i18n projectDetail.stage*) ──
|
||||||
const stages = [
|
const stages = [
|
||||||
{ key: 'idea', labelKey: 'stageIdea' },
|
{ key: 'idea', labelKey: 'stageIdea' },
|
||||||
@@ -317,17 +338,6 @@ async function handleSync() {
|
|||||||
// ── 代码目录绑定 ──
|
// ── 代码目录绑定 ──
|
||||||
const pathExists = ref<boolean | null>(null)
|
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() {
|
async function checkPath() {
|
||||||
const p = currentProject.value
|
const p = currentProject.value
|
||||||
@@ -351,16 +361,19 @@ async function relocateDir() {
|
|||||||
try {
|
try {
|
||||||
const conflict = await projectApi.checkBinding(dir, p.id)
|
const conflict = await projectApi.checkBinding(dir, p.id)
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
alert(`该目录已被项目「${conflict.name}」绑定`)
|
Message.warning(t('projectDetail.dirConflict', { name: conflict.name }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} 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 store.relocateProjectPath(p.id, dir)
|
||||||
await checkPath()
|
await checkPath()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('重定位失败:', e)
|
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() {
|
async function handleDelete() {
|
||||||
const p = currentProject.value
|
const p = currentProject.value
|
||||||
if (!p) return
|
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)
|
await store.deleteProject(p.id)
|
||||||
router.push('/projects')
|
router.push('/projects')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<textarea v-model="newDesc" :placeholder="$t('projects.descPlaceholder')" rows="3"></textarea>
|
<textarea v-model="newDesc" :placeholder="$t('projects.descPlaceholder')" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-field">
|
<div class="modal-field">
|
||||||
<label>{{ $t('projects.codeDirLabel') }}(可选,绑定后自动识别技术栈)</label>
|
<label>{{ $t('projects.codeDirLabel') }}{{ $t('projects.codeDirOptionalHint') }}</label>
|
||||||
<div class="dir-row">
|
<div class="dir-row">
|
||||||
<input v-model="newPath" :placeholder="$t('projects.codeDirPlaceholder')" readonly />
|
<input v-model="newPath" :placeholder="$t('projects.codeDirPlaceholder')" readonly />
|
||||||
<button class="btn btn-ghost btn-sm" type="button" @click="pickDir">{{ $t('projects.selectDir') }}</button>
|
<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="pathWarning" class="path-warning">⚠ {{ pathWarning }}</div>
|
||||||
<div v-if="newPath" class="scan-row">
|
<div v-if="newPath" class="scan-row">
|
||||||
<button class="btn btn-ghost btn-sm" type="button" @click="aiScan" :disabled="scanning">
|
<button class="btn btn-ghost btn-sm" type="button" @click="aiScan" :disabled="scanning">
|
||||||
{{ scanning ? 'AI 扫描中…' : '🤖 AI 扫描填信息' }}
|
{{ scanning ? $t('projects.aiScanning') : $t('projects.aiScanFill') }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="scanProjectType" class="tech-tag">{{ scanProjectType }}</span>
|
<span v-if="scanProjectType" class="tech-tag">{{ scanProjectType }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +107,9 @@
|
|||||||
<div v-if="!store.loading && store.projects.length === 0" class="empty-state">
|
<div v-if="!store.loading && store.projects.length === 0" class="empty-state">
|
||||||
{{ $t('projects.empty') }}
|
{{ $t('projects.empty') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认弹层(删除/彻底删除,替代原生 window.confirm) -->
|
||||||
|
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" @result="answerConfirm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -118,7 +121,9 @@ import { open } from '@tauri-apps/plugin-dialog'
|
|||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { projectApi } from '../api'
|
import { projectApi } from '../api'
|
||||||
import { formatDate } from '../utils/time'
|
import { formatDate } from '../utils/time'
|
||||||
|
import { parseStack } from '../utils/project'
|
||||||
import { projectStatusLabel as statusLabel, projectBadgeClass as stageClass } from '../constants/project'
|
import { projectStatusLabel as statusLabel, projectBadgeClass as stageClass } from '../constants/project'
|
||||||
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||||
import type { ProjectRecord } from '../api/types'
|
import type { ProjectRecord } from '../api/types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -128,6 +133,21 @@ const { t } = useI18n()
|
|||||||
// 状态映射统一走 ../constants/project(与 ProjectDetail/Tasks/Dashboard 一致)
|
// 状态映射统一走 ../constants/project(与 ProjectDetail/Tasks/Dashboard 一致)
|
||||||
// formatDate 由 ../utils/time 提供(统一毫秒字符串解析,根治 Invalid Date)
|
// 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 showCreateModal = ref(false)
|
||||||
const newName = ref('')
|
const newName = ref('')
|
||||||
@@ -139,17 +159,6 @@ const scanning = ref(false)
|
|||||||
const scanError = ref('')
|
const scanError = ref('')
|
||||||
const scanProjectType = 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() {
|
async function pickDir() {
|
||||||
try {
|
try {
|
||||||
@@ -163,7 +172,7 @@ async function pickDir() {
|
|||||||
// 查重(已被其他项目绑定则警告,禁止创建)
|
// 查重(已被其他项目绑定则警告,禁止创建)
|
||||||
try {
|
try {
|
||||||
const conflict = await projectApi.checkBinding(selected as string)
|
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 { /* ignore */ }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('选择目录失败:', e)
|
console.error('选择目录失败:', e)
|
||||||
@@ -190,9 +199,9 @@ async function aiScan() {
|
|||||||
detectedStack.value = result.stack
|
detectedStack.value = result.stack
|
||||||
if (result.description) newDesc.value = result.description
|
if (result.description) newDesc.value = result.description
|
||||||
scanProjectType.value = result.project_type ?? ''
|
scanProjectType.value = result.project_type ?? ''
|
||||||
if (!result.description) scanError.value = 'AI 未能生成描述,可手动填写'
|
if (!result.description) scanError.value = t('projects.aiScanNoDesc')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
scanError.value = e?.toString() ?? 'AI 扫描失败'
|
scanError.value = e?.toString() ?? t('projects.aiScanFailed')
|
||||||
} finally {
|
} finally {
|
||||||
scanning.value = false
|
scanning.value = false
|
||||||
}
|
}
|
||||||
@@ -214,7 +223,7 @@ async function submitCreate() {
|
|||||||
const showTrashModal = ref(false)
|
const showTrashModal = ref(false)
|
||||||
|
|
||||||
async function handleDelete(project: ProjectRecord) {
|
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)
|
await store.deleteProject(project.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +237,7 @@ async function handleRestore(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handlePurge(project: ProjectRecord) {
|
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)
|
await store.purgeProject(project.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user