refactor: refactor the page management and remove the function page (halo-dev/console#816)

#### What type of PR is this?

/kind api-change
/kind improvement

#### What this PR does / why we need it:

1. 重构页面管理,移除功能页面的功能,改为由插件自行配置菜单。
2. 改进自定义页面的 UI 权限绑定,不会再出现没有勾选相关角色,但仍然显示左侧**页面**菜单的问题。

原由请看:https://github.com/halo-dev/halo/issues/3124

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3124

#### Screenshots:

<img width="1920" alt="image" src="https://user-images.githubusercontent.com/21301288/211480169-fd0490a6-bd1a-447c-bde4-155a16355734.png">

插件需要自己定义菜单配置,如:

<img width="1920" alt="image" src="https://user-images.githubusercontent.com/21301288/211480228-146e6b53-9da4-4a60-b691-dd183f0a45c7.png">

```diff
export default definePlugin({
-  name: "PluginLinks",
   components: {},
   routes: [
     {
       parentName: "Root",
       route: {
-        path: "/pages/functional/links",
+        path: "/links",
         name: "Links",
         component: LinkList,
         meta: {
           permissions: ["plugin:links:view"],
+          menu: {
+            name: "链接",
+            group: "content",
+            icon: markRaw(RiLinksLine),
+          },
         },
       },
     },
   ],
-  extensionPoints: {
-    "page:functional:create": () => {
-      return [
-        {
-          name: "链接",
-          url: "/links",
-          path: "/pages/functional/links",
-          permissions: ["plugin:links:view"],
-        },
-      ];
-    },
-  },
 });
```

#### Special notes for your reviewer:

测试方式:

1. 测试左侧菜单的页面入口是否正常。
2. 创建一个新的角色,不勾选页面的查看权限,登录后检查是否可以在左侧菜单看到页面选项。
3. 测试插件添加菜单是否正常,可测试插件:[plugin-links-1.0.0-SNAPSHOT-plain.jar.zip](https://github.com/halo-dev/console/files/10379709/plugin-links-1.0.0-SNAPSHOT-plain.jar.zip)


#### Does this PR introduce a user-facing change?

```release-note
重构页面管理,移除功能页面的功能。
```
pull/3445/head
Ryan Wang 2023-01-10 18:44:40 +08:00 committed by GitHub
parent 4785660b18
commit 589bd0d1b4
6 changed files with 428 additions and 619 deletions

View File

@ -1,3 +1,4 @@
// @deprecated
export interface FunctionalPage { export interface FunctionalPage {
name: string; name: string;
path: string; path: string;

View File

@ -10,6 +10,7 @@ export interface RouteRecordAppend {
} }
export interface ExtensionPoint { export interface ExtensionPoint {
// @deprecated
"page:functional:create"?: () => FunctionalPage[] | Promise<FunctionalPage[]>; "page:functional:create"?: () => FunctionalPage[] | Promise<FunctionalPage[]>;
"attachment:selector:create"?: () => "attachment:selector:create"?: () =>

View File

@ -1,77 +0,0 @@
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import {
VEmpty,
VSpace,
VButton,
IconAddCircle,
VEntity,
VEntityField,
} from "@halo-dev/components";
import type { FunctionalPage, PluginModule } from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin";
const functionalPages = ref<FunctionalPage[]>([] as FunctionalPage[]);
// resolve plugin extension points
const { pluginModules } = usePluginModuleStore();
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.["page:functional:create"]) {
return;
}
const pages = extensionPoints[
"page:functional:create"
]() as FunctionalPage[];
if (pages) {
pages.forEach((page) => {
functionalPages.value.push(page);
});
}
});
});
</script>
<template>
<VEmpty
v-if="!functionalPages.length"
message="当前没有功能页面,功能页面通常由各个插件提供,你可以尝试安装新插件以获得支持"
title="当前没有功能页面"
>
<template #actions>
<VSpace>
<VButton :route="{ name: 'Plugins' }" type="primary">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
安装插件
</VButton>
</VSpace>
</template>
</VEmpty>
<ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li
v-for="(page, index) in functionalPages"
:key="index"
v-permission="page.permissions"
>
<VEntity>
<template #start>
<VEntityField
:title="page.name"
:route="page.path"
:description="page.url"
></VEntityField>
</template>
</VEntity>
</li>
</ul>
</template>

View File

@ -9,6 +9,7 @@ import {
IconAddCircle, IconAddCircle,
IconRefreshLine, IconRefreshLine,
IconExternalLinkLine, IconExternalLinkLine,
IconPages,
VButton, VButton,
VCard, VCard,
VPagination, VPagination,
@ -20,6 +21,7 @@ import {
VEntity, VEntity,
VEntityField, VEntityField,
VLoading, VLoading,
VPageHeader,
Toast, Toast,
} from "@halo-dev/components"; } from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue"; import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
@ -439,373 +441,411 @@ function handleClearFilters() {
</span> </span>
</template> </template>
</SinglePageSettingModal> </SinglePageSettingModal>
<VCard :body-class="['!p-0']" class="rounded-none border-none shadow-none">
<template #header>
<div class="block w-full bg-gray-50 px-4 py-3">
<div
class="relative flex flex-col items-start sm:flex-row sm:items-center"
>
<div
v-permission="['system:singlepages:manage']"
class="mr-4 hidden items-center sm:flex"
>
<input
v-model="checkedAll"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
@change="handleCheckAllChange"
/>
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<div
v-if="!selectedPageNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()"> <VPageHeader title="页面">
关键词{{ keyword }} <template #icon>
</FilterTag> <IconPages class="mr-2 self-center" />
<FilterTag
v-if="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
>
状态{{ selectedPublishStatusItem.label }}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
@close="handleVisibleItemChange(VisibleItems[0])"
>
可见性{{ selectedVisibleItem.label }}
</FilterTag>
<FilterTag v-if="selectedContributor" @close="handleSelectUser()">
作者{{ selectedContributor?.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@close="handleSortItemChange()"
>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"></VButton>
</VSpace>
</div>
<div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg">
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">状态</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in PublishStatusItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedPublishStatusItem.value ===
filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handlePublishStatusItemChange(filterItem)"
>
<span class="truncate">{{ filterItem.label }}</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5"> 可见性 </span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in VisibleItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedVisibleItem.value === filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handleVisibleItemChange(filterItem)"
>
<span class="truncate">
{{ filterItem.label }}
</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<UserDropdownSelector
v-model:selected="selectedContributor"
@select="handleSelectUser"
>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">作者</span>
<span>
<IconArrowDown />
</span>
</div>
</UserDropdownSelector>
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">排序</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(sortItem, index) in SortItems"
:key="index"
v-close-popper
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handleSortItemChange(sortItem)"
>
<span class="truncate">{{ sortItem.label }}</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchSinglePages()"
>
<IconRefreshLine
v-tooltip="`刷新`"
:class="{ 'animate-spin text-gray-900': loading }"
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
/>
</div>
</div>
</VSpace>
</div>
</div>
</div>
</template> </template>
<VLoading v-if="loading" /> <template #actions>
<Transition v-else-if="!singlePages.items.length" appear name="fade"> <VSpace>
<VEmpty message="你可以尝试刷新或者新建页面" title="当前没有页面"> <VButton
<template #actions> v-permission="['system:singlepages:view']"
<VSpace> :route="{ name: 'DeletedSinglePages' }"
<VButton @click="handleFetchSinglePages"></VButton> size="sm"
<VButton >
回收站
</VButton>
<VButton
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="secondary"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建
</VButton>
</VSpace>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<div class="block w-full bg-gray-50 px-4 py-3">
<div
class="relative flex flex-col items-start sm:flex-row sm:items-center"
>
<div
v-permission="['system:singlepages:manage']" v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }" class="mr-4 hidden items-center sm:flex"
type="primary"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建页面
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<Transition v-else appear name="fade">
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(singlePage, index) in singlePages.items" :key="index">
<VEntity :is-selected="checkSelection(singlePage.page)">
<template
v-if="currentUserHasPermission(['system:singlepages:manage'])"
#checkbox
> >
<input <input
v-model="selectedPageNames" v-model="checkedAll"
:value="singlePage.page.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox" type="checkbox"
@change="handleCheckAllChange"
/> />
</template> </div>
<template #start> <div class="flex w-full flex-1 items-center sm:w-auto">
<VEntityField <div
:title="singlePage.page.spec.title" v-if="!selectedPageNames.length"
:route="{ class="flex items-center gap-2"
name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name },
}"
> >
<template #extra> <FormKit
<VSpace> id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<FilterTag
v-if="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
>
状态{{ selectedPublishStatusItem.label }}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
@close="handleVisibleItemChange(VisibleItems[0])"
>
可见性{{ selectedVisibleItem.label }}
</FilterTag>
<FilterTag
v-if="selectedContributor"
@close="handleSelectUser()"
>
作者{{ selectedContributor?.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@close="handleSortItemChange()"
>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"
>删除</VButton
>
</VSpace>
</div>
<div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg">
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">状态</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in PublishStatusItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedPublishStatusItem.value ===
filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handlePublishStatusItemChange(filterItem)"
>
<span class="truncate">{{ filterItem.label }}</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5"> 可见性 </span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in VisibleItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedVisibleItem.value === filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handleVisibleItemChange(filterItem)"
>
<span class="truncate">
{{ filterItem.label }}
</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<UserDropdownSelector
v-model:selected="selectedContributor"
@select="handleSelectUser"
>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">作者</span>
<span>
<IconArrowDown />
</span>
</div>
</UserDropdownSelector>
<FloatingDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">排序</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(sortItem, index) in SortItems"
:key="index"
v-close-popper
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handleSortItemChange(sortItem)"
>
<span class="truncate">{{ sortItem.label }}</span>
</li>
</ul>
</div>
</template>
</FloatingDropdown>
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchSinglePages()"
>
<IconRefreshLine
v-tooltip="`刷新`"
:class="{ 'animate-spin text-gray-900': loading }"
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
/>
</div>
</div>
</VSpace>
</div>
</div>
</div>
</template>
<VLoading v-if="loading" />
<Transition v-else-if="!singlePages.items.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者新建页面" title="当前没有页面">
<template #actions>
<VSpace>
<VButton @click="handleFetchSinglePages"></VButton>
<VButton
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="primary"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建页面
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<Transition v-else appear name="fade">
<ul
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(singlePage, index) in singlePages.items" :key="index">
<VEntity :is-selected="checkSelection(singlePage.page)">
<template
v-if="currentUserHasPermission(['system:singlepages:manage'])"
#checkbox
>
<input
v-model="selectedPageNames"
:value="singlePage.page.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
</template>
<template #start>
<VEntityField
:title="singlePage.page.spec.title"
:route="{
name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name },
}"
>
<template #extra>
<VSpace>
<RouterLink
v-if="singlePage.page.status?.inProgress"
v-tooltip="`当前有内容已保存,但还未发布。`"
:to="{
name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name },
}"
class="flex items-center"
>
<VStatusDot state="success" animate />
</RouterLink>
<a
v-if="singlePage.page.status?.permalink"
target="_blank"
:href="singlePage.page.status?.permalink"
:title="singlePage.page.status?.permalink"
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
>
<IconExternalLinkLine class="h-3.5 w-3.5" />
</a>
</VSpace>
</template>
<template #description>
<div class="flex w-full flex-col gap-1">
<VSpace class="w-full">
<span class="text-xs text-gray-500">
访问量 {{ singlePage.stats.visit || 0 }}
</span>
<span class="text-xs text-gray-500">
评论 {{ singlePage.stats.totalComment || 0 }}
</span>
</VSpace>
</div>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink <RouterLink
v-if="singlePage.page.status?.inProgress" v-for="(
v-tooltip="`当前有内容已保存,但还未发布。`" contributor, contributorIndex
) in singlePage.contributors"
:key="contributorIndex"
:to="{ :to="{
name: 'SinglePageEditor', name: 'UserDetail',
query: { name: singlePage.page.metadata.name }, params: { name: contributor.name },
}" }"
class="flex items-center" class="flex items-center"
> >
<VStatusDot state="success" animate /> <VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink> </RouterLink>
<a </template>
v-if="singlePage.page.status?.permalink" </VEntityField>
target="_blank" <VEntityField :description="getPublishStatus(singlePage.page)">
:href="singlePage.page.status?.permalink" <template v-if="isPublishing(singlePage.page)" #description>
:title="singlePage.page.status?.permalink" <VStatusDot text="发布中" animate />
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block" </template>
> </VEntityField>
<IconExternalLinkLine class="h-3.5 w-3.5" /> <VEntityField>
</a> <template #description>
</VSpace> <IconEye
</template> v-if="singlePage.page.spec.visible === 'PUBLIC'"
<template #description> v-tooltip="`公开访问`"
<div class="flex w-full flex-col gap-1"> class="cursor-pointer text-sm transition-all hover:text-blue-600"
<VSpace class="w-full"> />
<span class="text-xs text-gray-500"> <IconEyeOff
访问量 {{ singlePage.stats.visit || 0 }} v-if="singlePage.page.spec.visible === 'PRIVATE'"
</span> v-tooltip="`私有访问`"
<span class="text-xs text-gray-500"> class="cursor-pointer text-sm transition-all hover:text-blue-600"
评论 {{ singlePage.stats.totalComment || 0 }} />
</span> <!-- TODO: 支持内部成员可访问 -->
</VSpace> <IconTeam
</div> v-if="false"
</template> v-tooltip="`内部成员可访问`"
</VEntityField> class="cursor-pointer text-sm transition-all hover:text-blue-600"
</template> />
<template #end> </template>
<VEntityField> </VEntityField>
<template #description> <VEntityField v-if="singlePage?.page?.spec.deleted">
<RouterLink <template #description>
v-for="( <VStatusDot v-tooltip="``" state="warning" animate />
contributor, contributorIndex </template>
) in singlePage.contributors" </VEntityField>
:key="contributorIndex" <VEntityField>
:to="{ <template #description>
name: 'UserDetail', <span class="truncate text-xs tabular-nums text-gray-500">
params: { name: contributor.name }, {{ formatDatetime(singlePage.page.spec.publishTime) }}
}" </span>
class="flex items-center" </template>
> </VEntityField>
<VAvatar </template>
v-tooltip="contributor.displayName" <template
size="xs" v-if="currentUserHasPermission(['system:singlepages:manage'])"
:src="contributor.avatar" #dropdownItems
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField :description="getPublishStatus(singlePage.page)">
<template v-if="isPublishing(singlePage.page)" #description>
<VStatusDot text="发布中" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<IconEye
v-if="singlePage.page.spec.visible === 'PUBLIC'"
v-tooltip="`公开访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<IconEyeOff
v-if="singlePage.page.spec.visible === 'PRIVATE'"
v-tooltip="`私有访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<!-- TODO: 支持内部成员可访问 -->
<IconTeam
v-if="false"
v-tooltip="`内部成员可访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
</template>
</VEntityField>
<VEntityField v-if="singlePage?.page?.spec.deleted">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(singlePage.page.spec.publishTime) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:singlepages:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenSettingModal(singlePage.page)"
> >
设置 <VButton
</VButton> v-close-popper
<VButton block
v-close-popper type="secondary"
block @click="handleOpenSettingModal(singlePage.page)"
type="danger" >
@click="handleDelete(singlePage.page)" 设置
> </VButton>
删除 <VButton
</VButton> v-close-popper
</template> block
</VEntity> type="danger"
</li> @click="handleDelete(singlePage.page)"
</ul> >
</Transition> 删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer> <template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end"> <div class="bg-white sm:flex sm:items-center sm:justify-end">
<VPagination <VPagination
:page="singlePages.page" :page="singlePages.page"
:size="singlePages.size" :size="singlePages.size"
:total="singlePages.total" :total="singlePages.total"
:size-options="[20, 30, 50, 100]" :size-options="[20, 30, 50, 100]"
@change="handlePaginationChange" @change="handlePaginationChange"
/> />
</div> </div>
</template> </template>
</VCard> </VCard>
</div>
</template> </template>

