feat: add the entity component as a list item (halo-dev/console#609)

#### What type of PR is this?

/kind feature
/milestone 2.0

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

添加 Entity 和 EntityField 组件,作为列表项使用。

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

#### Screenshots:

#### Special notes for your reviewer:

测试方式:检查后台各个页面的列表样式和功能是否正常。

/cc @halo-dev/sig-halo-admin 

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

```release-note
None
```
pull/3445/head
Ryan Wang 2022-09-13 12:08:12 +08:00 committed by GitHub
parent 0a4d31fa33
commit bcfe7a52ee
13 changed files with 732 additions and 909 deletions

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { IconMore, VSpace } from "@halo-dev/components";
withDefaults(
defineProps<{
isSelected?: boolean;
}>(),
{
isSelected: false,
}
);
</script>
<template>
<div
:class="{
'bg-gray-100': isSelected,
}"
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
>
<div
v-show="isSelected"
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
></div>
<slot name="prepend" />
<div class="relative flex flex-row items-center">
<div v-if="$slots.checkbox" class="mr-4 hidden items-center sm:flex">
<slot name="checkbox" />
</div>
<div class="flex flex-1 items-center gap-4">
<slot name="start" />
</div>
<div
class="flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<slot name="end" />
</div>
<div v-if="$slots.menuItems" class="ml-4 inline-flex items-center">
<FloatingDropdown>
<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">
<slot name="menuItems"></slot>
</VSpace>
</div>
</template>
</FloatingDropdown>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import type { RouteLocationRaw } from "vue-router";
withDefaults(
defineProps<{
title?: string;
description?: string;
route?: RouteLocationRaw;
}>(),
{
title: undefined,
description: undefined,
route: undefined,
}
);
const emit = defineEmits<{
(event: "click"): void;
}>();
</script>
<template>
<div class="inline-flex flex-col gap-1">
<div
v-if="title || $slots.title"
class="inline-flex flex-col items-center sm:flex-row"
>
<slot name="title">
<div
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
@click="emit('click')"
>
<RouterLink v-if="route" :to="route">
{{ title }}
</RouterLink>
<span v-else>
{{ title }}
</span>
</div>
<slot name="extra" />
</slot>
</div>
<div
v-if="description || $slots.description"
class="inline-flex items-center"
>
<slot name="description">
<span v-if="description" class="text-xs text-gray-500">
{{ description }}
</span>
</slot>
</div>
</div>
</template>

View File

