mirror of https://github.com/halo-dev/halo
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
parent
4785660b18
commit
589bd0d1b4
|
@ -1,3 +1,4 @@
|
||||||
|
// @deprecated
|
||||||
export interface FunctionalPage {
|
export interface FunctionalPage {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
@ -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"?: () =>
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
|
@ -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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue