refactor: add loading states for data list (halo-dev/console#703)

#### What type of PR is this?

/kind improvement
/milestone 2.0

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

优化列表的加载,添加加载动画,解决加载完成之后界面闪动的问题。

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

Ref https://github.com/halo-dev/halo/issues/2370

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

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

```release-note
优化列表的加载,添加加载动画,解决加载完成之后界面闪动的问题。
```
pull/3445/head
Ryan Wang 2022-11-24 11:46:10 +08:00 committed by GitHub
parent c0877e2a9c
commit 3f3147ce34
27 changed files with 1941 additions and 1790 deletions

View File

@ -21,3 +21,4 @@ export * from "./components/empty";
export * from "./components/status";
export * from "./components/entity";
export * from "./components/toast";
export * from "./components/loading";

View File

@ -0,0 +1,26 @@
<script lang="ts" setup></script>
<template>
<div class="flex items-center justify-center py-4">
<svg
class="h-5 w-5 animate-spin"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg>
</div>
</template>

View File

@ -0,0 +1 @@
export { default as VLoading } from "./Loading.vue";

View File

@ -19,6 +19,7 @@ import {
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -563,30 +564,33 @@ onMounted(() => {
/>
</div>
<VEmpty
v-if="!attachments.total && !loading"
message="当前分组没有附件,你可以尝试刷新或者上传附件"
title="当前分组没有附件"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchAttachments"></VButton>
<VButton
v-permission="['system:attachments:manage']"
type="secondary"
@click="uploadVisible = true"
>
<template #icon>
<IconUpload class="h-full w-full" />
</template>
上传附件
</VButton>
</VSpace>
</template>
</VEmpty>
<VLoading v-if="loading" />
<Transition v-else-if="!attachments.total" appear name="fade">
<VEmpty
message="当前分组没有附件,你可以尝试刷新或者上传附件"
title="当前分组没有附件"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchAttachments"></VButton>
<VButton
v-permission="['system:attachments:manage']"
type="secondary"
@click="uploadVisible = true"
>
<template #icon>
<IconUpload class="h-full w-full" />
</template>
上传附件
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<div v-else>
<div v-if="viewType === 'grid'">
<Transition v-if="viewType === 'grid'" appear name="fade">
<div
class="mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12"
role="list"
@ -666,119 +670,126 @@ onMounted(() => {
</div>
</VCard>
</div>
</div>
<ul
v-if="viewType === 'list'"
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(attachment, index) in attachments.items" :key="index">
<VEntity :is-selected="isChecked(attachment)">
<template
v-if="
currentUserHasPermission(['system:attachments:manage'])
"
#checkbox
>
<input
:checked="selectedAttachments.has(attachment)"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
@click="handleSelect(attachment)"
/>
</template>
<template #start>
<VEntityField>
<template #description>
<div
class="h-10 w-10 rounded border bg-white p-1 hover:shadow-sm"
>
<AttachmentFileTypeIcon
:display-ext="false"
:file-name="attachment.spec.displayName"
:width="8"
:height="8"
/>
</div>
</template>
</VEntityField>
<VEntityField
:title="attachment.spec.displayName"
@click="handleClickItem(attachment)"
>
<template #description>
<VSpace>
<span class="text-xs text-gray-500">
{{ attachment.spec.mediaType }}
</span>
<span class="text-xs text-gray-500">
{{ prettyBytes(attachment.spec.size || 0) }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField
:description="
getPolicyName(attachment.spec.policyRef?.name)
</Transition>
<Transition v-if="viewType === 'list'" appear name="fade">
<ul
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li
v-for="(attachment, index) in attachments.items"
:key="index"
>
<VEntity :is-selected="isChecked(attachment)">
<template
v-if="
currentUserHasPermission(['system:attachments:manage'])
"
/>
<VEntityField>
<template #description>
<RouterLink
:to="{
name: 'UserDetail',
params: { name: attachment.spec.uploadedBy?.name },
}"
class="text-xs text-gray-500"
>
{{ attachment.spec.uploadedBy?.name }}
</RouterLink>
</template>
</VEntityField>
<VEntityField v-if="attachment.metadata.deletionTimestamp">
<template #description>
<VStatusDot
v-tooltip="`删除中`"
state="warning"
animate
/>
</template>
</VEntityField>
<VEntityField>
<template #description>
<span
class="truncate text-xs tabular-nums text-gray-500"
>
{{
formatDatetime(
attachment.metadata.creationTimestamp
)
}}
</span>
</template>
</VEntityField>
</template>
<template
v-if="
currentUserHasPermission(['system:attachments:manage'])
"
#dropdownItems
>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(attachment)"
#checkbox
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<input
:checked="selectedAttachments.has(attachment)"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
@click="handleSelect(attachment)"
/>
</template>
<template #start>
<VEntityField>
<template #description>
<div
class="h-10 w-10 rounded border bg-white p-1 hover:shadow-sm"
>
<AttachmentFileTypeIcon
:display-ext="false"
:file-name="attachment.spec.displayName"
:width="8"
:height="8"
/>
</div>
</template>
</VEntityField>
<VEntityField
:title="attachment.spec.displayName"
@click="handleClickItem(attachment)"
>
<template #description>
<VSpace>
<span class="text-xs text-gray-500">
{{ attachment.spec.mediaType }}
</span>
<span class="text-xs text-gray-500">
{{ prettyBytes(attachment.spec.size || 0) }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField
:description="
getPolicyName(attachment.spec.policyRef?.name)
"
/>
<VEntityField>
<template #description>
<RouterLink
:to="{
name: 'UserDetail',
params: {
name: attachment.spec.uploadedBy?.name,
},
}"
class="text-xs text-gray-500"
>
{{ attachment.spec.uploadedBy?.name }}
</RouterLink>
</template>
</VEntityField>
<VEntityField
v-if="attachment.metadata.deletionTimestamp"
>
<template #description>
<VStatusDot
v-tooltip="`删除中`"
state="warning"
animate
/>
</template>
</VEntityField>
<VEntityField>
<template #description>
<span
class="truncate text-xs tabular-nums text-gray-500"
>
{{
formatDatetime(
attachment.metadata.creationTimestamp
)
}}
</span>
</template>
</VEntityField>
</template>
<template
v-if="
currentUserHasPermission(['system:attachments:manage'])
"
#dropdownItems
>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(attachment)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
</div>
<template #footer>

View File

@ -10,6 +10,7 @@ import {
IconRefreshLine,
VEmpty,
Dialog,
VLoading,
} from "@halo-dev/components";
import CommentListItem from "./components/CommentListItem.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -459,36 +460,40 @@ function handleClearFilters() {
</div>
</div>
</template>
<VEmpty
v-if="!comments.items.length && !loading"
message="你可以尝试刷新或者修改筛选条件"
title="当前没有评论"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchComments"></VButton>
</VSpace>
</template>
</VEmpty>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(comment, index) in comments.items" :key="index">
<CommentListItem
:comment="comment"
:is-selected="checkSelection(comment)"
@reload="handleFetchComments"
>
<template #checkbox>
<input
v-model="selectedCommentNames"
:value="comment?.comment?.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="comment-checkbox"
type="checkbox"
/>
</template>
</CommentListItem>
</li>
</ul>
<VLoading v-if="loading" />
<Transition v-else-if="!comments.items.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者修改筛选条件" title="当前没有评论">
<template #actions>
<VSpace>
<VButton @click="handleFetchComments"></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="(comment, index) in comments.items" :key="index">
<CommentListItem
:comment="comment"
:is-selected="checkSelection(comment)"
@reload="handleFetchComments"
>
<template #checkbox>
<input
v-model="selectedCommentNames"
:value="comment?.comment?.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="comment-checkbox"
type="checkbox"
/>
</template>
</CommentListItem>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -14,6 +14,7 @@ import {
VEntityField,
VPageHeader,
VStatusDot,
VLoading,
} from "@halo-dev/components";
import { onMounted, ref, watch } from "vue";
import type { ListedSinglePageList, SinglePage } from "@halo-dev/api-client";
@ -305,125 +306,130 @@ function handleClearKeyword() {
</div>
</div>
</template>
<VEmpty
v-if="!singlePages.items.length && !loading"
message="你可以尝试刷新或者返回自定义页面管理"
title="没有自定义页面被放入回收站"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchSinglePages"></VButton>
<VButton
v-permission="['system:singlepages:view']"
:route="{ name: 'SinglePages' }"
type="primary"
>
返回
</VButton>
</VSpace>
</template>
</VEmpty>
<ul
v-else
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">
<template #description>
<VSpace>
<span class="text-xs text-gray-500">
{{ singlePage.page.status?.permalink }}
</span>
<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>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(
contributor, contributorIndex
) in singlePage.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField v-if="!singlePage?.page?.spec.deleted">
<template #description>
<VStatusDot v-tooltip="``" state="success" animate />
</template>
</VEntityField>
<VEntityField v-if="singlePage?.page?.metadata.deletionTimestamp">
<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
>
<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-close-popper
block
type="danger"
@click="handleDeletePermanently(singlePage.page)"
v-permission="['system:singlepages:view']"
:route="{ name: 'SinglePages' }"
type="primary"
>
永久删除
返回
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleRecovery(singlePage.page)"
</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
>
恢复
</VButton>
</template>
</VEntity>
</li>
</ul>
<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">
<template #description>
<VSpace>
<span class="text-xs text-gray-500">
{{ singlePage.page.status?.permalink }}
</span>
<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>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(
contributor, contributorIndex
) in singlePage.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField v-if="!singlePage?.page?.spec.deleted">
<template #description>
<VStatusDot v-tooltip="``" state="success" animate />
</template>
</VEntityField>
<VEntityField
v-if="singlePage?.page?.metadata.deletionTimestamp"
>
<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="danger"
@click="handleDeletePermanently(singlePage.page)"
>
永久删除
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleRecovery(singlePage.page)"
>
恢复
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -18,6 +18,7 @@ import {
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
@ -613,170 +614,167 @@ function handleClearFilters() {
</div>
</div>
</template>
<VEmpty
v-if="!singlePages.items.length && !loading"
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" />
<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>
新建页面
</VButton>
</VSpace>
</template>
</VEmpty>
<ul
v-else
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 #start>
<VEntityField
:title="singlePage.page.spec.title"
:route="{
name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name },
}"
>
<template #extra>
<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>
</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>
<VSpace class="w-full">
<span class="truncate text-xs text-gray-500">
{{ singlePage.page.status?.permalink }}
</span>
</VSpace>
</div>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(
contributor, contributorIndex
) in singlePage.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
: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"
/>
<IconTeam
v-if="singlePage.page.spec.visible === 'INTERNAL'"
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
>
<template #extra>
<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>
</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>
<VSpace class="w-full">
<span class="truncate text-xs text-gray-500">
{{ singlePage.page.status?.permalink }}
</span>
</VSpace>
</div>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(
contributor, contributorIndex
) in singlePage.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
: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"
/>
<IconTeam
v-if="singlePage.page.spec.visible === 'INTERNAL'"
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
block
type="danger"
@click="handleDelete(singlePage.page)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenSettingModal(singlePage.page)"
>
设置
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(singlePage.page)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -14,6 +14,7 @@ import {
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import PostTag from "./tags/components/PostTag.vue";
import { onMounted, ref, watch } from "vue";
@ -298,138 +299,146 @@ function handleClearKeyword() {
</div>
</template>
<VEmpty
v-if="!posts.items.length && !loading"
message="你可以尝试刷新或者返回文章管理"
title="没有文章被放入回收站"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPosts"></VButton>
<VButton :route="{ name: 'Posts' }" type="primary"> 返回 </VButton>
</VSpace>
</template>
</VEmpty>
<VLoading v-if="loading" />
<ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(post, index) in posts.items" :key="index">
<VEntity :is-selected="checkSelection(post.post)">
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#checkbox
>
<input
v-model="selectedPostNames"
:value="post.post.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField :title="post.post.spec.title">
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</template>
<template #description>
<VSpace>
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
<Transition v-else-if="!posts.items.length" appear name="fade">
<VEmpty
message="你可以尝试刷新或者返回文章管理"
title="没有文章被放入回收站"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPosts"></VButton>
<VButton :route="{ name: 'Posts' }" type="primary">
返回
</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="(post, index) in posts.items" :key="index">
<VEntity :is-selected="checkSelection(post.post)">
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#checkbox
>
<input
v-model="selectedPostNames"
:value="post.post.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField :title="post.post.spec.title">
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</template>
<template #description>
<VSpace>
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
{{ category.spec.displayName }}
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
>
{{ category.spec.displayName }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(
contributor, contributorIndex
) in post.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField v-if="!post?.post?.spec.deleted">
<template #description>
<VStatusDot v-tooltip="``" state="success" animate />
</template>
</VEntityField>
<VEntityField v-if="post?.post?.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(post.post.spec.publishTime) }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(contributor, contributorIndex) in post.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField v-if="!post?.post?.spec.deleted">
<template #description>
<VStatusDot v-tooltip="``" state="success" animate />
</template>
</VEntityField>
<VEntityField v-if="post?.post?.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(post.post.spec.publishTime) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="danger"
@click="handleDeletePermanently(post.post)"
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
永久删除
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleRecovery(post.post)"
>
恢复
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="danger"
@click="handleDeletePermanently(post.post)"
>
永久删除
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleRecovery(post.post)"
>
恢复
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -20,6 +20,7 @@ import {
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue";
@ -823,183 +824,184 @@ const hasFilters = computed(() => {
</div>
</div>
</template>
<VEmpty
v-if="!posts.items.length && !loading"
message="你可以尝试刷新或者新建文章"
title="当前没有文章"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPosts"></VButton>
<VButton
v-permission="['system:posts:manage']"
:route="{ name: 'PostEditor' }"
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="(post, index) in posts.items" :key="index">
<VEntity :is-selected="checkSelection(post.post)">
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#checkbox
>
<input
v-model="selectedPostNames"
:value="post.post.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField
:title="post.post.spec.title"
:route="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
<VLoading v-if="loading" />
<Transition v-else-if="!posts.items.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者新建文章" title="当前没有文章">
<template #actions>
<VSpace>
<VButton @click="handleFetchPosts"></VButton>
<VButton
v-permission="['system:posts:manage']"
:route="{ name: 'PostEditor' }"
type="primary"
>
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<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="(post, index) in posts.items" :key="index">
<VEntity :is-selected="checkSelection(post.post)">
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#checkbox
>
<input
v-model="selectedPostNames"
:value="post.post.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField
:title="post.post.spec.title"
:route="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
>
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<RouterLink
v-if="post.post.status?.inProgress"
v-tooltip="`当前有内容已保存,但还未发布。`"
:to="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
class="flex items-center"
>
<VStatusDot state="success" animate />
</RouterLink>
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</template>
<template #description>
<VSpace>
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
>
{{ category.spec.displayName }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-if="post.post.status?.inProgress"
v-tooltip="`当前有内容已保存,但还未发布。`"
v-for="(
contributor, contributorIndex
) in post.contributors"
:key="contributorIndex"
:to="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VStatusDot state="success" animate />
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
:tag="tag"
route
></PostTag>
</VSpace>
</template>
<template #description>
<VSpace>
<p
v-if="post.categories.length"
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
>
分类<span
v-for="(category, categoryIndex) in post.categories"
:key="categoryIndex"
class="cursor-pointer hover:text-gray-900"
>
{{ category.spec.displayName }}
</span>
</p>
<span class="text-xs text-gray-500">
访问量 {{ post.stats.visit || 0 }}
</template>
</VEntityField>
<VEntityField :description="getPublishStatus(post.post)">
<template v-if="isPublishing(post.post)" #description>
<VStatusDot text="发布中" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<IconEye
v-if="post.post.spec.visible === 'PUBLIC'"
v-tooltip="`公开访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<IconEyeOff
v-if="post.post.spec.visible === 'PRIVATE'"
v-tooltip="`私有访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<IconTeam
v-if="post.post.spec.visible === 'INTERNAL'"
v-tooltip="`内部成员可访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
</template>
</VEntityField>
<VEntityField v-if="post?.post?.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(post.post.spec.publishTime) }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.stats.totalComment || 0 }}
</span>
</VSpace>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<RouterLink
v-for="(contributor, contributorIndex) in post.contributors"
:key="contributorIndex"
:to="{
name: 'UserDetail',
params: { name: contributor.name },
}"
class="flex items-center"
>
<VAvatar
v-tooltip="contributor.displayName"
size="xs"
:src="contributor.avatar"
:alt="contributor.displayName"
circle
></VAvatar>
</RouterLink>
</template>
</VEntityField>
<VEntityField :description="getPublishStatus(post.post)">
<template v-if="isPublishing(post.post)" #description>
<VStatusDot text="发布中" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<IconEye
v-if="post.post.spec.visible === 'PUBLIC'"
v-tooltip="`公开访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<IconEyeOff
v-if="post.post.spec.visible === 'PRIVATE'"
v-tooltip="`私有访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
<IconTeam
v-if="post.post.spec.visible === 'INTERNAL'"
v-tooltip="`内部成员可访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
</template>
</VEntityField>
<VEntityField v-if="post?.post?.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(post.post.spec.publishTime) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenSettingModal(post.post)"
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
设置
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(post.post)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenSettingModal(post.post)"
>
设置
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(post.post)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -12,6 +12,7 @@ import {
VEmpty,
VPageHeader,
VSpace,
VLoading,
} from "@halo-dev/components";
import CategoryEditingModal from "./components/CategoryEditingModal.vue";
import CategoryListItem from "./components/CategoryListItem.vue";
@ -111,34 +112,34 @@ const onEditingModalClose = () => {
</div>
</div>
</template>
<VEmpty
v-if="!categories.length && !loading"
message="你可以尝试刷新或者新建分类"
title="当前没有分类"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchCategories"></VButton>
<VButton
v-permission="['system:posts:manage']"
type="primary"
@click="editingModal = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建分类
</VButton>
</VSpace>
</template>
</VEmpty>
<CategoryListItem
v-else
:categories="categoriesTree"
@change="handleUpdateInBatch"
@delete="handleDelete"
@open-editing="handleOpenEditingModal"
/>
<VLoading v-if="loading" />
<Transition v-else-if="!categories.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者新建分类" title="当前没有分类">
<template #actions>
<VSpace>
<VButton @click="handleFetchCategories"></VButton>
<VButton
v-permission="['system:posts:manage']"
type="primary"
@click="editingModal = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建分类
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<Transition v-else appear name="fade">
<CategoryListItem
:categories="categoriesTree"
@change="handleUpdateInBatch"
@delete="handleDelete"
@open-editing="handleOpenEditingModal"
/>
</Transition>
</VCard>
</div>
</template>

View File

@ -16,6 +16,7 @@ import {
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import TagEditingModal from "./components/TagEditingModal.vue";
import PostTag from "./components/PostTag.vue";
@ -158,92 +159,99 @@ onMounted(async () => {
</div>
</div>
</template>
<VEmpty
v-if="!tags.length && !loading"
message="你可以尝试刷新或者新建标签"
title="当前没有标签"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchTags"></VButton>
<VButton type="primary" @click="editingModal = true">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建标签
</VButton>
</VSpace>
</template>
</VEmpty>
<div v-else>
<ul
v-if="viewType === 'list'"
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(tag, index) in tags" :key="index">
<VEntity
:is-selected="selectedTag?.metadata.name === tag.metadata.name"
>
<template #start>
<VEntityField :description="tag.status?.permalink">
<template #title>
<PostTag :tag="tag" />
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="tag.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField
:description="`${tag.status?.postCount || 0} 篇文章`"
/>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(tag.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<VButton
v-permission="['system:posts:manage']"
v-close-popper
block
type="secondary"
@click="handleOpenEditingModal(tag)"
>
修改
</VButton>
<VButton
v-permission="['system:posts:manage']"
v-close-popper
block
type="danger"
@click="handleDelete(tag)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<VLoading v-if="loading" />
<Transition v-else-if="!tags.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者新建标签" title="当前没有标签">
<template #actions>
<VSpace>
<VButton @click="handleFetchTags"></VButton>
<VButton type="primary" @click="editingModal = true">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建标签
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<div v-else class="flex flex-wrap gap-3 p-4" role="list">
<PostTag
v-for="(tag, index) in tags"
:key="index"
:tag="tag"
@click="handleOpenEditingModal(tag)"
/>
</div>
<div v-else>
<Transition v-if="viewType === 'list'" appear name="fade">
<ul
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(tag, index) in tags" :key="index">
<VEntity
:is-selected="selectedTag?.metadata.name === tag.metadata.name"
>
<template #start>
<VEntityField :description="tag.status?.permalink">
<template #title>
<PostTag :tag="tag" />
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="tag.metadata.deletionTimestamp">
<template #description>
<VStatusDot
v-tooltip="`删除中`"
state="warning"
animate
/>
</template>
</VEntityField>
<VEntityField
:description="`${tag.status?.postCount || 0} 篇文章`"
/>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(tag.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<VButton
v-permission="['system:posts:manage']"
v-close-popper
block
type="secondary"
@click="handleOpenEditingModal(tag)"
>
修改
</VButton>
<VButton
v-permission="['system:posts:manage']"
v-close-popper
block
type="danger"
@click="handleDelete(tag)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<Transition v-else appear name="fade">
<div class="flex flex-wrap gap-3 p-4" role="list">
<PostTag
v-for="(tag, index) in tags"
:key="index"
:tag="tag"
@click="handleOpenEditingModal(tag)"
/>
</div>
</Transition>
</div>
</VCard>
</div>

View File

@ -8,6 +8,7 @@ import {
VEmpty,
VPageHeader,
VSpace,
VLoading,
} from "@halo-dev/components";
import MenuItemEditingModal from "./components/MenuItemEditingModal.vue";
import MenuItemListItem from "./components/MenuItemListItem.vue";
@ -220,35 +221,38 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
</div>
</div>
</template>
<VEmpty
v-if="!menuItems.length && !loading"
message="你可以尝试刷新或者新建菜单项"
title="当前没有菜单项"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchMenuItems"> </VButton>
<VButton
v-permission="['system:menus:manage']"
type="primary"
@click="menuItemEditingModal = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新增菜单项
</VButton>
</VSpace>
</template>
</VEmpty>
<MenuItemListItem
v-else
:menu-tree-items="menuTreeItems"
@change="handleUpdateInBatch"
@delete="handleDelete"
@open-editing="handleOpenEditingModal"
@open-create-by-parent="handleOpenCreateByParentModal"
/>
<VLoading v-if="loading" />
<Transition v-else-if="!menuItems.length" appear name="fade">
<VEmpty
message="你可以尝试刷新或者新建菜单项"
title="当前没有菜单项"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchMenuItems"> </VButton>
<VButton
v-permission="['system:menus:manage']"
type="primary"
@click="menuItemEditingModal = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新增菜单项
</VButton>
</VSpace>
</template>
</VEmpty>
</Transition>
<Transition v-else appear name="fade">
<MenuItemListItem
:menu-tree-items="menuTreeItems"
@change="handleUpdateInBatch"
@delete="handleDelete"
@open-editing="handleOpenEditingModal"
@open-create-by-parent="handleOpenCreateByParentModal"
/>
</Transition>
</VCard>
</div>
</div>

View File

@ -9,6 +9,7 @@ import {
VEntity,
VEntityField,
VTag,
VLoading,
} from "@halo-dev/components";
import MenuEditingModal from "./MenuEditingModal.vue";
import { onMounted, onUnmounted, ref } from "vue";
@ -190,75 +191,76 @@ onMounted(handleFetchPrimaryMenuName);
@created="handleSelect"
/>
<VCard :body-class="['!p-0']" title="菜单">
<VEmpty
v-if="!menus.length && !loading"
message="你可以尝试刷新或者新建菜单"
title="当前没有菜单"
>
<template #actions>
<VSpace>
<VButton size="sm" @click="handleFetchMenus"> </VButton>
</VSpace>
</template>
</VEmpty>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li
v-for="(menu, index) in menus"
:key="index"
@click="handleSelect(menu)"
>
<VEntity
:is-selected="selectedMenu?.metadata.name === menu.metadata.name"
<VLoading v-if="loading" />
<Transition v-else-if="!menus.length" appear name="fade">
<VEmpty message="你可以尝试刷新或者新建菜单" title="当前没有菜单">
<template #actions>
<VSpace>
<VButton size="sm" @click="handleFetchMenus"> </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="(menu, index) in menus"
:key="index"
@click="handleSelect(menu)"
>
<template #start>
<VEntityField
:title="menu.spec?.displayName"
:description="`${menu.spec.menuItems?.length || 0} 个菜单项`"
>
<template v-if="menu.metadata.name === primaryMenuName" #extra>
<VTag>主菜单</VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="menu.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:menus:manage'])"
#dropdownItems
<VEntity
:is-selected="selectedMenu?.metadata.name === menu.metadata.name"
>
<VButton
v-close-popper
block
type="secondary"
@click="handleSetPrimaryMenu(menu)"
<template #start>
<VEntityField
:title="menu.spec?.displayName"
:description="`${menu.spec.menuItems?.length || 0} 个菜单项`"
>
<template v-if="menu.metadata.name === primaryMenuName" #extra>
<VTag>主菜单</VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="menu.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:menus:manage'])"
#dropdownItems
>
设置为主菜单
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleOpenEditingModal(menu)"
>
修改
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDeleteMenu(menu)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="secondary"
@click="handleSetPrimaryMenu(menu)"
>
设置为主菜单
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleOpenEditingModal(menu)"
>
修改
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDeleteMenu(menu)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template v-if="currentUserHasPermission(['system:menus:manage'])" #footer>
<VButton block type="secondary" @click="handleOpenEditingModal()">
新增

View File

@ -55,170 +55,177 @@ const onUpgradeModalClose = () => {
</script>
<template>
<div class="bg-white px-4 py-4 sm:px-6">
<div class="group flex items-center justify-between">
<div class="flex flex-row items-center gap-3">
<VAvatar
:key="selectedTheme?.metadata.name"
:alt="selectedTheme?.spec.displayName"
:src="selectedTheme?.spec.logo"
size="lg"
/>
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ selectedTheme?.spec.displayName }}
</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">
{{ selectedTheme?.spec.version }}
</span>
<VTag>
{{ isActivated ? "当前启用" : "未启用" }}
</VTag>
</p>
</div>
</div>
<FloatingDropdown v-permission="['system:themes:manage']">
<div
class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100"
>
<IconMore />
</div>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-close-popper
block
type="secondary"
@click="upgradeModal = true"
>
更新
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleReloadThemeSetting"
>
刷新设置表单
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.metadata.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">作者</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.author.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">网站</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme?.spec.website" target="_blank">
{{ selectedTheme?.spec.website }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">源码仓库</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme?.spec.repo" target="_blank">
{{ selectedTheme?.spec.repo }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">当前版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.require }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">存储位置</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.status?.location }}
</dd>
</div>
<!-- TODO: add display required plugins support -->
<div
v-if="false"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">插件依赖</dt>
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
<VAlert description="当前有 1 个插件还未安装" title="提示"></VAlert>
<ul class="mt-2 space-y-2">
<li>
<div
class="inline-flex w-96 cursor-pointer flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<RouterLink
:to="{
name: 'PluginDetail',
params: { name: 'PluginLinks' },
}"
class="font-medium text-gray-900 hover:text-blue-400"
>
run.halo.plugins.links
</RouterLink>
<div class="text-xs">
<VSpace>
<VTag> 已安装</VTag>
</VSpace>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-96 cursor-pointer flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<span class="font-medium hover:text-blue-400">
run.halo.plugins.photos
<Transition appear mode="out-in" name="fade">
<div>
<div class="bg-white px-4 py-4 sm:px-6">
<div class="group flex items-center justify-between">
<div class="flex flex-row items-center gap-3">
<VAvatar
:key="selectedTheme?.metadata.name"
:alt="selectedTheme?.spec.displayName"
:src="selectedTheme?.spec.logo"
size="lg"
/>
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">
{{ selectedTheme?.spec.displayName }}
</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">
{{ selectedTheme?.spec.version }}
</span>
<div class="text-xs">
<VSpace>
<VTag>未安装</VTag>
</VSpace>
</div>
<VTag>
{{ isActivated ? "当前启用" : "未启用" }}
</VTag>
</p>
</div>
</div>
<FloatingDropdown v-permission="['system:themes:manage']">
<div
class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100"
>
<IconMore />
</div>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-close-popper
block
type="secondary"
@click="upgradeModal = true"
>
更新
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleReloadThemeSetting"
>
刷新设置表单
</VButton>
</VSpace>
</div>
</li>
</ul>
</dd>
</template>
</FloatingDropdown>
</div>
</div>
</dl>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.metadata.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">作者</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.author.name }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">网站</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme?.spec.website" target="_blank">
{{ selectedTheme?.spec.website }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">源码仓库</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a :href="selectedTheme?.spec.repo" target="_blank">
{{ selectedTheme?.spec.repo }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">当前版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.spec.require }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">存储位置</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.status?.location }}
</dd>
</div>
<!-- TODO: add display required plugins support -->
<div
v-if="false"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">插件依赖</dt>
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
<VAlert
description="当前有 1 个插件还未安装"
title="提示"
></VAlert>
<ul class="mt-2 space-y-2">
<li>
<div
class="inline-flex w-96 cursor-pointer flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<RouterLink
:to="{
name: 'PluginDetail',
params: { name: 'PluginLinks' },
}"
class="font-medium text-gray-900 hover:text-blue-400"
>
run.halo.plugins.links
</RouterLink>
<div class="text-xs">
<VSpace>
<VTag> 已安装</VTag>
</VSpace>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-96 cursor-pointer flex-col gap-y-3 rounded border p-5 hover:border-primary"
>
<span class="font-medium hover:text-blue-400">
run.halo.plugins.photos
</span>
<div class="text-xs">
<VSpace>
<VTag>未安装</VTag>
</VSpace>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
</div>
</div>
</Transition>
<ThemeUploadModal
v-model:visible="upgradeModal"
:upgrade-theme="selectedTheme"

View File

@ -53,31 +53,36 @@ watch(
);
</script>
<template>
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
<FormKitSchema :schema="formSchema" :data="configMapFormData[group]" />
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
<Transition appear mode="out-in" name="fade">
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
保存
</VButton>
<FormKitSchema
:schema="formSchema"
:data="configMapFormData[group]"
/>
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
>
保存
</VButton>
</div>
</div>
</div>
</div>
</Transition>
</template>

View File

@ -13,6 +13,7 @@ import {
VStatusDot,
VTabItem,
VTabs,
VLoading,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
@ -214,257 +215,270 @@ const handleOpenPreview = (theme: Theme) => {
class="my-[12px] mx-[16px]"
>
<VTabItem id="installed" label="已安装" class="-mx-[16px]">
<VEmpty
v-if="!themes.length && !loading"
message="当前没有已安装的主题,你可以尝试刷新或者安装新主题"
title="当前没有已安装的主题"
>
<template #actions>
<VSpace>
<VButton :loading="loading" @click="handleFetchThemes">
刷新
</VButton>
<VButton
v-permission="['system:themes:manage']"
type="primary"
@click="themeInstall = true"
>
<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="(theme, index) in themes"
:key="index"
@click="handleSelectTheme(theme)"
<VLoading v-if="loading" />
<Transition v-else-if="!themes.length" appear name="fade">
<VEmpty
message="当前没有已安装的主题,你可以尝试刷新或者安装新主题"
title="当前没有已安装的主题"
>
<VEntity
:is-selected="
theme.metadata.name === selectedTheme?.metadata?.name
"
<template #actions>
<VSpace>
<VButton :loading="loading" @click="handleFetchThemes">
刷新
</VButton>
<VButton
v-permission="['system:themes:manage']"
type="primary"
@click="themeInstall = true"
>
<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="(theme, index) in themes"
:key="index"
@click="handleSelectTheme(theme)"
>
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400"
>加载中...</span
>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400">加载异常</span>
</div>
</template>
</LazyImage>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
<template #extra>
<VTag
v-if="
theme.metadata.name === activatedTheme?.metadata?.name
"
>
当前启用
</VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="theme.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:themes:manage'])"
#dropdownItems
<VEntity
:is-selected="
theme.metadata.name === selectedTheme?.metadata?.name
"
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenPreview(theme)"
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400"
>加载中...</span
>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400"
>加载异常</span
>
</div>
</template>
</LazyImage>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
<template #extra>
<VTag
v-if="
theme.metadata.name === activatedTheme?.metadata?.name
"
>
当前启用
</VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="theme.metadata.deletionTimestamp">
<template #description>
<VStatusDot
v-tooltip="`删除中`"
state="warning"
animate
/>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:themes:manage'])"
#dropdownItems
>
预览
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleUninstall(theme)"
>
卸载
</VButton>
<VButton
v-close-popper
:disabled="
theme.metadata.name === activatedTheme?.metadata?.name
"
block
type="danger"
@click="handleUninstall(theme, true)"
>
卸载并删除配置
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenPreview(theme)"
>
预览
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleUninstall(theme)"
>
卸载
</VButton>
<VButton
v-close-popper
:disabled="
theme.metadata.name === activatedTheme?.metadata?.name
"
block
type="danger"
@click="handleUninstall(theme, true)"
>
卸载并删除配置
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
</VTabItem>
<VTabItem id="uninstalled" label="未安装" class="-mx-[16px]">
<VEmpty v-if="!themes.length && !loading" title="当前没有未安装的主题">
<template #actions>
<VSpace>
<VButton :loading="loading" @click="handleFetchThemes">
刷新
</VButton>
</VSpace>
</template>
</VEmpty>
<ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(theme, index) in themes" :key="index">
<VEntity>
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
<VLoading v-if="loading" />
<Transition v-else-if="!themes.length" appear name="fade">
<VEmpty title="当前没有未安装的主题">
<template #actions>
<VSpace>
<VButton :loading="loading" @click="handleFetchThemes">
刷新
</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="(theme, index) in themes" :key="index">
<VEntity>
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400"
>加载中...</span
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400">加载异常</span>
</div>
</template>
</LazyImage>
<span class="text-xs text-gray-400"
>加载中...</span
>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400"
>加载异常</span
>
</div>
</template>
</LazyImage>
</div>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
<VEntityField v-permission="['system:themes:manage']">
<template #description>
<VButton
size="sm"
:disabled="creating"
@click="handleCreateTheme(theme)"
>
安装
</VButton>
</template>
</VEntityField>
</template>
</VEntity>
</li>
</ul>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
<VEntityField v-permission="['system:themes:manage']">
<template #description>
<VButton
size="sm"
:disabled="creating"
@click="handleCreateTheme(theme)"
>
安装
</VButton>
</template>
</VEntityField>
</template>
</VEntity>
</li>
</ul>
</Transition>
</VTabItem>
</VTabs>

View File

@ -24,6 +24,7 @@ import {
VPageHeader,
VSpace,
VTabbar,
VLoading,
} from "@halo-dev/components";
import ThemeListModal from "../components/ThemeListModal.vue";
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
@ -224,15 +225,13 @@ onMounted(() => {
></VTabbar>
</template>
</VCard>
<div>
<div class="bg-white">
<RouterView :key="activeTab" v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
<template #fallback>
<div class="flex h-32 w-full justify-center bg-white">
<span class="text-sm text-gray-600">加载中...</span>
</div>
<VLoading />
</template>
</Suspense>
</template>

View File

@ -59,166 +59,175 @@ watchEffect(() => {
</script>
<template>
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
<Transition appear mode="out-in" name="fade">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">插件信息</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">{{ plugin?.spec.version }}</span>
<VTag>
{{ isStarted ? "已启用" : "未启用" }}
</VTag>
</p>
</div>
<div v-permission="['system:plugins:manage']">
<VSwitch :model-value="isStarted" @change="changeStatus" />
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">名称</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.displayName }}
</dd>
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900">插件信息</h3>
<p class="mt-1 flex max-w-2xl items-center gap-2">
<span class="text-sm text-gray-500">{{
plugin?.spec.version
}}</span>
<VTag>
{{ isStarted ? "已启用" : "未启用" }}
</VTag>
</p>
</div>
<div v-permission="['system:plugins:manage']">
<VSwitch :model-value="isStarted" @change="changeStatus" />
</div>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">描述</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.description }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.requires }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">提供方</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<a :href="plugin?.spec.homepage" target="_blank">
{{ plugin?.spec.author }}
</a>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">协议</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul
v-if="plugin?.spec.license && plugin?.spec.license.length"
class="list-inside"
:class="{ 'list-disc': plugin?.spec.license.length > 1 }"
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<li v-for="(license, index) in plugin.spec.license" :key="index">
<a v-if="license.url" :href="license.url" target="_blank">
{{ license.name }}
<dt class="text-sm font-medium text-gray-900">名称</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.displayName }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">描述</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.description }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">版本</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.version }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ plugin?.spec.requires }}
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">提供方</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<a :href="plugin?.spec.homepage" target="_blank">
{{ plugin?.spec.author }}
</a>
<span>
{{ license.name }}
</span>
</li>
</ul>
</dd>
</div>
<!-- TODO add display extensions support -->
<div
v-if="false"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">模型定义</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<span></span>
</dd>
</div>
<div
:class="`${
pluginRoleTemplateGroups.length ? 'bg-gray-50' : 'bg-white'
}`"
class="px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">权限模板</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-5 sm:mt-0">
<dl
v-if="pluginRoleTemplateGroups.length"
class="divide-y divide-gray-100"
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<div
v-for="(group, groupIndex) in pluginRoleTemplateGroups"
:key="groupIndex"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ group.module }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary"
>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}}
</span>
<span
v-if="
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
"
class="text-xs text-gray-400"
<dt class="text-sm font-medium text-gray-900">协议</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul
v-if="plugin?.spec.license && plugin?.spec.license.length"
class="list-inside"
:class="{ 'list-disc': plugin?.spec.license.length > 1 }"
>
<li
v-for="(license, index) in plugin.spec.license"
:key="index"
>
<a v-if="license.url" :href="license.url" target="_blank">
{{ license.name }}
</a>
<span>
{{ license.name }}
</span>
</li>
</ul>
</dd>
</div>
<!-- TODO add display extensions support -->
<div
v-if="false"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">模型定义</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<span></span>
</dd>
</div>
<div
:class="`${
pluginRoleTemplateGroups.length ? 'bg-gray-50' : 'bg-white'
}`"
class="px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">权限模板</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-5 sm:mt-0">
<dl
v-if="pluginRoleTemplateGroups.length"
class="divide-y divide-gray-100"
>
<div
v-for="(group, groupIndex) in pluginRoleTemplateGroups"
:key="groupIndex"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ group.module }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary"
>
依赖于
{{
JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", ")
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else></span>
</dd>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
]
}}
</span>
<span
v-if="
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
"
class="text-xs text-gray-400"
>
依赖于
{{
JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", ")
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else></span>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">最近一次启动</dt>
<dd
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-2 sm:mt-0"
>
{{ formatDatetime(plugin?.status?.lastStartTime) }}
</dd>
</div>
</dl>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">最近一次启动</dt>
<dd
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-2 sm:mt-0"
>
{{ formatDatetime(plugin?.status?.lastStartTime) }}
</dd>
</div>
</dl>
</div>
</div>
</Transition>
</template>

View File

@ -10,6 +10,7 @@ import {
VPageHeader,
VPagination,
VSpace,
VLoading,
} from "@halo-dev/components";
import PluginListItem from "./components/PluginListItem.vue";
import PluginUploadModal from "./components/PluginUploadModal.vue";
@ -310,37 +311,41 @@ function handleClearFilters() {
</div>
</template>
<VEmpty
v-if="!plugins.total && !loading"
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
title="当前没有已安装的插件"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPlugins"></VButton>
<VButton
v-permission="['system:plugins:manage']"
type="secondary"
@click="pluginInstall = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
安装插件
</VButton>
</VSpace>
</template>
</VEmpty>
<VLoading v-if="loading" />
<ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(plugin, index) in plugins.items" :key="index">
<PluginListItem :plugin="plugin" @reload="handleFetchPlugins" />
</li>
</ul>
<Transition v-else-if="!plugins.total" appear name="fade">
<VEmpty
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
title="当前没有已安装的插件"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPlugins"></VButton>
<VButton
v-permission="['system:plugins:manage']"
type="secondary"
@click="pluginInstall = true"
>
<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="(plugin, index) in plugins.items" :key="index">
<PluginListItem :plugin="plugin" @reload="handleFetchPlugins" />
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -62,31 +62,36 @@ const handleFetchPlugin = async () => {
await handleFetchPlugin();
</script>
<template>
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
<FormKitSchema :schema="formSchema" :data="configMapFormData[group]" />
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
<Transition appear mode="out-in" name="fade">
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
保存
</VButton>
<FormKitSchema
:schema="formSchema"
:data="configMapFormData[group]"
/>
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
>
保存
</VButton>
</div>
</div>
</div>
</div>
</Transition>
</template>

View File

@ -17,6 +17,7 @@ import {
VPageHeader,
VTabbar,
VAvatar,
VLoading,
} from "@halo-dev/components";
import BasicLayout from "@/layouts/BasicLayout.vue";
@ -172,15 +173,13 @@ watch([() => route.name, () => route.params], () => {
></VTabbar>
</template>
</VCard>
<div>
<div class="bg-white">
<RouterView :key="activeTab" v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
<template #fallback>
<div class="flex h-32 w-full justify-center bg-white">
<span class="text-sm text-gray-600">加载中...</span>
</div>
<VLoading />
</template>
</Suspense>
</template>

View File

@ -12,12 +12,12 @@ import {
VButton,
VCard,
VPageHeader,
VPagination,
VSpace,
VTag,
VStatusDot,
VEntity,
VEntityField,
VLoading,
} from "@halo-dev/components";
import RoleEditingModal from "./components/RoleEditingModal.vue";
@ -39,7 +39,7 @@ const { currentUserHasPermission } = usePermission();
const editingModal = ref<boolean>(false);
const selectedRole = ref<Role>();
const { roles, handleFetchRoles } = useFetchRole();
const { roles, handleFetchRoles, loading } = useFetchRole();
let fuse: Fuse<Role> | undefined = undefined;
@ -228,16 +228,21 @@ const handleDelete = async (role: Role) => {
</div>
</div>
</template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(role, index) in searchResults" :key="index">
<VEntity>
<template #start>
<VEntityField
:title="
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name
"
:description="`包含
<VLoading v-if="loading" />
<Transition v-else appear name="fade">
<ul
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(role, index) in searchResults" :key="index">
<VEntity>
<template #start>
<VEntityField
:title="
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name
"
:description="`包含
${
JSON.parse(
role.metadata.annotations?.[
@ -246,67 +251,62 @@ const handleDelete = async (role: Role) => {
).length
}
个权限`"
:route="{
name: 'RoleDetail',
params: {
name: role.metadata.name,
},
}"
></VEntityField>
</template>
<template #end>
<VEntityField v-if="role.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField description="0 个用户" />
<VEntityField>
<template #description>
<VTag> 系统保留</VTag>
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(role.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:roles:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenEditingModal(role)"
:route="{
name: 'RoleDetail',
params: {
name: role.metadata.name,
},
}"
></VEntityField>
</template>
<template #end>
<VEntityField v-if="role.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField description="0 个用户" />
<VEntityField>
<template #description>
<VTag> 系统保留</VTag>
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(role.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:roles:manage'])"
#dropdownItems
>
编辑
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(role)"
>
删除
</VButton>
<VButton v-close-popper block @click="handleCloneRole(role)">
基于此角色创建
</VButton>
</template>
</VEntity>
</li>
</ul>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">
<VPagination :page="1" :size="10" :total="20" />
</div>
</template>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenEditingModal(role)"
>
编辑
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleDelete(role)"
>
删除
</VButton>
<VButton v-close-popper block @click="handleCloneRole(role)">
基于此角色创建
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
</VCard>
</div>
</template>

View File

@ -33,31 +33,36 @@ await handleFetchSettings();
await handleFetchConfigMap();
</script>
<template>
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
<FormKitSchema :schema="formSchema" :data="configMapFormData[group]" />
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
<Transition appear mode="out-in" name="fade">
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
:id="group"
v-model="configMapFormData[group]"
:name="group"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveConfigMap"
>
保存
</VButton>
<FormKitSchema
:schema="formSchema"
:data="configMapFormData[group]"
/>
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(group || '')"
>
保存
</VButton>
</div>
</div>
</div>
</div>
</Transition>
</template>

View File

@ -14,6 +14,7 @@ import {
VPageHeader,
VTabbar,
IconSettings,
VLoading,
} from "@halo-dev/components";
import type { SettingForm } from "@halo-dev/api-client";
@ -109,15 +110,13 @@ watch([() => route.name, () => route.params], async () => {
></VTabbar>
</template>
</VCard>
<div>
<div class="bg-white">
<RouterView :key="activeTab" v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
<template #fallback>
<div class="flex h-32 w-full justify-center bg-white">
<span class="text-sm text-gray-600">加载中...</span>
</div>
<VLoading />
</template>
</Suspense>
</template>

View File

@ -15,6 +15,7 @@ import {
VEntityField,
Dialog,
VStatusDot,
VLoading,
} from "@halo-dev/components";
import UserEditingModal from "./components/UserEditingModal.vue";
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
@ -47,6 +48,7 @@ const users = ref<UserList>({
hasPrevious: false,
totalPages: 0,
});
const loading = ref(false);
const selectedUserNames = ref<string[]>([]);
const selectedUser = ref<User>();
@ -56,6 +58,8 @@ let fuse: Fuse<User> | undefined = undefined;
const handleFetchUsers = async () => {
try {
loading.value = true;
const { data } = await apiClient.extension.user.listv1alpha1User({
page: users.value.page,
size: users.value.size,
@ -70,6 +74,7 @@ const handleFetchUsers = async () => {
console.error("Failed to fetch users", e);
} finally {
selectedUser.value = undefined;
loading.value = false;
}
};
@ -385,111 +390,117 @@ onMounted(() => {
</div>
</div>
</template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(user, index) in searchResults" :key="index">
<VEntity :is-selected="checkSelection(user)">
<template
v-if="currentUserHasPermission(['system:users:manage'])"
#checkbox
>
<input
v-model="selectedUserNames"
:value="user.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField>
<template #description>
<VAvatar
:alt="user.spec.displayName"
:src="user.spec.avatar"
size="md"
></VAvatar>
</template>
</VEntityField>
<VEntityField
:title="user.spec.displayName"
:description="user.metadata.name"
:route="{
name: 'UserDetail',
params: { name: user.metadata.name },
}"
/>
</template>
<template #end>
<VEntityField>
<template #description>
<div
v-for="(role, roleIndex) in getRoles(user)"
:key="roleIndex"
class="flex items-center"
>
<VTag>
{{ role }}
</VTag>
</div>
</template>
</VEntityField>
<VEntityField v-if="user.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(user.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:users:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenCreateModal(user)"
<VLoading v-if="loading" />
<Transition v-else appear name="fade">
<ul
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(user, index) in searchResults" :key="index">
<VEntity :is-selected="checkSelection(user)">
<template
v-if="currentUserHasPermission(['system:users:manage'])"
#checkbox
>
修改资料
</VButton>
<VButton
v-close-popper
block
@click="handleOpenPasswordChangeModal(user)"
<input
v-model="selectedUserNames"
:value="user.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
/>
</template>
<template #start>
<VEntityField>
<template #description>
<VAvatar
:alt="user.spec.displayName"
:src="user.spec.avatar"
size="md"
></VAvatar>
</template>
</VEntityField>
<VEntityField
:title="user.spec.displayName"
:description="user.metadata.name"
:route="{
name: 'UserDetail',
params: { name: user.metadata.name },
}"
/>
</template>
<template #end>
<VEntityField>
<template #description>
<div
v-for="(role, roleIndex) in getRoles(user)"
:key="roleIndex"
class="flex items-center"
>
<VTag>
{{ role }}
</VTag>
</div>
</template>
</VEntityField>
<VEntityField v-if="user.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(user.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:users:manage'])"
#dropdownItems
>
修改密码
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
@click="handleOpenGrantPermissionModal(user)"
>
分配角色
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
type="danger"
@click="handleDelete(user)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenCreateModal(user)"
>
修改资料
</VButton>
<VButton
v-close-popper
block
@click="handleOpenPasswordChangeModal(user)"
>
修改密码
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
@click="handleOpenGrantPermissionModal(user)"
>
分配角色
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
type="danger"
@click="handleDelete(user)"
>
删除
</VButton>
</template>
</VEntity>
</li>
</ul>
</Transition>
<template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end">

View File

@ -1,3 +1,4 @@
import "@halo-dev/richtext-editor/dist/style.css";
import "@halo-dev/components/dist/style.css";
import "@/styles/tailwind.css";
import "@/styles/index.css";

18
src/styles/index.css Normal file
View File

@ -0,0 +1,18 @@
.fade-enter-active {
@apply duration-200 ease-out;
}
.fade-enter-from {
@apply opacity-0;
}
.fade-enter-to {
@apply opacity-100;
}
.fade-leave-active {
@apply duration-200 ease-in;
}
.fade-leave-from {
@apply opacity-100;
}
.fade-leave-to {
@apply opacity-0;
}