顶栏不再挤在一起。 返回、项目名/路径、右侧按钮各占各的位置:路径会吃掉中间空档,标签收成一排小分段,不再拉成整条高亮条。
每个项目的排除规则有入口了。 详情右上角有「排除规则」(有规则时带数量);项目列表右键同样可以打开同一个弹窗。规则只对当前项目生效,和全局规则叠加,改完后重新分析才会算进去。
This commit is contained in:
82
frontend/src/components/ProjectRulesModal.vue
Normal file
82
frontend/src/components/ProjectRulesModal.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ListFilter, Plus, Trash2, X } from 'lucide-vue-next'
|
||||
import { call } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
const props = defineProps({
|
||||
open: Boolean,
|
||||
projectId: { type: [Number, String], default: 0 },
|
||||
projectName: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
const { t } = useI18n()
|
||||
const store = useAppStore()
|
||||
const rules = ref([])
|
||||
const pattern = ref('')
|
||||
const busy = ref(false)
|
||||
|
||||
async function load() {
|
||||
const id = +props.projectId
|
||||
if (!id) {
|
||||
rules.value = []
|
||||
return
|
||||
}
|
||||
try { rules.value = (await call('GetProjectRules', id)) || [] } catch { rules.value = [] }
|
||||
emit('changed', rules.value)
|
||||
}
|
||||
|
||||
watch(() => [props.open, props.projectId], ([open]) => {
|
||||
if (!open) return
|
||||
pattern.value = ''
|
||||
load()
|
||||
}, { immediate: true })
|
||||
|
||||
async function add() {
|
||||
const v = pattern.value.trim()
|
||||
const id = +props.projectId
|
||||
if (!v || !id || busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
await call('AddProjectRule', id, v, 'custom')
|
||||
pattern.value = ''
|
||||
await load()
|
||||
store.showToast({ type: 'success', key: 'projRuleAddedToast' })
|
||||
} catch (e) {
|
||||
store.showToast({ type: 'error', text: String(e) })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
async function remove(r) {
|
||||
try {
|
||||
await call('DeleteRule', r.id)
|
||||
await load()
|
||||
} catch (e) {
|
||||
store.showToast({ type: 'error', text: String(e) })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="overlay" @click.self="emit('close')">
|
||||
<section class="modal proj-rules-modal" @click.stop>
|
||||
<header class="modal-head">
|
||||
<h2><ListFilter />{{ t('projRulesTitle') }}<small v-if="projectName">{{ projectName }}</small></h2>
|
||||
<button type="button" class="nm-close" :title="t('close')" @click="emit('close')"><X /></button>
|
||||
</header>
|
||||
<p class="proj-rules-hint">{{ t('projRulesHint') }}</p>
|
||||
<div class="proj-rules-add">
|
||||
<input v-model="pattern" :placeholder="t('rulePatternPh')" :disabled="busy" @keyup.enter="add" />
|
||||
<button type="button" class="btn secondary" :disabled="busy || !pattern.trim()" @click="add"><Plus />{{ t('add') }}</button>
|
||||
</div>
|
||||
<div class="proj-rules-chips">
|
||||
<button v-for="r in rules" :key="r.id" type="button" class="proj-rule-chip" :title="t('delete')" @click="remove(r)">{{ r.pattern }}<Trash2 /></button>
|
||||
<span v-if="!rules.length" class="proj-rules-empty">{{ t('projRulesEmpty') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -308,13 +308,19 @@ html[data-theme=light] .lg-art{background:#141a2e}
|
||||
.toast{transition:opacity .32s ease,transform .36s ease,filter .32s ease}
|
||||
.toast.muted{opacity:.72;filter:saturate(.82)}
|
||||
.toast.leaving{opacity:0;transform:translate(18px,-12px);pointer-events:none}
|
||||
.detail-sticky-head{display:grid;gap:16px;align-items:stretch}
|
||||
.detail-sticky-head{display:grid;gap:12px;align-items:stretch}
|
||||
.detail-page{--detail-sticky-offset:16px;max-width:none;margin:0;padding:var(--detail-sticky-offset) 30px 60px}
|
||||
.detail-page .detail-sticky-head{position:sticky;top:calc(var(--topbar-h) + var(--detail-sticky-offset));width:100%;z-index:24;margin:0 0 24px;border-radius:8px;border-top:1px solid rgba(255,255,255,.08);background:color-mix(in srgb,var(--surface) 88%,transparent);box-shadow:0 18px 42px rgba(0,0,0,.34),0 1px 0 rgba(255,255,255,.06)}
|
||||
.detail-head-row{display:flex;align-items:center;gap:28px;min-width:0}
|
||||
.detail-head-row>div{min-width:0}
|
||||
.detail-head-row p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.detail-tabs{margin:0;background:rgba(255,255,255,.045)}
|
||||
.detail-page .detail-sticky-head{display:grid;position:sticky;top:calc(var(--topbar-h) + var(--detail-sticky-offset));width:100%;z-index:24;margin:0 0 20px;padding:14px 16px 12px;border-radius:10px;border-top:1px solid rgba(255,255,255,.08);background:color-mix(in srgb,var(--surface) 88%,transparent);box-shadow:0 18px 42px rgba(0,0,0,.34),0 1px 0 rgba(255,255,255,.06)}
|
||||
.detail-head-row{display:flex;align-items:center;gap:12px;min-width:0}
|
||||
.detail-head-row .back{flex:none;height:36px;padding:0 8px 0 4px;white-space:nowrap;align-self:center}
|
||||
.detail-head-id{flex:1;min-width:0}
|
||||
.detail-head-id h1{margin:0;font-size:20px;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.detail-head-id p{margin:3px 0 0;font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.detail-head-acts{display:flex;align-items:center;gap:8px;flex:none}
|
||||
.detail-head-acts .btn{height:34px;padding:0 12px;font-size:12.5px}
|
||||
.detail-rules-badge{margin-left:2px;min-width:16px;height:16px;padding:0 5px;border-radius:999px;display:inline-grid;place-items:center;font-style:normal;font-size:10px;font-weight:800;line-height:1;color:#fff;background:linear-gradient(135deg,#7b73ff,#5b52d6)}
|
||||
.detail-tabs{justify-self:start;width:max-content;max-width:100%;margin:0;padding:3px;background:rgba(255,255,255,.045);box-sizing:border-box}
|
||||
.detail-tabs button{height:32px;padding:0 12px;font-size:12.5px;flex:none}
|
||||
.heat-panel{height:auto;min-height:250px}
|
||||
.heat-grid-wrap{--cell:13px;--gap:4px;position:relative;margin-top:12px;overflow-x:auto;padding-bottom:8px}
|
||||
.heat-months{display:grid;grid-template-columns:repeat(var(--weeks),var(--cell));gap:var(--gap);margin-left:34px;margin-bottom:8px;min-width:calc(var(--weeks) * (var(--cell) + var(--gap)))}
|
||||
@@ -901,7 +907,7 @@ html[data-theme=light] .heat-cell.l1{background:#b9efd4}html[data-theme=light] .
|
||||
@media(max-width:1150px){main::before,main::after{left:72px}.sidebar-bottom{padding-left:0;padding-right:0;justify-content:center}.quick-settings{left:0}.quick-settings-btn{width:40px;height:40px}.stats-grid.three,.stats-grid.four{grid-template-columns:repeat(2,minmax(0,1fr))}.project-card{padding:20px}}
|
||||
@media(max-width:980px){body{min-width:0}.page{padding:24px 24px 56px}.stats-grid.three,.stats-grid.four,.stats-grid.five{grid-template-columns:repeat(2,minmax(0,1fr))}.project-grid{grid-template-columns:1fr}.section-head{align-items:flex-start;gap:12px;flex-direction:column}.project-tools{width:100%;justify-content:flex-start}.group-filter{width:100%;flex-wrap:wrap;height:auto}.group-filter select{flex:1;min-width:160px}.search{width:100%}.git-error-panel{grid-template-columns:36px 1fr}.git-error-panel .btn{grid-column:1/3;width:max-content}}
|
||||
@media(max-width:720px){.loading-style-grid{grid-template-columns:1fr}.loading-style-card{min-height:104px}}
|
||||
@media(max-width:980px){.detail-page{--detail-sticky-offset:12px;padding:var(--detail-sticky-offset) 20px 56px}.insights-hero{grid-template-columns:1fr}.detail-head-row{align-items:flex-start;flex-direction:column;gap:10px}.detail-tabs{width:100%;overflow-x:auto}}
|
||||
@media(max-width:980px){.detail-page{--detail-sticky-offset:12px;padding:var(--detail-sticky-offset) 20px 56px}.insights-hero{grid-template-columns:1fr}.detail-head-row{align-items:flex-start;flex-wrap:wrap;gap:10px}.detail-head-id{flex:1 1 220px}.detail-head-acts{width:100%;flex-wrap:wrap;justify-content:flex-start}.detail-tabs{width:100%;max-width:100%;overflow-x:auto}}
|
||||
@media(prefers-reduced-motion:reduce){main::before,main::after,.logo-track,.logo-spark,.shine-card:hover::after{animation:none!important}.shine-card:hover,.heat-cell:hover{transform:none}.toast{transition:opacity .2s ease}}
|
||||
|
||||
/* ---------- 日历节日背景:默认整卡插画(eggart.js 生成),管理员可换成真实照片 ---------- */
|
||||
@@ -1795,11 +1801,20 @@ html[data-theme=light] .lp-log-line{color:#1f2937}
|
||||
.cp-main b{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.cp-main small{color:var(--muted);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.cloud-pending-item .btn{height:30px;padding:0 11px;font-size:12px}
|
||||
/* 项目详情页:项目专属排除规则 */
|
||||
.proj-rules-panel h2{display:flex;align-items:center;gap:8px}
|
||||
.proj-rules-panel h2 small{color:var(--muted);font-weight:400;font-size:12px}
|
||||
.proj-rules-add{display:flex;gap:9px;margin:4px 0 12px}
|
||||
.proj-rules-add input{flex:1;max-width:420px;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--border);background:var(--surface-2);color:var(--text);font:inherit;font-size:12.5px}
|
||||
/* 项目专属排除规则:详情顶栏 / 列表右键共用弹窗 */
|
||||
.proj-rules-modal{width:min(480px,92vw);max-height:min(560px,80vh);padding:0;overflow:hidden;display:flex;flex-direction:column}
|
||||
.proj-rules-modal .modal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 18px 8px;border:0}
|
||||
.proj-rules-modal .modal-head h2{display:flex;align-items:center;gap:8px;margin:0;font-size:15px;min-width:0}
|
||||
.proj-rules-modal .modal-head h2 svg{width:16px;height:16px;flex:none;color:var(--primary)}
|
||||
.proj-rules-modal .modal-head h2 small{color:var(--muted);font-weight:400;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.proj-rules-modal .nm-close{display:grid;place-items:center;width:32px;height:32px;border:0;border-radius:8px;background:transparent;color:var(--muted);cursor:pointer;flex:none}
|
||||
.proj-rules-modal .nm-close:hover{background:var(--surface-3);color:var(--text)}
|
||||
.proj-rules-modal .nm-close svg{width:16px;height:16px}
|
||||
.proj-rules-hint{margin:0;padding:0 18px 12px;font-size:12.5px;color:var(--muted);line-height:1.45}
|
||||
.proj-rules-modal .proj-rules-add{margin:0 18px 12px}
|
||||
.proj-rules-modal .proj-rules-chips{padding:0 18px 18px;overflow:auto;min-height:0}
|
||||
.proj-rules-add{display:flex;gap:9px}
|
||||
.proj-rules-add input{flex:1;min-width:0;width:auto;margin-top:0;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--border);background:var(--surface-2);color:var(--text);font:inherit;font-size:12.5px}
|
||||
.proj-rules-add input:focus{outline:none;border-color:var(--primary)}
|
||||
.proj-rules-chips{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.proj-rule-chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:999px;border:1px solid var(--border);background:var(--surface-2);color:var(--text);font:inherit;font-size:12.5px;cursor:pointer;transition:border-color .15s,color .15s}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Folder, Code2, GitCommitHorizontal, Plus, RefreshCw, Search, Trash2, Pencil, FolderOpen, PieChart, X, CheckCircle2, CircleX, Ban, Star, CloudDownload, ChevronUp, ChevronDown, Rocket, Image, Eye, Package, ChevronRight } from 'lucide-vue-next'
|
||||
import { Folder, Code2, GitCommitHorizontal, Plus, RefreshCw, Search, Trash2, Pencil, FolderOpen, PieChart, X, CheckCircle2, CircleX, Ban, Star, CloudDownload, ChevronUp, ChevronDown, Rocket, Image, Eye, Package, ChevronRight, ListFilter } from 'lucide-vue-next'
|
||||
import StatCard from '../components/StatCard.vue'
|
||||
import ChartView from '../components/ChartView.vue'
|
||||
import PackCmdsModal from '../components/PackCmdsModal.vue'
|
||||
import ProjectRulesModal from '../components/ProjectRulesModal.vue'
|
||||
import { useAppStore } from '../store'
|
||||
import { call, on } from '../api'
|
||||
import { getPackCmds } from '../packCmds'
|
||||
@@ -37,6 +38,7 @@ const flyEl = ref(null)
|
||||
const flyStyle = ref({ left: '0px', top: '0px' })
|
||||
const packModal = ref({ open: false, id: 0, title: '', dir: '' })
|
||||
const packTick = ref(0)
|
||||
const rulesModal = ref({ open: false, id: 0, name: '' })
|
||||
let subLeaveTimer = 0
|
||||
|
||||
const filtered = computed(() => {
|
||||
@@ -152,6 +154,11 @@ function openPackConfig(p) {
|
||||
if (!p?.id) return
|
||||
packModal.value = { open: true, id: p.id, title: t('packCmdsTitleNamed', { name: p.name }), dir: p.path || '' }
|
||||
}
|
||||
function openProjRules(p) {
|
||||
closeCtx()
|
||||
if (!p?.id) return
|
||||
rulesModal.value = { open: true, id: p.id, name: p.name || '' }
|
||||
}
|
||||
async function runPackCmd(p, cmd) {
|
||||
closeCtx()
|
||||
if (!p?.id || !cmd?.cmd) return
|
||||
@@ -514,6 +521,7 @@ watch(() => store.projects.length, () => loadKindFallback())
|
||||
<button type="button" @mouseenter="ctx.sub = ''" @click="fetchProjectIcon(ctx.project)"><Image />{{ t('lpFetchIcon') }}</button>
|
||||
<button type="button" @mouseenter="ctx.sub = ''" @click="refreshFromCtx(ctx.project)"><RefreshCw />{{ t('refreshProject') }}</button>
|
||||
<button type="button" @mouseenter="ctx.sub = ''" @click="editProject(ctx.project)"><Pencil />{{ t('edit') }}</button>
|
||||
<button type="button" @mouseenter="ctx.sub = ''" @click="openProjRules(ctx.project)"><ListFilter />{{ t('tabRules') }}</button>
|
||||
<button type="button" class="danger" @mouseenter="ctx.sub = ''" @click="deleteProject(ctx.project)"><Trash2 />{{ t('delete') }}</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -537,6 +545,12 @@ watch(() => store.projects.length, () => loadKindFallback())
|
||||
@close="packModal.open = false"
|
||||
@saved="packTick++"
|
||||
/>
|
||||
<ProjectRulesModal
|
||||
:open="rulesModal.open"
|
||||
:project-id="rulesModal.id"
|
||||
:project-name="rulesModal.name"
|
||||
@close="rulesModal.open = false"
|
||||
/>
|
||||
</Teleport>
|
||||
<Teleport to="body">
|
||||
<div v-if="modal" class="overlay" @click.self="!saving && (modal = false)">
|
||||
|
||||
@@ -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