feat: add description component (#3792)

#### What type of PR is this?

/kind feature
/area console
/milestone 2.5.0

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

为 Console 端添加 Description 组件。

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

Fixes #3790 

#### Special notes for your reviewer:

测试方式:

1. 检查主题管理、插件详情、认证方式详情页面的样式是否异常即可。

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


```release-note
为 Console 端添加 Description 组件。
```
pull/3822/head
Ryan Wang 2023-04-23 10:49:32 +08:00 committed by GitHub
parent 676df239ea
commit eec1d0758e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 514 additions and 844 deletions

View File

@ -19,3 +19,4 @@ export * from "./components/toast";
export * from "./components/loading";
export * from "./components/dropdown";
export * from "./components/tooltip";
export * from "./components/description";

View File

@ -0,0 +1,13 @@
<script lang="ts" setup></script>
<template>
<dl class="description-wrapper">
<slot />
</dl>
</template>
<style>
.description-wrapper {
@apply divide-y divide-gray-100;
}
</style>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
label: string;
content?: string;
verticalCenter?: boolean;
}>(),
{
content: undefined,
verticalCenter: false,
}
);
</script>
<template>
<div
class="description-item-wrapper"
:class="{ 'items-center': verticalCenter }"
>
<dt class="description-item__label">{{ label }}</dt>
<dd class="description-item__content">
<slot v-if="$slots.default" />
<template v-else>
{{ content }}
</template>
</dd>
</div>
</template>
<style lang="scss">
.description-item-wrapper {
@apply bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6;
.description-item__label {
@apply text-sm font-medium text-gray-900;
}
.description-item__content {
@apply mt-1 text-sm text-gray-900 sm:col-span-6 md:col-span-5 lg:col-span-3 sm:mt-0;
}
}
</style>

View File

@ -0,0 +1,2 @@
export { default as VDescription } from "./Description.vue";
export { default as VDescriptionItem } from "./DescriptionItem.vue";

View File

@ -1,5 +1,11 @@
<script lang="ts" setup>
import { VButton, VModal, VSpace } from "@halo-dev/components";
import {
VButton,
VDescription,
VDescriptionItem,
VModal,
VSpace,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import type { Attachment } from "@halo-dev/api-client";
import prettyBytes from "pretty-bytes";
@ -79,6 +85,7 @@ const onVisibleChange = (visible: boolean) => {
:mount-to-body="mountToBody"
:layer-closable="true"
height="calc(100vh - 20px)"
:body-class="['!p-0']"
@update:visible="onVisibleChange"
>
<template #actions>
@ -87,7 +94,7 @@ const onVisibleChange = (visible: boolean) => {
<div class="overflow-hidden bg-white">
<div
v-if="onlyPreview && isImage(attachment?.spec.mediaType)"
class="flex justify-center"
class="flex justify-center p-4"
>
<img
v-tooltip.bottom="
@ -99,14 +106,11 @@ const onVisibleChange = (visible: boolean) => {
@click="onlyPreview = !onlyPreview"
/>
</div>
<dl v-else>
<div
class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.preview") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<div v-else>
<VDescription>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.preview')"
>
<div
v-if="isImage(attachment?.spec.mediaType)"
@click="onlyPreview = !onlyPreview"
@ -149,80 +153,47 @@ const onVisibleChange = (visible: boolean) => {
<span v-else>
{{ $t("core.attachment.detail_modal.preview.not_support") }}
</span>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.storage_policy") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ policy?.spec.displayName }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.group") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.storage_policy')"
:content="policy?.spec.displayName"
></VDescriptionItem>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.group')"
:content="
getGroupName(attachment?.spec.groupName) ||
$t("core.attachment.common.text.ungrouped")
}}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.display_name") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ attachment?.spec.displayName }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.media_type") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ attachment?.spec.mediaType }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.size") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ prettyBytes(attachment?.spec.size || 0) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.owner") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ attachment?.spec.ownerName }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.creation_time") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDatetime(attachment?.metadata.creationTimestamp) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.attachment.detail_modal.fields.permalink") }}
</dt>
<dd
class="mt-1 text-sm text-gray-900 hover:text-blue-600 sm:col-span-2 sm:mt-0"
$t('core.attachment.common.text.ungrouped')
"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.display_name')"
:content="attachment?.spec.displayName"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.media_type')"
:content="attachment?.spec.mediaType"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.size')"
:content="prettyBytes(attachment?.spec.size || 0)"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.owner')"
:content="attachment?.spec.ownerName"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.creation_time')"
:content="formatDatetime(attachment?.metadata.creationTimestamp)"
/>
<VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.permalink')"
>
<a target="_blank" :href="attachment?.status?.permalink">
{{ attachment?.status?.permalink }}
</a>
</dd>
</div>
</dl>
</VDescriptionItem>
</VDescription>
</div>
</div>
<template #footer>
<VSpace>