@ -7,7 +7,6 @@ import {
IconDatabase2Line, IconDatabase2Line,
IconGrid, IconGrid,
IconList, IconList,
IconSettings,
IconUpload, IconUpload,
VButton, VButton,
VCard, VCard,
@ -36,6 +35,8 @@ import cloneDeep from "lodash.clonedeep";
import { isImage } from "@/utils/image"; import { isImage } from "@/utils/image";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { useFetchAttachmentGroup } from "./composables/use-attachment-group"; import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
const policyVisible = ref(false); const policyVisible = ref(false);
const uploadVisible = ref(false); const uploadVisible = ref(false);
@ -555,47 +556,35 @@ onMounted(() => {
role="list" role="list"
> >
<li v-for="(attachment, index) in attachments.items" :key="index"> <li v-for="(attachment, index) in attachments.items" :key="index">
<div <Entity :is-selected="isChecked(attachment)">
:class="{ <template #checkbox>
'bg-gray-100': isChecked(attachment), <input
}" :checked="selectedAttachments.has(attachment)"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
> type="checkbox"
<div @click="handleSelect(attachment)"
v-show="isChecked(attachment)" />
class="absolute inset-y-0 left-0 w-0.5 bg-primary" </template>
></div> <template #start>
<div class="relative flex flex-row items-center"> <EntityField>
<div class="mr-4 hidden items-center sm:flex"> <template #description>
<input <div
:checked="selectedAttachments.has(attachment)" class="h-10 w-10 rounded border bg-white p-1 hover:shadow-sm"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
@click="handleSelect(attachment)"
/>
</div>
<div class="mr-4">
<div
class="h-12 w-12 rounded border bg-white p-1 hover:shadow-sm"
>
<AttachmentFileTypeIcon
:display-ext="false"
:file-name="attachment.spec.displayName"
:width="8"
:height="8"
/>
</div>
</div>
<div class="flex-1">
<div class="flex flex-col sm:flex-row">
<span
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
@click="handleClickItem(attachment)"
> >
{{ attachment.spec.displayName }} <AttachmentFileTypeIcon
</span> :display-ext="false"
</div> :file-name="attachment.spec.displayName"
<div class="mt-1 flex"> :width="8"
:height="8"
/>
</div>
</template>
</EntityField>
<EntityField
:title="attachment.spec.displayName"
@click="handleClickItem(attachment)"
>
<template #description>
<VSpace> <VSpace>
<span class="text-xs text-gray-500"> <span class="text-xs text-gray-500">
{{ attachment.spec.mediaType }} {{ attachment.spec.mediaType }}
@ -604,54 +593,50 @@ onMounted(() => {
{{ prettyBytes(attachment.spec.size || 0) }} {{ prettyBytes(attachment.spec.size || 0) }}
</span> </span>
</VSpace> </VSpace>
</div> </template>
</div> </EntityField>
<div class="flex"> </template>
<div <template #end>
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6" <EntityField
> :description="
<span class="text-sm text-gray-500"> getPolicyName(attachment.spec.policyRef?.name)
{{ getPolicyName(attachment.spec.policyRef?.name) }} "
</span> />
<EntityField>
<template #description>
<RouterLink <RouterLink
:to="{ :to="{
name: 'UserDetail', name: 'UserDetail',
params: { name: attachment.spec.uploadedBy?.name }, params: { name: attachment.spec.uploadedBy?.name },
}" }"
class="text-xs text-gray-500"
> >
<span class="text-sm text-gray-500"> {{ attachment.spec.uploadedBy?.name }}
{{ attachment.spec.uploadedBy?.name }}
</span>
</RouterLink> </RouterLink>
<FloatingTooltip </template>
v-if="attachment.metadata.deletionTimestamp" </EntityField>
class="hidden items-center sm:flex" <EntityField v-if="attachment.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
> >
<div <span
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600" class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
> ></span>
<span </div>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600" </template>
></span> </EntityField>
</div> <EntityField
<template #popper> 删除中</template> :description="
</FloatingTooltip> formatDatetime(attachment.metadata.creationTimestamp)
<time class="text-sm text-gray-500"> "
{{ />
formatDatetime( </template>
attachment.metadata.creationTimestamp <template #menuItems>
) <VButton v-close-popper block type="danger"> 删除 </VButton>
}} </template>
</time> </Entity>
<span class="cursor-pointer">
<IconSettings
@click.stop="handleClickItem(attachment)"
/>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -3,6 +3,8 @@ import { ref } from "vue";
import { VEmpty, VSpace, VButton, IconAddCircle } from "@halo-dev/components"; import { VEmpty, VSpace, VButton, IconAddCircle } from "@halo-dev/components";
import type { PagesPublicState } from "@halo-dev/admin-shared"; import type { PagesPublicState } from "@halo-dev/admin-shared";
import { useExtensionPointsState } from "@/composables/usePlugins"; import { useExtensionPointsState } from "@/composables/usePlugins";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
const pagesPublicState = ref<PagesPublicState>({ const pagesPublicState = ref<PagesPublicState>({
functionalPages: [], functionalPages: [],
@ -38,27 +40,15 @@ useExtensionPointsState("PAGES", pagesPublicState);
:key="index" :key="index"
v-permission="page.permissions" v-permission="page.permissions"
> >
<div <Entity>
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" <template #start>
> <EntityField
<div class="relative flex flex-row items-center"> :title="page.name"
<div class="flex-1"> :route="page.path"
<div class="flex"> :description="page.url"
<RouterLink ></EntityField>
:to="page.path" </template>
class="truncate text-sm font-medium text-gray-900" </Entity>
>
{{ page.name }}
</RouterLink>
</div>
<div class="mt-1 flex">
<span class="text-xs text-gray-500">
{{ page.url }}
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>
</template> </template>

View File

@ -3,7 +3,6 @@ import {
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconSettings,
IconEye, IconEye,
IconEyeOff, IconEyeOff,
IconTeam, IconTeam,
@ -30,6 +29,8 @@ import { apiClient } from "@halo-dev/admin-shared";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import Entity from "../../../components/entity/Entity.vue";
import EntityField from "../../../components/entity/EntityField.vue";
enum SinglePagePhase { enum SinglePagePhase {
DRAFT = "未发布", DRAFT = "未发布",
@ -332,68 +333,47 @@ const handleSelectUser = (user?: User) => {
role="list" role="list"
> >
<li v-for="(singlePage, index) in singlePages.items" :key="index"> <li v-for="(singlePage, index) in singlePages.items" :key="index">
<div <Entity :is-selected="checkAll">
:class="{ <template #checkbox>
'bg-gray-100': checkAll, <input
}" v-model="checkAll"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
> type="checkbox"
<div />
v-show="checkAll" </template>
class="absolute inset-y-0 left-0 w-0.5 bg-primary" <template #start>
></div> <EntityField
<div class="relative flex flex-row items-center"> :title="singlePage.page.spec.title"
<div class="mr-4 hidden items-center sm:flex"> :description="singlePage.page.status?.permalink"
<input :route="{
v-model="checkAll" name: 'SinglePageEditor',
class="h-4 w-4 rounded border-gray-300 text-indigo-600" query: { name: singlePage.page.metadata.name },
type="checkbox" }"
/> >
</div> <template #extra>
<div class="flex-1">
<div class="flex flex-row items-center">
<RouterLink <RouterLink
v-if="singlePage.page.status?.inProgress"
v-tooltip="`当前有内容已保存,但还未发布。`"
:to="{ :to="{
name: 'SinglePageEditor', name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name }, query: { name: singlePage.page.metadata.name },
}" }"
class="flex items-center"
> >
<span class="truncate text-sm font-medium text-gray-900"> <div
{{ singlePage.page.spec.title }} class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600"
</span>
</RouterLink>
<FloatingTooltip
v-if="singlePage.page.status?.inProgress"
class="hidden items-center sm:flex"
>
<RouterLink
:to="{
name: 'SinglePageEditor',
query: { name: singlePage.page.metadata.name },
}"
class="flex items-center"
> >
<div <span
class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600" class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600"
> ></span>
<span </div>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600" </RouterLink>
></span> </template>
</div> </EntityField>
</RouterLink> </template>
<template #popper> 当前有内容已保存但还未发布 </template> <template #end>
</FloatingTooltip> <EntityField>
</div> <template #description>
<div class="mt-1 flex">
<span class="text-xs text-gray-500">
{{ singlePage.page.status?.permalink }}
</span>
</div>
</div>
<div class="flex">
<div
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<RouterLink <RouterLink
v-for="( v-for="(
contributor, contributorIndex contributor, contributorIndex
@ -413,64 +393,53 @@ const handleSelectUser = (user?: User) => {
circle circle
></VAvatar> ></VAvatar>
</RouterLink> </RouterLink>
<span class="text-sm text-gray-500"> </template>
{{ finalStatus(singlePage.page) }} </EntityField>
</span> <EntityField :description="finalStatus(singlePage.page)" />
<span> <EntityField>
<IconEye <template #description>
v-if="singlePage.page.spec.visible === 'PUBLIC'" <IconEye
v-tooltip="`公开访问`" v-if="singlePage.page.spec.visible === 'PUBLIC'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`公开访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
<IconEyeOff />
v-if="singlePage.page.spec.visible === 'PRIVATE'" <IconEyeOff
v-tooltip="`私有访问`" v-if="singlePage.page.spec.visible === 'PRIVATE'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`私有访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
<IconTeam />
v-if="singlePage.page.spec.visible === 'INTERNAL'" <IconTeam
v-tooltip="`内部成员可访问`" v-if="singlePage.page.spec.visible === 'INTERNAL'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`内部成员可访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
</span> />
<time class="text-sm text-gray-500"> </template>
{{ </EntityField>
formatDatetime(singlePage.page.metadata.creationTimestamp) <EntityField
}} :description="
</time> formatDatetime(singlePage.page.metadata.creationTimestamp)
<span> "
<FloatingDropdown> />
<IconSettings </template>
class="cursor-pointer transition-all hover:text-blue-600" <template #menuItems>
/> <VButton
<template #popper> v-close-popper
<div class="w-48 p-2"> block
<VSpace class="w-full" direction="column"> type="secondary"
<VButton @click="handleOpenSettingModal(singlePage.page)"
v-close-popper >
block 设置
type="secondary" </VButton>
@click="handleOpenSettingModal(singlePage.page)" <VButton
> v-close-popper
设置 block
</VButton> type="danger"
<VButton @click="handleDelete(singlePage.page)"
v-close-popper >
block 删除
type="danger" </VButton>
@click="handleDelete(singlePage.page)" </template>
> </Entity>
删除
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>

View File

@ -7,7 +7,6 @@ import {
IconBookRead, IconBookRead,
IconEye, IconEye,
IconEyeOff, IconEyeOff,
IconSettings,
IconTeam, IconTeam,
IconCloseCircle, IconCloseCircle,
useDialog, useDialog,
@ -21,6 +20,8 @@ import {
} from "@halo-dev/components"; } from "@halo-dev/components";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue"; import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue"; import PostSettingModal from "./components/PostSettingModal.vue";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
import PostTag from "../posts/tags/components/PostTag.vue"; import PostTag from "../posts/tags/components/PostTag.vue";
import { onMounted, ref, watch, watchEffect } from "vue"; import { onMounted, ref, watch, watchEffect } from "vue";
import type { import type {
@ -738,64 +739,43 @@ function handleContributorFilterItemChange(user?: User) {
role="list" role="list"
> >
<li v-for="(post, index) in posts.items" :key="index"> <li v-for="(post, index) in posts.items" :key="index">
<div <Entity :is-selected="checkSelection(post.post)">
:class="{ <template #checkbox>
'bg-gray-100': checkSelection(post.post), <input
}" v-model="selectedPostNames"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" :value="post.post.metadata.name"
> class="h-4 w-4 rounded border-gray-300 text-indigo-600"
<div name="post-checkbox"
v-show="checkSelection(post.post)" type="checkbox"
class="absolute inset-y-0 left-0 w-0.5 bg-primary" />
></div> </template>
<div class="relative flex flex-row items-center"> <template #start>
<div class="mr-4 hidden items-center sm:flex"> <EntityField
<input :title="post.post.spec.title"
v-model="selectedPostNames" :route="{
:value="post.post.metadata.name" name: 'PostEditor',
class="h-4 w-4 rounded border-gray-300 text-indigo-600" query: { name: post.post.metadata.name },
name="post-checkbox" }"
type="checkbox" >
/> <template #extra>
</div>
<div class="flex-1">
<div class="flex flex-col items-center sm:flex-row">
<RouterLink
:to="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
>
<span
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
>
{{ post.post.spec.title }}
</span>
</RouterLink>
<VSpace class="mt-1 sm:mt-0"> <VSpace class="mt-1 sm:mt-0">
<FloatingTooltip <RouterLink
v-if="post.post.status?.inProgress" v-if="post.post.status?.inProgress"
class="hidden items-center sm:flex" v-tooltip="`当前有内容已保存,但还未发布。`"
:to="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
class="flex items-center"
> >
<RouterLink <div
:to="{ class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600"
name: 'PostEditor',
query: { name: post.post.metadata.name },
}"
class="flex items-center"
> >
<div <span
class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600" class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600"
> ></span>
<span </div>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600" </RouterLink>
></span>
</div>
</RouterLink>
<template #popper>
当前有内容已保存但还未发布
</template>
</FloatingTooltip>
<PostTag <PostTag
v-for="(tag, tagIndex) in post.tags" v-for="(tag, tagIndex) in post.tags"
:key="tagIndex" :key="tagIndex"
@ -803,8 +783,8 @@ function handleContributorFilterItemChange(user?: User) {
route route
></PostTag> ></PostTag>
</VSpace> </VSpace>
</div> </template>
<div class="mt-1 flex"> <template #description>
<VSpace> <VSpace>
<p <p
v-if="post.categories.length" v-if="post.categories.length"
@ -821,12 +801,12 @@ function handleContributorFilterItemChange(user?: User) {
<span class="text-xs text-gray-500">访问量 0</span> <span class="text-xs text-gray-500">访问量 0</span>
<span class="text-xs text-gray-500"> 评论 0 </span> <span class="text-xs text-gray-500"> 评论 0 </span>
</VSpace> </VSpace>
</div> </template>
</div> </EntityField>
<div class="flex"> </template>
<div <template #end>
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6" <EntityField>
> <template #description>
<RouterLink <RouterLink
v-for="(contributor, contributorIndex) in post.contributors" v-for="(contributor, contributorIndex) in post.contributors"
:key="contributorIndex" :key="contributorIndex"
@ -844,62 +824,53 @@ function handleContributorFilterItemChange(user?: User) {
circle circle
></VAvatar> ></VAvatar>
</RouterLink> </RouterLink>
<span class="text-sm text-gray-500"> </template>
{{ finalStatus(post.post) }} </EntityField>
</span> <EntityField :description="finalStatus(post.post)"></EntityField>
<span> <EntityField>
<IconEye <template #description>
v-if="post.post.spec.visible === 'PUBLIC'" <IconEye
v-tooltip="`公开访问`" v-if="post.post.spec.visible === 'PUBLIC'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`公开访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
<IconEyeOff />
v-if="post.post.spec.visible === 'PRIVATE'" <IconEyeOff
v-tooltip="`私有访问`" v-if="post.post.spec.visible === 'PRIVATE'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`私有访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
<IconTeam />
v-if="post.post.spec.visible === 'INTERNAL'" <IconTeam
v-tooltip="`内部成员可访问`" v-if="post.post.spec.visible === 'INTERNAL'"
class="cursor-pointer text-sm transition-all hover:text-blue-600" v-tooltip="`内部成员可访问`"
/> class="cursor-pointer text-sm transition-all hover:text-blue-600"
</span> />
<time class="text-sm text-gray-500"> </template>
{{ formatDatetime(post.post.metadata.creationTimestamp) }} </EntityField>
</time> <EntityField
<span> :description="
<FloatingDropdown> formatDatetime(post.post.metadata.creationTimestamp)
<IconSettings "
class="cursor-pointer transition-all hover:text-blue-600" />
/> </template>
<template #popper> <template #menuItems>
<div class="w-48 p-2"> <VButton
<VSpace class="w-full" direction="column"> v-close-popper
<VButton block
v-close-popper type="secondary"
block @click="handleOpenSettingModal(post.post)"
type="secondary" >
@click="handleOpenSettingModal(post.post)" 设置
> </VButton>
设置 <VButton
</VButton> v-close-popper
<VButton block
v-close-popper type="danger"
block @click="handleDelete(post.post)"
type="danger" >
@click="handleDelete(post.post)" 删除
> </VButton>
删除 </template>
</VButton> </Entity>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>

View File

@ -1,9 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { IconList, IconSettings, VButton, VSpace } from "@halo-dev/components"; import { IconList, VButton } from "@halo-dev/components";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import type { CategoryTree } from "../utils"; import type { CategoryTree } from "../utils";
import { ref } from "vue"; import { ref } from "vue";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
withDefaults( withDefaults(
defineProps<{ defineProps<{
@ -49,86 +51,59 @@ function onDelete(category: CategoryTree) {
> >
<template #item="{ element: category }"> <template #item="{ element: category }">
<li> <li>
<div <Entity>
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" <template #prepend>
> <div
<div class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex" >
> <IconList class="h-3.5 w-3.5" />
<IconList class="h-3.5 w-3.5" />
</div>
<div class="relative flex flex-row items-center">
<div class="flex-1">
<div class="flex flex-col sm:flex-row">
<span
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
>
{{ category.spec.displayName }}
</span>
<VSpace class="mt-1 sm:mt-0"></VSpace>
</div>
<div class="mt-1 flex">
<span class="text-xs text-gray-500">
{{ category.status.permalink }}
</span>
</div>
</div> </div>
<div class="flex"> </template>
<div <template #start>
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6" <EntityField
> :title="category.spec.displayName"
<FloatingTooltip :description="category.status.permalink"
v-if="category.metadata.deletionTimestamp" />
class="mr-4 hidden items-center sm:flex" </template>
> <template #end>
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"> <EntityField v-if="category.metadata.deletionTimestamp">
<span <template #description>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<template #popper> 删除中</template>
</FloatingTooltip>
<div <div
class="cursor-pointer text-sm text-gray-500 hover:text-gray-900" v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
> >
{{ category.status?.posts?.length || 0 }} 篇文章 <span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div> </div>
<time class="text-sm text-gray-500"> </template>
{{ formatDatetime(category.metadata.creationTimestamp) }} </EntityField>
</time> <EntityField
<span class="self-center"> :description="`${category.status?.posts?.length || 0} 篇文章`"
<FloatingDropdown> />
<IconSettings <EntityField
class="cursor-pointer transition-all hover:text-blue-600" :description="formatDatetime(category.metadata.creationTimestamp)"
/> />
<template #popper> </template>
<div class="w-48 p-2"> <template #menuItems>
<VSpace class="w-full" direction="column"> <VButton
<VButton v-close-popper
v-close-popper block
block type="secondary"
type="secondary" @click="onOpenEditingModal(category)"
@click="onOpenEditingModal(category)" >
> 修改
修改 </VButton>
</VButton> <VButton
<VButton v-close-popper
v-close-popper block
block type="danger"
type="danger" @click="onDelete(category)"
@click="onDelete(category)" >
> 删除
删除 </VButton>
</VButton> </template>
</VSpace> </Entity>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
<CategoryListItem <CategoryListItem
:categories="category.spec.children" :categories="category.spec.children"
class="pl-10 transition-all duration-300" class="pl-10 transition-all duration-300"

View File

@ -8,7 +8,6 @@ import {
IconBookRead, IconBookRead,
IconGrid, IconGrid,
IconList, IconList,
IconSettings,
VButton, VButton,
VCard, VCard,
VEmpty, VEmpty,
@ -17,6 +16,8 @@ import {
} from "@halo-dev/components"; } from "@halo-dev/components";
import TagEditingModal from "./components/TagEditingModal.vue"; import TagEditingModal from "./components/TagEditingModal.vue";
import PostTag from "./components/PostTag.vue"; import PostTag from "./components/PostTag.vue";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
// types // types
import type { Tag } from "@halo-dev/api-client"; import type { Tag } from "@halo-dev/api-client";
@ -173,85 +174,55 @@ onMounted(async () => {
role="list" role="list"
> >
<li v-for="(tag, index) in tags" :key="index"> <li v-for="(tag, index) in tags" :key="index">
<div <Entity
:class="{ :is-selected="selectedTag?.metadata.name === tag.metadata.name"
'bg-gray-100': selectedTag?.metadata.name === tag.metadata.name,
}"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
> >
<div <template #start>
v-show="selectedTag?.metadata.name === tag.metadata.name" <EntityField :description="tag.status?.permalink">
class="absolute inset-y-0 left-0 w-0.5 bg-primary" <template #title>
></div>
<div class="relative flex flex-row items-center">
<div class="flex-1">
<div class="flex flex-col sm:flex-row">
<PostTag :tag="tag" /> <PostTag :tag="tag" />
</div> </template>
<div class="mt-1 flex"> </EntityField>
<span class="text-xs text-gray-500"> </template>
{{ tag.status?.permalink }} <template #end>
</span> <EntityField v-if="tag.metadata.deletionTimestamp">
</div> <template #description>
</div>
<div class="flex">
<div
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<FloatingTooltip
v-if="tag.metadata.deletionTimestamp"
class="mr-4 hidden items-center sm:flex"
>
<div
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div>
<template #popper> 删除中</template>
</FloatingTooltip>
<div <div
class="cursor-pointer text-sm text-gray-500 hover:text-gray-900" v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
> >
{{ tag.status?.posts?.length || 0 }} 篇文章 <span
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
></span>
</div> </div>
<time class="text-sm text-gray-500"> </template>
{{ formatDatetime(tag.metadata.creationTimestamp) }} </EntityField>
</time> <EntityField
<span class="self-center"> :description="`${tag.status?.posts?.length || 0} 篇文章`"
<FloatingDropdown> />
<IconSettings <EntityField
class="cursor-pointer transition-all hover:text-blue-600" :description="formatDatetime(tag.metadata.creationTimestamp)"
/> />
<template #popper> </template>
<div class="w-48 p-2"> <template #menuItems>
<VSpace class="w-full" direction="column"> <VButton
<VButton v-close-popper
v-close-popper block
block type="secondary"
type="secondary" @click="handleOpenEditingModal(tag)"
@click="handleOpenEditingModal(tag)" >
> 修改
修改 </VButton>
</VButton> <VButton
<VButton v-close-popper
v-close-popper block
block type="danger"
type="danger" @click="handleDelete(tag)"
@click="handleDelete(tag)" >
> 删除
删除 </VButton>
</VButton> </template>
</VSpace> </Entity>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>

View File

@ -1,14 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { IconList, VButton, VTag } from "@halo-dev/components";
IconList,
IconSettings,
VButton,
VSpace,
VTag,
} from "@halo-dev/components";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import { ref } from "vue"; import { ref } from "vue";
import type { MenuTreeItem } from "@/modules/interface/menus/utils"; import type { MenuTreeItem } from "@/modules/interface/menus/utils";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
withDefaults( withDefaults(
defineProps<{ defineProps<{
@ -70,80 +66,59 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
> >
<template #item="{ element: menuItem }"> <template #item="{ element: menuItem }">
<li> <li>
<div <Entity>
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" <template #prepend>
> <div
<div class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex" >
> <IconList class="h-3.5 w-3.5" />
<IconList class="h-3.5 w-3.5" /> </div>
</div> </template>
<div class="relative flex flex-row items-center"> <template #start>
<div class="flex-1"> <EntityField
<div class="flex flex-row items-center gap-2"> :title="menuItem.status.displayName"
<span class="truncate text-sm font-medium text-gray-900"> :description="menuItem.status.href"
{{ menuItem.status.displayName }} >
</span> <template #extra>
<VTag v-if="getMenuItemRefDisplayName(menuItem)"> <VTag v-if="getMenuItemRefDisplayName(menuItem)">
{{ getMenuItemRefDisplayName(menuItem) }} {{ getMenuItemRefDisplayName(menuItem) }}
</VTag> </VTag>
</div> </template>
</EntityField>
<div class="mt-1 flex"> </template>
<VSpace align="start" direction="column" spacing="xs"> <template #end>
<a <EntityField v-if="menuItem.metadata.deletionTimestamp">
:href="menuItem.status.href" <template #description>
class="text-xs text-gray-500 hover:text-gray-900" <div
target="_blank" v-tooltip="`删除中`"
> class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
{{ menuItem.status.href }} >
</a> <span
</VSpace> class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
</div> ></span>
</div> </div>
<FloatingTooltip </template>
v-if="menuItem.metadata.deletionTimestamp" </EntityField>
class="mr-4 hidden items-center sm:flex" </template>
<template #menuItems>
<VButton
v-close-popper
block
type="secondary"
@click="onOpenEditingModal(menuItem)"
> >
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"> 修改
<span </VButton>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600" <VButton
></span> v-close-popper
</div> block
<template #popper> 删除中</template> type="danger"
</FloatingTooltip> @click="onDelete(menuItem)"
<div class="self-center"> >
<FloatingDropdown> 删除
<IconSettings </VButton>
class="cursor-pointer transition-all hover:text-blue-600" </template>
/> </Entity>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-close-popper
block
type="secondary"
@click="onOpenEditingModal(menuItem)"
>
修改
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="onDelete(menuItem)"
>
删除
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</div>
</div>
</div>
<MenuItemListItem <MenuItemListItem
:menu-tree-items="menuItem.spec.children" :menu-tree-items="menuItem.spec.children"
class="pl-10 transition-all duration-300" class="pl-10 transition-all duration-300"

View File

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconSettings,
useDialog, useDialog,
VButton, VButton,
VCard, VCard,
@ -12,6 +11,8 @@ import { defineExpose, onMounted, ref } from "vue";
import type { Menu } from "@halo-dev/api-client"; import type { Menu } from "@halo-dev/api-client";
import { apiClient } from "@halo-dev/admin-shared"; import { apiClient } from "@halo-dev/admin-shared";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -137,71 +138,56 @@ defineExpose({
</VSpace> </VSpace>
</template> </template>
</VEmpty> </VEmpty>
<div class="divide-y divide-gray-100 bg-white"> <ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<div <li
v-for="(menu, index) in menus" v-for="(menu, index) in menus"
:key="index" :key="index"
:class="{
'bg-gray-50': selectedMenu?.metadata.name === menu.metadata.name,
}"
class="relative flex items-center p-4"
@click="handleSelect(menu)" @click="handleSelect(menu)"
> >
<div <Entity
v-if="selectedMenu?.metadata.name === menu.metadata.name" :is-selected="selectedMenu?.metadata.name === menu.metadata.name"
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
></div>
<span class="flex flex-1 cursor-pointer flex-col gap-y-1">
<span class="block text-sm font-medium">
{{ menu.spec?.displayName }}
</span>
<span class="block text-xs text-gray-400">
{{ menu.spec.menuItems?.length || 0 }}
个菜单项
</span>
</span>
<FloatingTooltip
v-if="menu.metadata.deletionTimestamp"
class="mr-4 hidden items-center sm:flex"
> >
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"> <template #start>
<span <EntityField
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600" :title="menu.spec?.displayName"
></span> :description="`${menu.spec.menuItems?.length || 0} 个菜单项`"
</div> ></EntityField>
<template #popper> 删除中</template> </template>
</FloatingTooltip> <template #end>
<div class="self-center"> <EntityField v-if="menu.metadata.deletionTimestamp">
<FloatingDropdown> <template #description>
<IconSettings <div
class="cursor-pointer transition-all hover:text-blue-600" v-tooltip="`删除中`"
/> class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
<template #popper> >
<div class="w-48 p-2"> <span
<VSpace class="w-full" direction="column"> class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
<VButton ></span>
v-close-popper </div>
block </template>
type="secondary" </EntityField>
@click="handleOpenEditingModal(menu)" </template>
> <template #menuItems>
修改 <VButton
</VButton> v-close-popper
<VButton block
v-close-popper type="secondary"
block @click="handleOpenEditingModal(menu)"
type="danger" >
@click="handleDeleteMenu(menu)" 修改
> </VButton>
删除 <VButton
</VButton> v-close-popper
</VSpace> block
</div> type="danger"
</template> @click="handleDeleteMenu(menu)"
</FloatingDropdown> >
</div> 删除
</div> </VButton>
</div> </template>
</Entity>
</li>
</ul>
<template #footer> <template #footer>
<VButton block type="secondary" @click="handleOpenEditingModal(null)"> <VButton block type="secondary" @click="handleOpenEditingModal(null)">
新增 新增

View File

@ -1,11 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { VButton, VSpace, VSwitch, VTag } from "@halo-dev/components";
IconSettings, import Entity from "@/components/entity/Entity.vue";
VButton, import EntityField from "@/components/entity/EntityField.vue";
VSpace,
VSwitch,
VTag,
} from "@halo-dev/components";
import { toRefs } from "vue"; import { toRefs } from "vue";
import { usePluginLifeCycle } from "../composables/use-plugin"; import { usePluginLifeCycle } from "../composables/use-plugin";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
@ -25,17 +21,10 @@ const { plugin } = toRefs(props);
const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin); const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
</script> </script>
<template> <template>
<div <Entity>
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" <template #start>
> <EntityField>
<div class="relative flex flex-row items-center"> <template #description>
<div v-if="plugin?.spec.logo" class="mr-4">
<RouterLink
:to="{
name: 'PluginDetail',
params: { name: plugin?.metadata.name },
}"
>
<div class="h-12 w-12 rounded border bg-white p-1 hover:shadow-sm"> <div class="h-12 w-12 rounded border bg-white p-1 hover:shadow-sm">
<img <img
:alt="plugin?.metadata.name" :alt="plugin?.metadata.name"
@ -43,55 +32,40 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
class="h-full w-full" class="h-full w-full"
/> />
</div> </div>
</RouterLink> </template>
</div> </EntityField>
<div class="flex-1"> <EntityField
<div class="flex flex-row items-center"> :title="plugin?.spec.displayName"
<RouterLink :description="plugin?.spec.description"
:to="{ :route="{
name: 'PluginDetail', name: 'PluginDetail',
params: { name: plugin?.metadata.name }, params: { name: plugin?.metadata.name },
}" }"
> >
<span class="mr-2 truncate text-sm font-medium text-gray-900"> <template #extra>
{{ plugin?.spec.displayName }}
</span>
</RouterLink>
<VSpace> <VSpace>
<VTag> <VTag>
{{ isStarted ? "已启用" : "未启用" }} {{ isStarted ? "已启用" : "未启用" }}
</VTag> </VTag>
</VSpace> </VSpace>
</div> </template>
<div class="mt-2 flex"> </EntityField>
<VSpace align="start" direction="column" spacing="xs"> </template>
<span class="text-xs text-gray-500"> <template #end>
{{ plugin?.spec.description }} <EntityField v-if="plugin?.status?.phase === 'FAILED'">
</span> <template #description>
<span class="text-xs text-gray-500 sm:hidden"> <div
@{{ plugin?.spec.author }} {{ plugin?.spec.version }} v-tooltip="`${plugin?.status?.reason}:${plugin?.status?.message}`"
</span> class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
</VSpace>
</div>
</div>
<div class="flex">
<div
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<FloatingTooltip
v-if="plugin?.status?.phase === 'FAILED'"
class="hidden items-center sm:flex"
> >
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"> <span
<span class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600" ></span>
></span> </div>
</div> </template>
<template #popper> </EntityField>
{{ plugin?.status?.reason }}: <EntityField>
{{ plugin?.status?.message }} <template #description>
</template>
</FloatingTooltip>
<a <a
:href="plugin?.spec.homepage" :href="plugin?.spec.homepage"
class="hidden text-sm text-gray-500 hover:text-gray-900 sm:block" class="hidden text-sm text-gray-500 hover:text-gray-900 sm:block"
@ -99,12 +73,15 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
> >
@{{ plugin?.spec.author }} @{{ plugin?.spec.author }}
</a> </a>
<span class="hidden text-sm text-gray-500 sm:block"> </template>
{{ plugin?.spec.version }} </EntityField>
</span> <EntityField :description="plugin?.spec.version" />
<time class="hidden text-sm text-gray-500 sm:block"> <EntityField
{{ formatDatetime(plugin?.metadata.creationTimestamp) }} v-if="plugin?.metadata.creationTimestamp"
</time> :description="formatDatetime(plugin?.metadata.creationTimestamp)"
/>
<EntityField>
<template #description>
<div <div
v-permission="['system:plugins:manage']" v-permission="['system:plugins:manage']"
class="flex items-center" class="flex items-center"
@ -114,27 +91,13 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
@click="changeStatus" @click="changeStatus"
/> />
</div> </div>
<span v-permission="['system:plugins:manage']" class="cursor-pointer"> </template>
<FloatingDropdown> </EntityField>
<IconSettings /> </template>
<template #popper> <template #menuItems>
<div class="w-48 p-2"> <VButton v-close-popper block type="danger" @click="uninstall">
<VSpace class="w-full" direction="column"> 卸载
<VButton </VButton>
v-close-popper </template>
block </Entity>
type="danger"
@click="uninstall"
>
卸载
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</template> </template>

View File

@ -7,7 +7,6 @@ import type { Role } from "@halo-dev/api-client";
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown, IconArrowDown,
IconSettings,
IconShieldUser, IconShieldUser,
useDialog, useDialog,
VButton, VButton,
@ -18,6 +17,8 @@ import {
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import RoleEditingModal from "./components/RoleEditingModal.vue"; import RoleEditingModal from "./components/RoleEditingModal.vue";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
// constants // constants
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
@ -199,114 +200,75 @@ const handleDelete = async (role: Role) => {
</template> </template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list"> <ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(role, index) in roles" :key="index"> <li v-for="(role, index) in roles" :key="index">
<div <Entity>
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" <template #start>
> <EntityField
<div class="relative flex flex-row items-center"> :title="
<div class="flex-1"> role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
<div class="flex flex-row items-center"> role.metadata.name
<RouterLink "
:to="{ :description="`包含
name: 'RoleDetail', ${
params: {
name: role.metadata.name,
},
}"
>
<span
class="mr-2 truncate text-sm font-medium text-gray-900"
>
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || role.metadata.name
}}
</span>
</RouterLink>
</div>
<div class="mt-2 flex">
<span class="text-xs text-gray-500">
包含
{{
JSON.parse( JSON.parse(
role.metadata.annotations?.[ role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES rbacAnnotations.DEPENDENCIES
] || "[]" ] || '[]'
).length ).length
}} }
个权限 个权限`"
</span> :route="{
</div> name: 'RoleDetail',
</div> params: {
<div class="flex"> name: role.metadata.name,
<div },
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6" }"
> ></EntityField>
<FloatingTooltip </template>
v-if="role.metadata.deletionTimestamp" <template #end>
class="hidden items-center sm:flex" <EntityField v-if="role.metadata.deletionTimestamp">
<template #description>
<div
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
> >
<div <span
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600" class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
> ></span>
<span </div>
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600" </template>
></span> </EntityField>
</div> <EntityField description="0 个用户" />
<template #popper> 删除中</template> <EntityField>
</FloatingTooltip> <template #description>
<a
class="hidden text-sm text-gray-500 hover:text-gray-900 sm:block"
target="_blank"
>
0 个用户
</a>
<VTag> 系统保留</VTag> <VTag> 系统保留</VTag>
<time class="text-sm text-gray-500"> </template>
{{ formatDatetime(role.metadata.creationTimestamp) }} </EntityField>
</time> <EntityField
<span :description="formatDatetime(role.metadata.creationTimestamp)"
v-permission="['system:roles:manage']" />
class="cursor-pointer" </template>
> <template #menuItems>
<FloatingDropdown> <VButton
<IconSettings /> v-close-popper
<template #popper> block
<div class="w-48 p-2"> type="secondary"
<VSpace class="w-full" direction="column"> @click="handleOpenEditingModal(role)"
<VButton >
v-close-popper 编辑
block </VButton>
type="secondary" <VButton
@click="handleOpenEditingModal(role)" v-close-popper
> block
编辑 type="danger"
</VButton> @click="handleDelete(role)"
<VButton >
v-close-popper 删除
block </VButton>
type="danger" <VButton v-close-popper block @click="handleCloneRole(role)">
@click="handleDelete(role)" 基于此角色创建
> </VButton>
删除 </template>
</VButton> </Entity>
<VButton
v-close-popper
block
@click="handleCloneRole(role)"
>
基于此角色创建
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>

View File

@ -2,7 +2,6 @@
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown, IconArrowDown,
IconSettings,
IconUserFollow, IconUserFollow,
IconUserSettings, IconUserSettings,
VButton, VButton,
@ -21,6 +20,8 @@ import type { User, UserList } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
const checkAll = ref(false); const checkAll = ref(false);
const editingModal = ref<boolean>(false); const editingModal = ref<boolean>(false);
@ -275,104 +276,70 @@ onMounted(() => {
</template> </template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list"> <ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li v-for="(user, index) in users.items" :key="index"> <li v-for="(user, index) in users.items" :key="index">
<div <Entity :is-selected="checkAll">
:class="{ <template #checkbox>
'bg-gray-100': checkAll, <input
}" v-model="checkAll"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50" v-permission="['system:users:manage']"
> class="h-4 w-4 rounded border-gray-300 text-indigo-600"
<div type="checkbox"
v-show="checkAll" />
class="absolute inset-y-0 left-0 w-0.5 bg-primary" </template>
></div> <template #start>
<div class="relative flex flex-row items-center"> <EntityField>
<div class="mr-4 hidden items-center sm:flex"> <template #description>
<input <VAvatar
v-model="checkAll" :alt="user.spec.displayName"
v-permission="['system:users:manage']" :src="user.spec.avatar"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" size="md"
type="checkbox" ></VAvatar>
/> </template>
</div> </EntityField>
<div v-if="user.spec.avatar" class="mr-4 flex items-center"> <EntityField
<VAvatar :title="user.spec.displayName"
:alt="user.spec.displayName" :description="user.metadata.name"
:src="user.spec.avatar" :route="{
size="md" name: 'UserDetail',
></VAvatar> params: { name: user.metadata.name },
</div> }"
<div class="flex-1"> />
<div class="flex flex-row items-center"> </template>
<span <template #end>
class="mr-2 truncate text-sm font-medium text-gray-900" <EntityField>
@click=" <template #description>
$router.push({
name: 'UserDetail',
params: { name: user.metadata.name },
})
"
>
{{ user.spec.displayName }}
</span>
<VTag class="sm:hidden">{{ user.metadata.name }}</VTag>
</div>
<div class="mt-1 flex">
<VSpace align="start" direction="column" spacing="xs">
<span class="text-xs text-gray-500">
{{ user.metadata.name }}
</span>
</VSpace>
</div>
</div>
<div class="flex">
<div
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
>
<div <div
v-for="(role, roleIndex) in getRoles(user)" v-for="(role, roleIndex) in getRoles(user)"
:key="roleIndex" :key="roleIndex"
class="hidden items-center sm:flex" class="flex items-center"
> >
<VTag> <VTag>
{{ role }} {{ role }}
</VTag> </VTag>
</div> </div>
<time class="text-sm text-gray-500"> </template>
{{ formatDatetime(user.metadata.creationTimestamp) }} </EntityField>
</time> <EntityField
<span :description="formatDatetime(user.metadata.creationTimestamp)"
v-permission="['system:users:manage']" />
class="cursor-pointer" </template>
> <template #menuItems>
<FloatingDropdown> <VButton
<IconSettings /> v-close-popper
<template #popper> block
<div class="w-48 p-2"> type="secondary"
<VSpace class="w-full" direction="column"> @click="handleOpenCreateModal(user)"
<VButton >
v-close-popper 修改资料
block </VButton>
type="secondary" <VButton
@click="handleOpenCreateModal(user)" v-close-popper
> block
修改资料 @click="handleOpenPasswordChangeModal(user)"
</VButton> >
<VButton 修改密码
v-close-popper </VButton>
block </template>
@click="handleOpenPasswordChangeModal(user)" </Entity>
>
修改密码
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</li> </li>
</ul> </ul>