顶栏不再挤在一起。 返回、项目名/路径、右侧按钮各占各的位置:路径会吃掉中间空档,标签收成一排小分段,不再拉成整条高亮条。
每个项目的排除规则有入口了。 详情右上角有「排除规则」(有规则时带数量);项目列表右键同样可以打开同一个弹窗。规则只对当前项目生效,和全局规则叠加,改完后重新分析才会算进去。
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ArrowLeft, Code2, GitCommitHorizontal, FolderTree, RefreshCw, Files, MessageSquareText, Rows3, Users, Plus, Minus, HardDrive, Folder, FileWarning, GitBranch, ExternalLink, TriangleAlert, ClipboardCheck, Flame, Sparkles, MessageCircleQuestion, Rocket, ListFilter, Trash2 } from 'lucide-vue-next'
|
||||
import { ArrowLeft, Code2, GitCommitHorizontal, FolderTree, RefreshCw, Files, MessageSquareText, Rows3, Users, Plus, Minus, HardDrive, Folder, FileWarning, GitBranch, ExternalLink, TriangleAlert, ClipboardCheck, Flame, Sparkles, MessageCircleQuestion, Rocket, ListFilter } from 'lucide-vue-next'
|
||||
import StatCard from '../components/StatCard.vue'
|
||||
import ChartView from '../components/ChartView.vue'
|
||||
import GitHeatmap from '../components/GitHeatmap.vue'
|
||||
@@ -11,6 +11,7 @@ import CommitDrawer from '../components/CommitDrawer.vue'
|
||||
import FilePreviewModal from '../components/FilePreviewModal.vue'
|
||||
import AIBrief from '../components/AIBrief.vue'
|
||||
import ProjectAIDrawer from '../components/ProjectAIDrawer.vue'
|
||||
import ProjectRulesModal from '../components/ProjectRulesModal.vue'
|
||||
import { call } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
@@ -71,36 +72,15 @@ async function load() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// ---- 项目专属排除规则:与全局规则叠加,仅本项目生效;随项目身份上云,认领后自动带回 ----
|
||||
// ---- 项目专属排除规则:顶栏 / 列表右键打开同一弹窗 ----
|
||||
const projRules = ref([])
|
||||
const rulePattern = ref('')
|
||||
const ruleBusy = ref(false)
|
||||
const rulesOpen = ref(false)
|
||||
|
||||
async function loadProjRules() {
|
||||
try { projRules.value = (await call('GetProjectRules', +route.params.id)) || [] } catch { projRules.value = [] }
|
||||
}
|
||||
async function addProjRule() {
|
||||
const v = rulePattern.value.trim()
|
||||
if (!v || ruleBusy.value) return
|
||||
ruleBusy.value = true
|
||||
try {
|
||||
await call('AddProjectRule', +route.params.id, v, 'custom')
|
||||
rulePattern.value = ''
|
||||
await loadProjRules()
|
||||
store.showToast({ type: 'success', key: 'projRuleAddedToast' })
|
||||
} catch (e) {
|
||||
store.showToast({ type: 'error', text: String(e) })
|
||||
} finally {
|
||||
ruleBusy.value = false
|
||||
}
|
||||
}
|
||||
async function removeProjRule(r) {
|
||||
try {
|
||||
await call('DeleteRule', r.id)
|
||||
await loadProjRules()
|
||||
} catch (e) {
|
||||
store.showToast({ type: 'error', text: String(e) })
|
||||
}
|
||||
function onRulesChanged(list) {
|
||||
projRules.value = list || []
|
||||
}
|
||||
async function refreshInsights() {
|
||||
insightLoading.value = true
|
||||
@@ -162,18 +142,27 @@ onMounted(load)
|
||||
<div class="page detail-page">
|
||||
<header class="project-head sticky-head detail-sticky-head">
|
||||
<div class="detail-head-row">
|
||||
<button class="back" @click="router.push('/projects')"><ArrowLeft />{{ t('back') }}</button>
|
||||
<div><h1>{{ p.name }}</h1><p>{{ p.path }}</p></div>
|
||||
<button class="btn secondary" @click="router.push({ path: '/launchpad', query: { projectId: p.id } })"><Rocket />{{ t('lpToLaunchpad') }}</button>
|
||||
<button class="btn secondary ai-entry" @click="openChat()"><Sparkles />{{ t('aiAskGo') }}</button>
|
||||
</div>
|
||||
<div class="tabs detail-tabs">
|
||||
<button :class="{ active: tab === 'code' }" @click="tab = 'code'"><Code2 />{{ t('code') }}</button>
|
||||
<button :class="{ active: tab === 'git' }" @click="tab = 'git'"><GitCommitHorizontal />{{ t('git') }}</button>
|
||||
<button :class="{ active: tab === 'structure' }" @click="tab = 'structure'"><FolderTree />{{ t('structure') }}</button>
|
||||
<button :class="{ active: tab === 'insights' }" @click="tab = 'insights'"><ClipboardCheck />{{ t('insights') }}</button>
|
||||
<button :class="{ active: tab === 'ai' }" @click="tab = 'ai'"><Sparkles />{{ t('aiAnalysis') }}</button>
|
||||
<button type="button" class="back" @click="router.push('/projects')"><ArrowLeft />{{ t('back') }}</button>
|
||||
<div class="detail-head-id">
|
||||
<h1>{{ p.name }}</h1>
|
||||
<p :title="p.path">{{ p.path }}</p>
|
||||
</div>
|
||||
<div class="detail-head-acts">
|
||||
<button type="button" class="btn secondary" @click="rulesOpen = true">
|
||||
<ListFilter />{{ t('tabRules') }}
|
||||
<em v-if="projRules.length" class="detail-rules-badge">{{ projRules.length }}</em>
|
||||
</button>
|
||||
<button type="button" class="btn secondary" @click="router.push({ path: '/launchpad', query: { projectId: p.id } })"><Rocket />{{ t('lpToLaunchpad') }}</button>
|
||||
<button type="button" class="btn secondary" @click="openChat()"><Sparkles />{{ t('aiAskGo') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="tabs detail-tabs">
|
||||
<button type="button" :class="{ active: tab === 'code' }" @click="tab = 'code'"><Code2 />{{ t('code') }}</button>
|
||||
<button type="button" :class="{ active: tab === 'git' }" @click="tab = 'git'"><GitCommitHorizontal />{{ t('git') }}</button>
|
||||
<button type="button" :class="{ active: tab === 'structure' }" @click="tab = 'structure'"><FolderTree />{{ t('structure') }}</button>
|
||||
<button type="button" :class="{ active: tab === 'insights' }" @click="tab = 'insights'"><ClipboardCheck />{{ t('insights') }}</button>
|
||||
<button type="button" :class="{ active: tab === 'ai' }" @click="tab = 'ai'"><Sparkles />{{ t('aiAnalysis') }}</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<template v-if="tab === 'code'">
|
||||
@@ -188,17 +177,6 @@ onMounted(load)
|
||||
<section class="panel shine-card"><h2>{{ t('languageDistribution') }}</h2><ChartView v-if="p.languages.length" :option="pie" /><div v-else class="empty">{{ t('empty') }}</div></section>
|
||||
<section class="panel shine-card"><h2>{{ t('languageDetails') }}</h2><div class="language-list"><div v-for="(x, i) in p.languages" :key="x.name"><b><i :style="{ background: colors[i % colors.length] }" />{{ x.name }}</b><span>{{ x.files }} {{ t('files') }}</span><strong>{{ fmt(x.code) }} {{ t('lines') }}</strong></div></div></section>
|
||||
</div>
|
||||
<section class="panel proj-rules-panel">
|
||||
<h2><ListFilter class="panel-icon" />{{ t('projRulesTitle') }}<small>{{ t('projRulesHint') }}</small></h2>
|
||||
<div class="proj-rules-add">
|
||||
<input v-model="rulePattern" :placeholder="t('rulePatternPh')" :disabled="ruleBusy" @keyup.enter="addProjRule" />
|
||||
<button class="btn secondary" :disabled="ruleBusy || !rulePattern.trim()" @click="addProjRule"><Plus />{{ t('add') }}</button>
|
||||
</div>
|
||||
<div class="proj-rules-chips">
|
||||
<button v-for="r in projRules" :key="r.id" type="button" class="proj-rule-chip" :title="t('delete')" @click="removeProjRule(r)">{{ r.pattern }}<Trash2 /></button>
|
||||
<span v-if="!projRules.length" class="proj-rules-empty">{{ t('projRulesEmpty') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab === 'git'">
|
||||
@@ -302,5 +280,12 @@ onMounted(load)
|
||||
<div class="center-action"><button class="btn secondary" @click="analyze()"><RefreshCw />{{ t('analyze') }}</button></div>
|
||||
<FilePreviewModal v-if="preview" :project-id="+route.params.id" :path="preview" :line="previewLine" @close="preview = ''; previewLine = 0" />
|
||||
<ProjectAIDrawer v-if="aiDrawer" :project-id="p.id" :project-name="p.name" :ask="aiAsk" @close="aiDrawer = false; aiAsk = ''" />
|
||||
<ProjectRulesModal
|
||||
:open="rulesOpen"
|
||||
:project-id="p.id"
|
||||
:project-name="p.name"
|
||||
@close="rulesOpen = false"
|
||||
@changed="onRulesChanged"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user