mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): PR-8 frontend protection badges + delete-button gating
This commit is contained in:
parent
1e62dbad47
commit
4ed88ebbf9
@ -1340,6 +1340,10 @@ export default {
|
||||
loadMore: 'Load {n} more',
|
||||
filteredByRaw: 'Filtered by source material',
|
||||
clearFilter: 'Clear filter',
|
||||
systemPageBadge: 'System',
|
||||
systemPageHint: 'overview / log are built-in system pages — they can\'t be deleted',
|
||||
lockedPageBadge: 'Locked',
|
||||
lockedPageHint: 'This page is locked — AI tools and batch operations skip it',
|
||||
pageTypes: {
|
||||
concept: 'Concepts',
|
||||
person: 'People',
|
||||
|
||||
@ -1350,6 +1350,10 @@ export default {
|
||||
loadMore: '加载更多 {n} 条',
|
||||
filteredByRaw: '按原始材料过滤',
|
||||
clearFilter: '清除过滤',
|
||||
systemPageBadge: '系统页',
|
||||
systemPageHint: 'overview / log 是知识库自带的系统页,无法删除',
|
||||
lockedPageBadge: '已锁定',
|
||||
lockedPageHint: '该页被手动锁定,AI 工具与批量操作不会修改或删除',
|
||||
pageTypes: {
|
||||
concept: '概念',
|
||||
person: '人物',
|
||||
|
||||
@ -46,10 +46,22 @@ export interface WikiPage {
|
||||
version: number
|
||||
lastUpdatedBy: string
|
||||
pageType?: string | null
|
||||
// RFC-051 PR-2: locked=1 blocks AI/tool/UI deletion (combined with pageType=system
|
||||
// for the built-in overview/log pages, but users can lock any page).
|
||||
locked?: number | null
|
||||
// RFC-051 PR-7: archived=1 hides the page from default list/search/related.
|
||||
archived?: number | null
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
/** RFC-051 PR-8: shared protection check used by viewer + list to gate delete UI. */
|
||||
export function isProtectedPage(page: WikiPage | null | undefined): boolean {
|
||||
if (!page) return false
|
||||
if (page.pageType === 'system') return true
|
||||
return page.locked === 1
|
||||
}
|
||||
|
||||
export const useWikiStore = defineStore('wiki', () => {
|
||||
const knowledgeBases = ref<WikiKB[]>([])
|
||||
const currentKB = ref<WikiKB | null>(null)
|
||||
|
||||
@ -29,9 +29,20 @@
|
||||
<el-icon><SetUp /></el-icon>
|
||||
{{ t('wiki.page.repair') }}
|
||||
</button>
|
||||
<button v-if="!editing" class="btn-secondary btn-sm btn-delete" @click="handleDelete">
|
||||
<!-- RFC-051 PR-8: hide delete on protected pages (system / locked). -->
|
||||
<button
|
||||
v-if="!editing && !isProtected"
|
||||
class="btn-secondary btn-sm btn-delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
<span v-if="!editing && isSystem" class="system-badge" :title="t('wiki.systemPageHint')">
|
||||
{{ t('wiki.systemPageBadge') }}
|
||||
</span>
|
||||
<span v-if="!editing && isLockedNotSystem" class="locked-badge" :title="t('wiki.lockedPageHint')">
|
||||
{{ t('wiki.lockedPageBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
@ -79,7 +90,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWikiStore, type WikiPage } from '@/stores/useWikiStore'
|
||||
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
|
||||
import { wikiApi } from '@/api/index'
|
||||
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||
import { Link, SetUp } from '@element-plus/icons-vue'
|
||||
@ -97,6 +108,11 @@ const backlinks = ref<WikiPage[]>([])
|
||||
const citationDrawerOpen = ref(false)
|
||||
const enrichToast = ref('')
|
||||
|
||||
// RFC-051 PR-8: protection state for delete-button gating + badge rendering.
|
||||
const isSystem = computed(() => store.currentPage?.pageType === 'system')
|
||||
const isProtected = computed(() => isProtectedPage(store.currentPage))
|
||||
const isLockedNotSystem = computed(() => isProtected.value && !isSystem.value)
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!store.currentPage?.content) return ''
|
||||
// Build a lookup map: title (normalized) → slug, for resolving [[Title]] links
|
||||
@ -208,7 +224,30 @@ onMounted(() => {
|
||||
.btn-secondary.btn-delete:hover { background: var(--el-color-danger-light-9, #fef0f0); border-color: var(--el-color-danger-light-5, #fab6b6); }
|
||||
.btn-secondary.btn-action { color: var(--mc-primary); }
|
||||
|
||||
.page-actions-bar { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.page-actions-bar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
|
||||
/* RFC-051 PR-8: protection badges shown next to action buttons. */
|
||||
.system-badge,
|
||||
.locked-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 9px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
user-select: none;
|
||||
}
|
||||
.system-badge {
|
||||
background: var(--mc-primary-bg);
|
||||
color: var(--mc-primary);
|
||||
border: 1px solid var(--mc-primary);
|
||||
}
|
||||
.locked-badge {
|
||||
background: var(--mc-bg-elevated);
|
||||
color: var(--mc-text-secondary);
|
||||
border: 1px solid var(--mc-border-light);
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.page-summary { padding: 16px 20px; background: var(--mc-bg-muted); border-radius: 12px; border-left: 3px solid var(--mc-primary); }
|
||||
|
||||
@ -121,20 +121,29 @@
|
||||
<div v-if="!collapsedGroups.has(group.type)" class="group-items">
|
||||
<div
|
||||
v-for="page in paginatedGroupPages(group)" :key="page.slug"
|
||||
class="page-item" :class="{ active: !batchMode && store.currentPage?.slug === page.slug }"
|
||||
class="page-item"
|
||||
:class="{
|
||||
active: !batchMode && store.currentPage?.slug === page.slug,
|
||||
'page-item--system': page.pageType === 'system'
|
||||
}"
|
||||
@click="batchMode ? toggleSelect(page.slug) : openPage(page.slug)"
|
||||
>
|
||||
<!-- RFC-051 PR-8: batch checkbox is disabled for protected pages so a stray
|
||||
batch-delete can't drag in overview/log or any locked page. -->
|
||||
<input
|
||||
v-if="batchMode"
|
||||
type="checkbox"
|
||||
:checked="selectedSlugs.includes(page.slug)"
|
||||
:disabled="isProtectedPage(page)"
|
||||
class="page-checkbox"
|
||||
@click.stop="toggleSelect(page.slug)"
|
||||
@click.stop="!isProtectedPage(page) && toggleSelect(page.slug)"
|
||||
/>
|
||||
<div class="page-item-body">
|
||||
<div class="page-item-title">{{ page.title }}</div>
|
||||
<div class="page-item-meta">
|
||||
<span v-if="page.lastUpdatedBy === 'manual'" class="edit-dot manual" title="Manual edit"></span>
|
||||
<span v-if="page.pageType === 'system'" class="page-flag page-flag--system">{{ t('wiki.systemPageBadge') }}</span>
|
||||
<span v-else-if="page.locked === 1" class="page-flag page-flag--locked">{{ t('wiki.lockedPageBadge') }}</span>
|
||||
<span class="meta-text">v{{ page.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,20 +165,27 @@
|
||||
<div class="page-list" v-else>
|
||||
<div
|
||||
v-for="page in paginatedSearch" :key="page.slug"
|
||||
class="page-item" :class="{ active: !batchMode && store.currentPage?.slug === page.slug }"
|
||||
class="page-item"
|
||||
:class="{
|
||||
active: !batchMode && store.currentPage?.slug === page.slug,
|
||||
'page-item--system': page.pageType === 'system'
|
||||
}"
|
||||
@click="batchMode ? toggleSelect(page.slug) : openPage(page.slug)"
|
||||
>
|
||||
<input
|
||||
v-if="batchMode"
|
||||
type="checkbox"
|
||||
:checked="selectedSlugs.includes(page.slug)"
|
||||
:disabled="isProtectedPage(page)"
|
||||
class="page-checkbox"
|
||||
@click.stop="toggleSelect(page.slug)"
|
||||
@click.stop="!isProtectedPage(page) && toggleSelect(page.slug)"
|
||||
/>
|
||||
<div class="page-item-body">
|
||||
<div class="page-item-title">{{ page.title }}</div>
|
||||
<div class="page-item-meta">
|
||||
<span class="type-chip">{{ formatGroupLabel(page.pageType || 'other') }}</span>
|
||||
<span v-if="page.pageType === 'system'" class="page-flag page-flag--system">{{ t('wiki.systemPageBadge') }}</span>
|
||||
<span v-else-if="page.locked === 1" class="page-flag page-flag--locked">{{ t('wiki.lockedPageBadge') }}</span>
|
||||
<span class="meta-text">v{{ page.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -255,7 +271,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWikiStore } from '@/stores/useWikiStore'
|
||||
import { useWikiStore, isProtectedPage } from '@/stores/useWikiStore'
|
||||
import { wikiApi } from '@/api/index'
|
||||
import RawMaterialPanel from './components/RawMaterialPanel.vue'
|
||||
import WikiPageViewer from './components/WikiPageViewer.vue'
|
||||
@ -631,6 +647,15 @@ onMounted(() => {
|
||||
.edit-dot { width: 5px; height: 5px; border-radius: 9999px; flex-shrink: 0; }
|
||||
.edit-dot.manual { background: var(--mc-primary); }
|
||||
.page-checkbox { flex-shrink: 0; cursor: pointer; }
|
||||
.page-checkbox:disabled { cursor: not-allowed; opacity: 0.4; }
|
||||
|
||||
/* RFC-051 PR-8: protection flag chips inside the page-item meta line. */
|
||||
.page-flag { font-size: 10px; padding: 1px 6px; border-radius: 99px; font-weight: 600; }
|
||||
.page-flag--system { background: var(--mc-primary-bg); color: var(--mc-primary); border: 1px solid var(--mc-primary); }
|
||||
.page-flag--locked { background: var(--mc-bg-elevated); color: var(--mc-text-secondary); border: 1px solid var(--mc-border-light); }
|
||||
|
||||
/* Subtle accent on the row itself so system pages read as "managed by the system". */
|
||||
.page-item--system { border-left: 2px solid var(--mc-primary); padding-left: 6px; }
|
||||
|
||||
.load-more-btn {
|
||||
width: 100%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user