View File

@ -1,109 +0,0 @@
<script lang="ts" setup>
import { ref, watchEffect } from "vue";
import {
VCard,
IconAddCircle,
VPageHeader,
VTabbar,
IconPages,
VButton,
VSpace,
} from "@halo-dev/components";
import BasicLayout from "@/layouts/BasicLayout.vue";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
interface PageTab {
id: string;
label: string;
route: {
name: string;
};
}
const tabs = ref<PageTab[]>([
{
id: "functional",
label: "功能页面",
route: {
name: "FunctionalPages",
},
},
{
id: "single",
label: "自定义页面",
route: {
name: "SinglePages",
},
},
]);
const activeTab = ref(tabs.value[0].id);
const onTabChange = (routeName: string) => {
const tab = tabs.value.find((tab) => {
return tab.route.name === routeName;
});
if (tab) {
activeTab.value = tab.id;
return;
}
activeTab.value = tabs.value[0].id;
};
const handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id);
if (tab) {
router.push(tab.route);
}
};
watchEffect(() => {
onTabChange(route.name as string);
});
</script>
<template>
<BasicLayout>
<VPageHeader title="页面">
<template #icon>
<IconPages class="mr-2 self-center" />
</template>
<template #actions>
<VSpace>
<VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
回收站
</VButton>
<VButton
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="secondary"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建
</VButton>
</VSpace>
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="activeTab"
:items="tabs"
class="w-full !rounded-none"
type="outline"
@change="handleTabChange"
></VTabbar>
</template>
<div>
<RouterView :key="activeTab" />
</div>
</VCard>
</div>
</BasicLayout>
</template>

