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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import {
IconBookRead,
IconEye,
IconEyeOff,
IconSettings,
IconTeam,
IconCloseCircle,
useDialog,
@ -21,6 +20,8 @@ import {
} from "@halo-dev/components";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.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 { onMounted, ref, watch, watchEffect } from "vue";
import type {
@ -738,18 +739,8 @@ function handleContributorFilterItemChange(user?: User) {
role="list"
>
<li v-for="(post, index) in posts.items" :key="index">
<div
:class="{
'bg-gray-100': checkSelection(post.post),
}"
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
>
<div
v-show="checkSelection(post.post)"
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
></div>
<div class="relative flex flex-row items-center">
<div class="mr-4 hidden items-center sm:flex">
<Entity :is-selected="checkSelection(post.post)">
<template #checkbox>
<input
v-model="selectedPostNames"
:value="post.post.metadata.name"
@ -757,27 +748,20 @@ function handleContributorFilterItemChange(user?: User) {
name="post-checkbox"
type="checkbox"
/>
</div>
<div class="flex-1">
<div class="flex flex-col items-center sm:flex-row">
<RouterLink
:to="{
</template>
<template #start>
<EntityField
:title="post.post.spec.title"
:route="{
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>
<template #extra>
<VSpace class="mt-1 sm:mt-0">
<FloatingTooltip
v-if="post.post.status?.inProgress"
class="hidden items-center sm:flex"
>
<RouterLink
v-if="post.post.status?.inProgress"
v-tooltip="`当前有内容已保存,但还未发布。`"
:to="{
name: 'PostEditor',
query: { name: post.post.metadata.name },
@ -792,10 +776,6 @@ function handleContributorFilterItemChange(user?: User) {
></span>
</div>
</RouterLink>
<template #popper>
当前有内容已保存但还未发布
</template>
</FloatingTooltip>
<PostTag
v-for="(tag, tagIndex) in post.tags"
:key="tagIndex"
@ -803,8 +783,8 @@ function handleContributorFilterItemChange(user?: User) {
route
></PostTag>
</VSpace>
</div>
<div class="mt-1 flex">
</template>
<template #description>
<VSpace>
<p
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>
</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"
>
</template>
</EntityField>
</template>
<template #end>
<EntityField>
<template #description>
<RouterLink
v-for="(contributor, contributorIndex) in post.contributors"
:key="contributorIndex"
@ -844,10 +824,11 @@ function handleContributorFilterItemChange(user?: User) {
circle
></VAvatar>
</RouterLink>
<span class="text-sm text-gray-500">
{{ finalStatus(post.post) }}
</span>
<span>
</template>
</EntityField>
<EntityField :description="finalStatus(post.post)"></EntityField>
<EntityField>
<template #description>
<IconEye
v-if="post.post.spec.visible === 'PUBLIC'"
v-tooltip="`公开访问`"
@ -863,18 +844,15 @@ function handleContributorFilterItemChange(user?: User) {
v-tooltip="`内部成员可访问`"
class="cursor-pointer text-sm transition-all hover:text-blue-600"
/>
</span>
<time class="text-sm text-gray-500">
{{ formatDatetime(post.post.metadata.creationTimestamp) }}
</time>
<span>
<FloatingDropdown>
<IconSettings
class="cursor-pointer transition-all hover:text-blue-600"
</template>
</EntityField>
<EntityField
:description="
formatDatetime(post.post.metadata.creationTimestamp)
"
/>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
</template>
<template #menuItems>
<VButton
v-close-popper
block
@ -891,15 +869,8 @@ function handleContributorFilterItemChange(user?: User) {
>
删除
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</Entity>
</li>
</ul>

View File

@ -1,9 +1,11 @@
<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 type { CategoryTree } from "../utils";
import { ref } from "vue";
import { formatDatetime } from "@/utils/date";
import Entity from "@/components/entity/Entity.vue";
import EntityField from "@/components/entity/EntityField.vue";
withDefaults(
defineProps<{
@ -49,61 +51,41 @@ function onDelete(category: CategoryTree) {
>
<template #item="{ element: category }">
<li>
<div
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
>
<Entity>
<template #prepend>
<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"
>
<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 class="flex">
</template>
<template #start>
<EntityField
:title="category.spec.displayName"
:description="category.status.permalink"
/>
</template>
<template #end>
<EntityField v-if="category.metadata.deletionTimestamp">
<template #description>
<div
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
v-tooltip="`删除中`"
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
>
<FloatingTooltip
v-if="category.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
class="cursor-pointer text-sm text-gray-500 hover:text-gray-900"
>
{{ category.status?.posts?.length || 0 }} 篇文章
</div>
<time class="text-sm text-gray-500">
{{ formatDatetime(category.metadata.creationTimestamp) }}
</time>
<span class="self-center">
<FloatingDropdown>
<IconSettings
class="cursor-pointer transition-all hover:text-blue-600"
</template>
</EntityField>
<EntityField
:description="`${category.status?.posts?.length || 0} 篇文章`"
/>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<EntityField
:description="formatDatetime(category.metadata.creationTimestamp)"
/>
</template>
<template #menuItems>
<VButton
v-close-popper
block
@ -120,15 +102,8 @@ function onDelete(category: CategoryTree) {
>
删除
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</span>
</div>
</div>
</div>
</div>
</Entity>
<CategoryListItem
:categories="category.spec.children"
class="pl-10 transition-all duration-300"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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