View File

@ -17,6 +17,8 @@ import {
VDropdown,
VDropdownItem,
VDropdownDivider,
VDescription,
VDescriptionItem,
} from "@halo-dev/components";
import ThemeUploadModal from "./components/ThemeUploadModal.vue";
@ -126,141 +128,46 @@ const onUpgradeModalClose = () => {
</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">
{{ $t("core.theme.detail.fields.author") }}
</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">
{{ $t("core.theme.detail.fields.website") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a
:href="selectedTheme?.spec.website"
class="hover:text-gray-600"
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">
{{ $t("core.theme.detail.fields.repo") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a
:href="selectedTheme?.spec.repo"
class="hover:text-gray-600"
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">
{{ $t("core.theme.detail.fields.version") }}
</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">
{{ $t("core.theme.detail.fields.requires") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ selectedTheme?.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">
{{ $t("core.theme.detail.fields.storage_location") }}
</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">
{{ $t("core.theme.detail.fields.plugin_requires") }}
</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>{{ $t("core.common.status.installed") }}</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>
{{ $t("core.common.status.not_installed") }}
</VTag>
</VSpace>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<VDescription>
<VDescriptionItem
label="ID"
:content="selectedTheme?.metadata.name"
/>
<VDescriptionItem
:label="$t('core.theme.detail.fields.author')"
:content="selectedTheme?.spec.author.name"
/>
<VDescriptionItem :label="$t('core.theme.detail.fields.website')">
<a
:href="selectedTheme?.spec.website"
class="hover:text-gray-600"
target="_blank"
>
{{ selectedTheme?.spec.website }}
</a>
</VDescriptionItem>
<VDescriptionItem :label="$t('core.theme.detail.fields.repo')">
<a
:href="selectedTheme?.spec.repo"
class="hover:text-gray-600"
target="_blank"
>
{{ selectedTheme?.spec.repo }}
</a>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.theme.detail.fields.version')"
:content="selectedTheme?.spec.version"
/>
<VDescriptionItem
:label="$t('core.theme.detail.fields.requires')"
:content="selectedTheme?.spec.requires"
/>
<VDescriptionItem
:label="$t('core.theme.detail.fields.storage_location')"
:content="selectedTheme?.status?.location"
/>
</VDescription>
</div>
</div>
</Transition>

View File

@ -7,6 +7,8 @@ import {
VCard,
VButton,
Toast,
VDescription,
VDescriptionItem,
} from "@halo-dev/components";
import { computed, onMounted, ref } from "vue";
import type { Info, GlobalInfo, Startup } from "./types";
@ -159,64 +161,39 @@ const handleDownloadLogfile = () => {
</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">
{{ $t("core.actuator.fields.external_url") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<span v-if="globalInfo?.externalUrl">
{{ globalInfo?.externalUrl }}
</span>
<span v-else>
{{ $t("core.actuator.fields_values.external_url.not_setup") }}
</span>
<VAlert
v-if="!isExternalUrlValid"
class="mt-3"
type="warning"
:title="$t('core.common.text.warning')"
:closable="false"
>
<template #description>
{{ $t("core.actuator.alert.external_url_invalid") }}
</template>
</VAlert>
</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">
{{ $t("core.actuator.fields.start_time") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDatetime(startup?.timeline.startTime) }}
</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">
{{ $t("core.actuator.fields.timezone") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ globalInfo?.timeZone }}
</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">
{{ $t("core.actuator.fields.locale") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ globalInfo?.locale }}
</dd>
</div>
</dl>
<VDescription>
<VDescriptionItem :label="$t('core.actuator.fields.external_url')">
<span v-if="globalInfo?.externalUrl">
{{ globalInfo?.externalUrl }}
</span>
<span v-else>
{{ $t("core.actuator.fields_values.external_url.not_setup") }}
</span>
<VAlert
v-if="!isExternalUrlValid"
class="mt-3"
type="warning"
:title="$t('core.common.text.warning')"
:closable="false"
>
<template #description>
{{ $t("core.actuator.alert.external_url_invalid") }}
</template>
</VAlert>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.actuator.fields.start_time')"
:content="formatDatetime(startup?.timeline.startTime)"
/>
<VDescriptionItem
:label="$t('core.actuator.fields.timezone')"
:content="globalInfo?.timeZone"
/>
<VDescriptionItem
:label="$t('core.actuator.fields.locale')"
:content="globalInfo?.locale"
/>
</VDescription>
</div>
</div>
</VCard>
@ -232,91 +209,55 @@ const handleDownloadLogfile = () => {
</div>
</div>
<div class="border-t border-gray-200">
<dl class="divide-y divide-gray-100">
<div
<VDescription>
<VDescriptionItem
v-if="info.build"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
:label="$t('core.actuator.fields.version')"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.actuator.fields.version") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<a
:href="`https://github.com/halo-dev/halo/releases/tag/v${info.build.version}`"
class="hover:text-gray-600"
target="_blank"
>
{{ info.build.version }}
</a>
</dd>
</div>
<div
<a
:href="`https://github.com/halo-dev/halo/releases/tag/v${info.build.version}`"
class="hover:text-gray-600"
target="_blank"
>
{{ info.build.version }}
</a>
</VDescriptionItem>
<VDescriptionItem
v-if="info.build"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
:label="$t('core.actuator.fields.build_time')"
:content="formatDatetime(info.build.time)"
/>
<VDescriptionItem v-if="info.git" label="Git Commit">
<a
:href="`https://github.com/halo-dev/halo/commit/${info.git.commit.id}`"
class="hover:text-gray-600"
target="_blank"
>
{{ info.git.commit.id }}
</a>
</VDescriptionItem>
<VDescriptionItem
label="Java"
:content="
[info.java.runtime.name, info.java.runtime.version].join(' / ')
"
/>
<VDescriptionItem
:label="$t('core.actuator.fields.database')"
:content="[info.database.name, info.database.version].join(' / ')"
/>
<VDescriptionItem :label="$t('core.actuator.fields.os')">
{{ info.os.name }} {{ info.os.version }} / {{ info.os.arch }}
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.actuator.fields.log')"
vertical-center
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.actuator.fields.build_time") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDatetime(info.build.time) }}
</dd>
</div>
<div
v-if="info.git"
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">Git Commit</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<a
:href="`https://github.com/halo-dev/halo/commit/${info.git.commit.id}`"
class="hover:text-gray-600"
target="_blank"
>
{{ info.git.commit.id }}
</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">Java</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ info.java.runtime.name }} / {{ info.java.runtime.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">
{{ $t("core.actuator.fields.database") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ [info.database.name, info.database.version].join(" / ") }}
</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">
{{ $t("core.actuator.fields.os") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ info.os.name }} {{ info.os.version }} / {{ info.os.arch }}
</dd>
</div>
<div
class="items-center 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">
{{ $t("core.actuator.fields.log") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<VButton size="sm" @click="handleDownloadLogfile()">
{{ $t("core.common.buttons.download") }}
</VButton>
</dd>
</div>
</dl>
<VButton size="sm" @click="handleDownloadLogfile()">
{{ $t("core.common.buttons.download") }}
</VButton>
</VDescriptionItem>
</VDescription>
</div>
</div>
</VCard>

View File

@ -9,6 +9,8 @@ import {
VAvatar,
VButton,
VCard,
VDescription,
VDescriptionItem,
VPageHeader,
VTabbar,
} from "@halo-dev/components";
@ -167,82 +169,56 @@ const handleSaveConfigMap = async () => {
</template>
<div class="bg-white">
<div v-if="activeTab === 'detail'">
<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"
<VDescription>
<VDescriptionItem
:label="
$t('core.identity_authentication.detail.fields.display_name')
"
:content="authProvider?.spec.displayName"
/>
<VDescriptionItem
:label="
$t('core.identity_authentication.detail.fields.description')
"
:content="authProvider?.spec.description"
/>
<VDescriptionItem
:label="$t('core.identity_authentication.detail.fields.website')"
>
<dt class="text-sm font-medium text-gray-900">
{{
$t("core.identity_authentication.detail.fields.display_name")
}}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ authProvider?.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"
<a
v-if="authProvider?.spec.website"
:href="authProvider?.spec.website"
target="_blank"
>
{{ authProvider.spec.website }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem
:label="
$t('core.identity_authentication.detail.fields.help_page')
"
>
<dt class="text-sm font-medium text-gray-900">
{{
$t("core.identity_authentication.detail.fields.description")
}}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ authProvider?.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">
{{ $t("core.identity_authentication.detail.fields.website") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a
v-if="authProvider?.spec.website"
:href="authProvider?.spec.website"
target="_blank"
>
{{ authProvider.spec.website }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</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">
{{ $t("core.identity_authentication.detail.fields.help_page") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<a
v-if="authProvider?.spec.helpPage"
:href="authProvider?.spec.helpPage"
target="_blank"
>
{{ authProvider.spec.helpPage }}
</a>
<span v-else>{{ $t("core.common.text.none") }}</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">
{{
$t(
"core.identity_authentication.detail.fields.authentication_url"
)
}}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ authProvider?.spec.authenticationUrl }}
</dd>
</div>
</dl>
<a
v-if="authProvider?.spec.helpPage"
:href="authProvider?.spec.helpPage"
target="_blank"
>
{{ authProvider.spec.helpPage }}
</a>
<span v-else>{{ $t("core.common.text.none") }}</span>
</VDescriptionItem>
<VDescriptionItem
:label="
$t(
'core.identity_authentication.detail.fields.authentication_url'
)
"
:content="authProvider?.spec.authenticationUrl"
/>
</VDescription>
</div>
<div v-if="activeTab === 'setting'" class="bg-white p-4">
<div>

View File

@ -1,5 +1,10 @@
<script lang="ts" setup>
import { VSwitch, VTag } from "@halo-dev/components";
import {
VDescription,
VDescriptionItem,
VSwitch,
VTag,
} from "@halo-dev/components";
import type { Ref } from "vue";
import { computed, inject } from "vue";
import { apiClient } from "@/utils/api-client";
@ -83,183 +88,114 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
</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">
{{ $t("core.plugin.detail.fields.display_name") }}
</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">
{{ $t("core.plugin.detail.fields.description") }}
</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">
{{ $t("core.plugin.detail.fields.version") }}
</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">
{{ $t("core.plugin.detail.fields.requires") }}
</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">
{{ $t("core.plugin.detail.fields.author") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<a
v-if="plugin?.spec.author"
:href="plugin?.spec.author.website"
target="_blank"
>
{{ plugin?.spec.author.name }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</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">
{{ $t("core.plugin.detail.fields.license") }}
</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>
{{ $t("core.common.text.none") }}
</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">
{{ $t("core.plugin.detail.fields.role_templates") }}
</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"
>
<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"
>
{{
$t("core.role.common.text.dependent_on", {
roles: JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", "),
})
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else>
{{ $t("core.common.text.none") }}
</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">
{{ $t("core.plugin.detail.fields.last_starttime") }}
</dt>
<dd
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-2 sm:mt-0"
<VDescription>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.display_name')"
:content="plugin?.spec.displayName"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.description')"
:content="plugin?.spec.description"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.version')"
:content="plugin?.spec.version"
/>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.requires')"
:content="plugin?.spec.requires"
/>
<VDescriptionItem :label="$t('core.plugin.detail.fields.author')">
<a
v-if="plugin?.spec.author"
:href="plugin?.spec.author.website"
target="_blank"
>
{{ formatDatetime(plugin?.status?.lastStartTime) }}
</dd>
</div>
</dl>
{{ plugin?.spec.author.name }}
</a>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem :label="$t('core.plugin.detail.fields.license')">
<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>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.role_templates')"
>
<dl
v-if="pluginRoleTemplateGroups.length"
class="divide-y divide-gray-100"
>
<div
v-for="(group, groupIndex) in pluginRoleTemplateGroups"
:key="groupIndex"
class="rounded bg-gray-50 px-4 py-5 hover:bg-gray-100 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"
>
{{
$t("core.role.common.text.dependent_on", {
roles: JSON.parse(
role.metadata.annotations?.[
rbacAnnotations.DEPENDENCIES
]
).join(", "),
})
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
<span v-else>
{{ $t("core.common.text.none") }}
</span>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.plugin.detail.fields.last_starttime')"
:content="formatDatetime(plugin?.status?.lastStartTime)"
/>
</VDescription>
</div>
</div>
</Transition>

View File

@ -9,6 +9,8 @@ import {
VTag,
VAvatar,
VAlert,
VDescription,
VDescriptionItem,
} from "@halo-dev/components";
import { useRoute } from "vue-router";
import { computed, onMounted, ref, watch } from "vue";
@ -19,7 +21,6 @@ import {
useRoleForm,
useRoleTemplateSelection,
} from "@/modules/system/roles/composables/use-role";
import { useUserFetch } from "@/modules/system/users/composables/use-user";
import { SUPER_ROLE_NAME } from "@/constants/constants";
import { useI18n } from "vue-i18n";
import { formatDatetime } from "@/utils/date";
@ -34,8 +35,6 @@ const { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =
const { formState, saving, handleCreateOrUpdate } = useRoleForm();
const { users } = useUserFetch({ fetchOnMounted: false });
const isSystemReserved = computed(() => {
return (
formState.value.metadata.labels?.[roleLabels.SYSTEM_RESERVED] === "true"
@ -130,114 +129,34 @@ onMounted(() => {
</p>
</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-3 sm:gap-4 sm:px-6"
<VDescription>
<VDescriptionItem
:label="$t('core.role.detail.fields.display_name')"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.role.detail.fields.display_name") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{
formState.metadata?.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || formState.metadata?.name
}}
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.role.detail.fields.name')"
:content="formState.metadata?.name"
/>
<VDescriptionItem :label="$t('core.role.detail.fields.type')">
<VTag>
{{
formState.metadata?.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || formState.metadata?.name
isSystemReserved
? t("core.role.common.text.system_reserved")
: t("core.role.common.text.custom")
}}
</dd>
</div>
<div
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">
{{ $t("core.role.detail.fields.name") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formState.metadata?.name }}
</dd>
</div>
<div
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">
{{ $t("core.role.detail.fields.type") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<VTag>
{{
isSystemReserved
? t("core.role.common.text.system_reserved")
: t("core.role.common.text.custom")
}}
</VTag>
</dd>
</div>
<div
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">
{{ $t("core.role.detail.fields.creation_time") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDatetime(formState.metadata.creationTimestamp) }}
</dd>
</div>
<!-- TODO: 支持通过当前角色查询用户 -->
<div
v-if="false"
class="bg-gray-50 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">用户</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<div
class="h-96 overflow-y-auto overflow-x-hidden rounded-sm bg-white shadow-sm transition-all hover:shadow"
>
<ul class="divide-y divide-gray-100" role="list">
<RouterLink
v-for="(user, index) in users"
:key="index"
:to="{
name: 'UserDetail',
params: { name: user.metadata.name },
}"
>
<li class="block cursor-pointer hover:bg-gray-50">
<div class="flex items-center px-4 py-4">
<div class="flex min-w-0 flex-1 items-center">
<div class="flex flex-shrink-0 items-center">
<VAvatar
:alt="user.spec.displayName"
:src="user.spec.avatar"
size="md"
/>
</div>
<div
class="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4"
>
<div>
<p
class="truncate text-sm font-medium text-gray-900"
>
{{ user.spec.displayName }}
</p>
<p class="mt-2 flex items-center">
<span class="text-xs text-gray-500">
{{ user.metadata.name }}
</span>
</p>
</div>
</div>
</div>
<div>
<IconArrowRight />
</div>
</div>
</li>
</RouterLink>
</ul>
</div>
</dd>
</div>
</dl>
</VTag>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.role.detail.fields.creation_time')"
:content="formatDatetime(formState.metadata.creationTimestamp)"
/>
</VDescription>
</div>
</div>

View File

@ -1,5 +1,12 @@
<script lang="ts" setup>
import { Dialog, IconUserSettings, VButton, VTag } from "@halo-dev/components";
import {
Dialog,
IconUserSettings,
VButton,
VDescription,
VDescriptionItem,
VTag,
} from "@halo-dev/components";
import type { ComputedRef, Ref } from "vue";
import { inject, computed } from "vue";
import { useRouter } from "vue-router";
@ -64,144 +71,99 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
</script>
<template>
<div class="border-t border-gray-100">
<dl class="divide-y divide-gray-50">
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
<VDescription>
<VDescriptionItem
:label="$t('core.user.detail.fields.display_name')"
:content="user?.user.spec.displayName"
class="!px-2"
/>
<VDescriptionItem
:label="$t('core.user.detail.fields.username')"
:content="user?.user.metadata.name"
class="!px-2"
/>
<VDescriptionItem
:label="$t('core.user.detail.fields.email')"
:content="user?.user.spec.email || $t('core.common.text.none')"
class="!px-2"
/>
<VDescriptionItem
:label="$t('core.user.detail.fields.roles')"
class="!px-2"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.display_name") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.user.spec?.displayName }}
</dd>
</div>
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.username") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.user.metadata?.name }}
</dd>
</div>
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.email") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.user.spec?.email || $t("core.common.text.none") }}
</dd>
</div>
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.roles") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<VTag
v-for="(role, index) in user?.roles"
:key="index"
@click="
router.push({
name: 'RoleDetail',
params: { name: role.metadata.name },
})
"
>
<template #leftIcon>
<IconUserSettings />
</template>
{{
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name
}}
</VTag>
</dd>
</div>
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.bio") }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.user.spec?.bio || $t("core.common.text.none") }}
</dd>
</div>
<div
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.creation_time") }}
</dt>
<dd
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-3 sm:mt-0"
<VTag
v-for="(role, index) in user?.roles"
:key="index"
@click="
router.push({
name: 'RoleDetail',
params: { name: role.metadata.name },
})
"
>
{{ formatDatetime(user?.user.metadata?.creationTimestamp) }}
</dd>
</div>
<!-- TODO: add display last login time support -->
<div
v-if="false"
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
>
<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">
{{ user?.user.metadata?.creationTimestamp }}
</dd>
</div>
<div
<template #leftIcon>
<IconUserSettings />
</template>
{{
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name
}}
</VTag>
</VDescriptionItem>
<VDescriptionItem
:label="$t('core.user.detail.fields.bio')"
:content="user?.user.spec?.bio || $t('core.common.text.none')"
class="!px-2"
/>
<VDescriptionItem
:label="$t('core.user.detail.fields.creation_time')"
:content="formatDatetime(user?.user.metadata?.creationTimestamp)"
class="!px-2"
/>
<VDescriptionItem
v-if="!isFetching && isCurrentUser && availableAuthProviders?.length"
class="bg-white px-2 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4"
:label="$t('core.user.detail.fields.identity_authentication')"
class="!px-2"
>
<dt class="text-sm font-medium text-gray-900">
{{ $t("core.user.detail.fields.identity_authentication") }}
</dt>
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
<ul class="space-y-2">
<template v-for="(authProvider, index) in authProviders">
<li
v-if="authProvider.supportsBinding && authProvider.enabled"
:key="index"
<ul class="space-y-2">
<template v-for="(authProvider, index) in authProviders">
<li
v-if="authProvider.supportsBinding && authProvider.enabled"
:key="index"
>
<div
class="flex w-full cursor-pointer flex-wrap justify-between gap-y-3 rounded border p-5 hover:border-primary sm:w-1/2"
>
<div
class="flex w-full cursor-pointer flex-wrap justify-between gap-y-3 rounded border p-5 hover:border-primary sm:w-1/2"
>
<div class="inline-flex items-center gap-3">
<div>
<img class="h-7 w-7 rounded" :src="authProvider.logo" />
</div>
<div class="text-sm font-medium text-gray-900">
{{ authProvider.displayName }}
</div>
<div class="inline-flex items-center gap-3">
<div>
<img class="h-7 w-7 rounded" :src="authProvider.logo" />
</div>
<div class="inline-flex items-center">
<VButton
v-if="authProvider.isBound"
size="sm"
@click="handleUnbindAuth(authProvider)"
>
{{ $t("core.user.detail.operations.unbind.button") }}
</VButton>
<VButton
v-else
size="sm"
type="secondary"
@click="handleBindAuth(authProvider)"
>
{{ $t("core.user.detail.operations.bind.button") }}
</VButton>
<div class="text-sm font-medium text-gray-900">
{{ authProvider.displayName }}
</div>
</div>
</li>
</template>
</ul>
</dd>
</div>
</dl>
<div class="inline-flex items-center">
<VButton
v-if="authProvider.isBound"
size="sm"
@click="handleUnbindAuth(authProvider)"
>
{{ $t("core.user.detail.operations.unbind.button") }}
</VButton>
<VButton
v-else
size="sm"
type="secondary"
@click="handleBindAuth(authProvider)"
>
{{ $t("core.user.detail.operations.bind.button") }}
</VButton>
</div>
</div>
</li>
</template>
</ul>
</VDescriptionItem>
</VDescription>
</div>
</template>