View File

@ -1,8 +1,5 @@
import { definePlugin } from "@halo-dev/console-shared"; import { definePlugin } from "@halo-dev/console-shared";
import BasicLayout from "@/layouts/BasicLayout.vue"; import BasicLayout from "@/layouts/BasicLayout.vue";
import BlankLayout from "@/layouts/BlankLayout.vue";
import PageLayout from "./layouts/PageLayout.vue";
import FunctionalPageList from "./FunctionalPageList.vue";
import SinglePageList from "./SinglePageList.vue"; import SinglePageList from "./SinglePageList.vue";
import DeletedSinglePageList from "./DeletedSinglePageList.vue"; import DeletedSinglePageList from "./DeletedSinglePageList.vue";
import SinglePageEditor from "./SinglePageEditor.vue"; import SinglePageEditor from "./SinglePageEditor.vue";
@ -16,89 +13,45 @@ export default definePlugin({
}, },
routes: [ routes: [
{ {
path: "/pages", path: "/single-pages",
component: BlankLayout, component: BasicLayout,
name: "BasePages",
redirect: {
name: "FunctionalPages",
},
meta: {
menu: {
name: "页面",
group: "content",
icon: markRaw(IconPages),
priority: 1,
},
},
children: [ children: [
{ {
path: "functional", path: "",
component: PageLayout, name: "SinglePages",
children: [ component: SinglePageList,
{ meta: {
path: "", title: "页面",
name: "FunctionalPages", searchable: true,
component: FunctionalPageList, permissions: ["system:singlepages:view"],
meta: { menu: {
title: "功能页面", name: "页面",
searchable: true, group: "content",
}, icon: markRaw(IconPages),
priority: 1,
mobile: true,
}, },
], },
}, },
{ {
path: "single", path: "deleted",
component: BlankLayout, name: "DeletedSinglePages",
children: [ component: DeletedSinglePageList,
{ meta: {
path: "", title: "页面回收站",
component: PageLayout, searchable: true,
children: [ permissions: ["system:singlepages:view"],
{ },
path: "", },
name: "SinglePages", {
component: SinglePageList, path: "editor",
meta: { name: "SinglePageEditor",
title: "自定义页面", component: SinglePageEditor,
searchable: true, meta: {
permissions: ["system:singlepages:view"], title: "页面编辑",
}, searchable: true,
}, permissions: ["system:singlepages:manage"],
], },
},
{
path: "deleted",
component: BasicLayout,
children: [
{
path: "",
name: "DeletedSinglePages",
component: DeletedSinglePageList,
meta: {
title: "自定义页面回收站",
searchable: true,
permissions: ["system:singlepages:view"],
},
},
],
},
{
path: "editor",
component: BasicLayout,
children: [
{
path: "",
name: "SinglePageEditor",
component: SinglePageEditor,
meta: {
title: "页面编辑",
searchable: true,
permissions: ["system:singlepages:manage"],
},
},
],
},
],
}, },
], ],
}, },