feat: add enable and upgrade support for theme list (#774)

#### What type of PR is this?

/kind improvement

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

1. 在主题管理列表添加启用和升级的支持。
2. 优化主题页面右上角的按钮布局
3. 优化主题管理的代码结构。

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

Fixes https://github.com/halo-dev/halo/issues/2948

#### Screenshots:

<img width="1515" alt="image" src="https://user-images.githubusercontent.com/21301288/207893631-6db10293-d0ee-4af4-9c43-f137055df28e.png">
<img width="1665" alt="image" src="https://user-images.githubusercontent.com/21301288/207894016-dd1a5d27-9fff-4211-8869-381208932eaf.png">


#### Special notes for your reviewer:

测试方式:

1. 进入主题管理,点击右上角的主题管理。
4. 测试单个主题的启用和升级是否符合预期。

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

```release-note
Console 端的主题管理列表添加启用和升级的支持。
```
pull/773/head^2
Ryan Wang 2022-12-16 10:46:11 +08:00 committed by GitHub
parent 75f2cb0540
commit 6ac0816380
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 240 additions and 193 deletions

View File

@ -2,15 +2,12 @@
import {
IconAddCircle,
IconGitHub,
Dialog,
VButton,
VEmpty,
VModal,
VSpace,
VTag,
VEntity,
VEntityField,
VStatusDot,
VTabItem,
VTabs,
VLoading,
@ -18,15 +15,11 @@ import {
import LazyImage from "@/components/image/LazyImage.vue";
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
import ThemeUploadModal from "./ThemeUploadModal.vue";
import ThemeListItem from "./components/ThemeListItem.vue";
import { computed, ref, watch } from "vue";
import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import { useThemeStore } from "@/stores/theme";
import { storeToRefs } from "pinia";
const { currentUserHasPermission } = usePermission();
const props = withDefaults(
defineProps<{
@ -49,7 +42,7 @@ const emit = defineEmits<{
const activeTab = ref("installed");
const themes = ref<Theme[]>([] as Theme[]);
const loading = ref(false);
const themeInstall = ref(false);
const themeUploadVisible = ref(false);
const creating = ref(false);
const refreshInterval = ref();
@ -57,8 +50,6 @@ const modalTitle = computed(() => {
return activeTab.value === "installed" ? "已安装的主题" : "未安装的主题";
});
const { activatedTheme } = storeToRefs(useThemeStore());
const handleFetchThemes = async (options?: { mute?: boolean }) => {
try {
clearInterval(refreshInterval.value);
@ -104,57 +95,6 @@ watch(
}
);
const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
Dialog.warning({
title: `${
deleteExtensions
? "确定要删除该主题以及对应的配置吗?"
: "确定要删除该主题吗?"
}`,
description: "该操作不可恢复。",
onConfirm: async () => {
try {
await apiClient.extension.theme.deletethemeHaloRunV1alpha1Theme({
name: theme.metadata.name,
});
// delete theme setting and configMap
if (!deleteExtensions) {
return;
}
const { settingName, configMapName } = theme.spec;
if (settingName) {
await apiClient.extension.setting.deletev1alpha1Setting(
{
name: settingName,
},
{
mute: true,
}
);
}
if (configMapName) {
await apiClient.extension.configMap.deletev1alpha1ConfigMap(
{
name: configMapName,
},
{
mute: true,
}
);
}
} catch (e) {
console.error("Failed to uninstall theme", e);
} finally {
await handleFetchThemes();
}
},
});
};
const handleCreateTheme = async (theme: Theme) => {
try {
creating.value = true;
@ -204,6 +144,7 @@ defineExpose({
handleFetchThemes,
});
// preview
const previewVisible = ref(false);
const selectedPreviewTheme = ref<Theme>();
@ -211,6 +152,19 @@ const handleOpenPreview = (theme: Theme) => {
selectedPreviewTheme.value = theme;
previewVisible.value = true;
};
// upgrade
const themeToUpgrade = ref<Theme>();
const handleOpenUpgradeModal = (theme: Theme) => {
themeToUpgrade.value = theme;
themeUploadVisible.value = true;
};
const handleOpenInstallModal = () => {
themeToUpgrade.value = undefined;
themeUploadVisible.value = true;
};
</script>
<template>
<VModal
@ -241,7 +195,7 @@ const handleOpenPreview = (theme: Theme) => {
<VButton
v-permission="['system:themes:manage']"
type="primary"
@click="themeInstall = true"
@click="handleOpenInstallModal()"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
@ -262,129 +216,15 @@ const handleOpenPreview = (theme: Theme) => {
:key="index"
@click="handleSelectTheme(theme)"
>
<VEntity
<ThemeListItem
:theme="theme"
:is-selected="
theme.metadata.name === selectedTheme?.metadata?.name
"
>
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400"
>加载中...</span
>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400"
>加载异常</span
>
</div>
</template>
</LazyImage>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
<template #extra>
<VTag
v-if="
theme.metadata.name === activatedTheme?.metadata?.name
"
>
当前启用
</VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="theme.metadata.deletionTimestamp">
<template #description>
<VStatusDot
v-tooltip="`删除中`"
state="warning"
animate
/>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:themes:manage'])"
#dropdownItems
>
<VButton
v-close-popper
block
type="secondary"
@click="handleOpenPreview(theme)"
>
预览
</VButton>
<VButton
v-close-popper
block
type="danger"
@click="handleUninstall(theme)"
>
卸载
</VButton>
<VButton
v-close-popper
:disabled="
theme.metadata.name === activatedTheme?.metadata?.name
"
block
type="danger"
@click="handleUninstall(theme, true)"
>
卸载并删除配置
</VButton>
</template>
</VEntity>
@reload="handleFetchThemes({ mute: true })"
@preview="handleOpenPreview(theme)"
@upgrade="handleOpenUpgradeModal(theme)"
/>
</li>
</ul>
</Transition>
@ -499,7 +339,7 @@ const handleOpenPreview = (theme: Theme) => {
<VButton
v-permission="['system:themes:manage']"
type="secondary"
@click="themeInstall = true"
@click="handleOpenInstallModal()"
>
安装主题
</VButton>
@ -510,7 +350,8 @@ const handleOpenPreview = (theme: Theme) => {
<ThemeUploadModal
v-if="visible"
v-model:visible="themeInstall"
v-model:visible="themeUploadVisible"
:upgrade-theme="themeToUpgrade"
@close="handleFetchThemes"
/>

View File

@ -0,0 +1,206 @@
<script lang="ts" setup>
import {
IconGitHub,
VButton,
VSpace,
VTag,
VEntity,
VEntityField,
VStatusDot,
Dialog,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { toRefs } from "vue";
import { useThemeLifeCycle } from "../../composables/use-theme";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults(
defineProps<{
theme: Theme;
isSelected?: boolean;
}>(),
{
isSelected: false,
}
);
const emit = defineEmits<{
(event: "reload"): void;
(event: "upgrade"): void;
(event: "preview"): void;
}>();
const { theme } = toRefs(props);
const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
Dialog.warning({
title: `${
deleteExtensions
? "确定要删除该主题以及对应的配置吗?"
: "确定要删除该主题吗?"
}`,
description: "该操作不可恢复。",
onConfirm: async () => {
try {
await apiClient.extension.theme.deletethemeHaloRunV1alpha1Theme({
name: theme.metadata.name,
});
// delete theme setting and configMap
if (!deleteExtensions) {
return;
}
const { settingName, configMapName } = theme.spec;
if (settingName) {
await apiClient.extension.setting.deletev1alpha1Setting(
{
name: settingName,
},
{
mute: true,
}
);
}
if (configMapName) {
await apiClient.extension.configMap.deletev1alpha1ConfigMap(
{
name: configMapName,
},
{
mute: true,
}
);
}
} catch (e) {
console.error("Failed to uninstall theme", e);
} finally {
emit("reload");
}
},
});
};
</script>
<template>
<VEntity :is-selected="isSelected">
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<template #loading>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-gray-400">加载中...</span>
</div>
</template>
<template #error>
<div
class="flex h-full items-center justify-center object-cover"
>
<span class="text-xs text-red-400">加载异常</span>
</div>
</template>
</LazyImage>
</div>
</div>
</template>
</VEntityField>
<VEntityField
:title="theme.spec.displayName"
:description="theme.spec.version"
>
<template #extra>
<VTag v-if="isActivated"> </VTag>
</template>
</VEntityField>
</template>
<template #end>
<VEntityField v-if="theme.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
</template>
<template
v-if="currentUserHasPermission(['system:themes:manage'])"
#dropdownItems
>
<VButton v-close-popper block type="secondary" @click="handleActiveTheme">
启用
</VButton>
<VButton v-close-popper block type="default" @click="emit('upgrade')">
升级
</VButton>
<VButton v-close-popper block type="default" @click="emit('preview')">
预览
</VButton>
<FloatingDropdown class="w-full" placement="right" :triggers="['click']">
<VButton block type="danger"> 卸载 </VButton>
<template #popper>
<div class="w-52 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-close-popper.all
block
type="danger"
@click="handleUninstall(theme)"
>
卸载
</VButton>
<VButton
v-close-popper.all
block
type="danger"
@click="handleUninstall(theme, true)"
>
卸载并删除配置
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</template>
</VEntity>
</template>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import ThemeListItem from "./ThemeListItem.vue";
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
import { useSettingForm } from "@/composables/use-setting-form";
import { useThemeStore } from "@/stores/theme";
import { apiClient } from "@/utils/api-client";
@ -339,7 +339,7 @@ const iframeClasses = computed(() => {
:key="index"
@click="handleSelect(item)"
>
<ThemeListItem
<ThemePreviewListItem
:theme="item"
:is-selected="
selectedTheme?.metadata.name === item.metadata.name

View File

@ -169,12 +169,6 @@ onMounted(() => {
</template>
<template #actions>
<VSpace>
<VButton size="sm" type="default" @click="themesModal = true">
<template #icon>
<IconExchange class="h-full w-full" />
</template>
切换主题
</VButton>
<VButton
v-if="!isActivated"
v-permission="['system:themes:manage']"
@ -190,6 +184,12 @@ onMounted(() => {
</template>
预览
</VButton>
<VButton size="sm" type="default" @click="themesModal = true">
<template #icon>
<IconExchange class="h-full w-full" />
</template>
主题管理
</VButton>
</VSpace>
</template>
</VPageHeader>