feat: add support for displaying and installing uninstalled themes (halo-dev/console#648)

#### What type of PR is this?

/kind feature
/milestone 2.0

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

支持显示和安装**未安装**的主题,以方便主题开发的时候,创建主题资源。适配 https://github.com/halo-dev/halo/pull/2586

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

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

#### Screenshots:

<img width="1663" alt="image" src="https://user-images.githubusercontent.com/21301288/196148567-f43b1bf3-e745-4c1a-950d-65899c1ae73c.png">

#### Special notes for your reviewer:

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

测试方式:

1. 需要 `pnpm install`
2. Halo 需要切换到 https://github.com/halo-dev/halo/pull/2586 PR 的分支。
3. 在本地的 `~/halo-dev/themes` 创建新的主题,需要包含 `themes.yaml`,或者将现有的主题直接下载到 `~/halo-dev/themes`
4. 检查后台主题管理的主题列表中是否显示了未安装的主题,以及测试是否可以安装成功。

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

```release-note
支持显示和安装未安装的主题,以方便主题开发的时候,创建主题资源。
```
pull/3445/head
Ryan Wang 2022-10-18 18:12:11 +08:00 committed by GitHub
parent a0512e43bc
commit 97f0d99538
1 changed files with 288 additions and 133 deletions

View File

@ -11,10 +11,12 @@ import {
VEntity, VEntity,
VEntityField, VEntityField,
VStatusDot, VStatusDot,
VTabItem,
VTabs,
} from "@halo-dev/components"; } from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue"; import LazyImage from "@/components/image/LazyImage.vue";
import ThemeInstallModal from "./ThemeInstallModal.vue"; import ThemeInstallModal from "./ThemeInstallModal.vue";
import { ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import type { Theme } from "@halo-dev/api-client"; import type { Theme } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
@ -41,15 +43,22 @@ const emit = defineEmits<{
(event: "select", theme: Theme | null): void; (event: "select", theme: Theme | null): void;
}>(); }>();
const themes = ref<Theme[]>([]); const activeTab = ref("installed");
const themes = ref<Theme[]>([] as Theme[]);
const loading = ref(false); const loading = ref(false);
const themeInstall = ref(false); const themeInstall = ref(false);
const creating = ref(false);
const modalTitle = computed(() => {
return activeTab.value === "installed" ? "已安装的主题" : "未安装的主题";
});
const handleFetchThemes = async () => { const handleFetchThemes = async () => {
try { try {
loading.value = true; loading.value = true;
const { data } = const { data } = await apiClient.theme.listThemes({
await apiClient.extension.theme.listthemeHaloRunV1alpha1Theme(); uninstalled: activeTab.value !== "installed",
});
themes.value = data.items; themes.value = data.items;
} catch (e) { } catch (e) {
console.error("Failed to fetch themes", e); console.error("Failed to fetch themes", e);
@ -58,6 +67,13 @@ const handleFetchThemes = async () => {
} }
}; };
watch(
() => activeTab.value,
() => {
handleFetchThemes();
}
);
const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => { const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
Dialog.warning({ Dialog.warning({
title: `${ title: `${
@ -99,6 +115,27 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
}); });
}; };
const handleCreateTheme = async (theme: Theme) => {
try {
creating.value = true;
const { data } =
await apiClient.extension.theme.createthemeHaloRunV1alpha1Theme({
theme,
});
// create theme settings
apiClient.theme.reloadThemeSetting({ name: data.metadata.name });
activeTab.value = "installed";
} catch (error) {
console.error("Failed to create theme", error);
} finally {
creating.value = false;
handleFetchThemes();
}
};
const onVisibleChange = (visible: boolean) => { const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
if (!visible) { if (!visible) {
@ -131,143 +168,261 @@ defineExpose({
:visible="visible" :visible="visible"
:width="888" :width="888"
height="calc(100vh - 20px)" height="calc(100vh - 20px)"
title="已安装的主题" :title="modalTitle"
@update:visible="onVisibleChange" @update:visible="onVisibleChange"
> >
<VEmpty <VTabs
v-if="!themes.length && !loading" v-model:active-id="activeTab"
message="当前没有已安装的主题,你可以尝试刷新或者安装新主题" type="outline"
title="当前没有已安装的主题" class="my-[12px] mx-[16px]"
> >
<template #actions> <VTabItem id="installed" label="已安装" class="-mx-[16px]">
<VSpace> <VEmpty
<VButton @click="handleFetchThemes"> </VButton> v-if="!themes.length && !loading"
<VButton message="当前没有已安装的主题,你可以尝试刷新或者安装新主题"
v-permission="['system:themes:manage']" title="当前没有已安装的主题"
type="primary"
@click="themeInstall = true"
>
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
安装主题
</VButton>
</VSpace>
</template>
</VEmpty>
<ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li
v-for="(theme, index) in themes"
:key="index"
@click="handleSelectTheme(theme)"
>
<VEntity
:is-selected="theme.metadata.name === selectedTheme?.metadata?.name"
> >
<template #start> <template #actions>
<VEntityField> <VSpace>
<template #description> <VButton :loading="loading" @click="handleFetchThemes">
<div class="w-32"> 刷新
<div </VButton>
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100" <VButton
> v-permission="['system:themes:manage']"
<LazyImage type="primary"
:src="theme.spec.logo" @click="themeInstall = true"
:alt="theme.spec.displayName" >
classes="pointer-events-none object-cover group-hover:opacity-75" <template #icon>
> <IconAddCircle class="h-full w-full" />
<template #loading> </template>
<div 安装主题
class="flex h-full items-center justify-center object-cover" </VButton>
> </VSpace>
<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>
<template #end> </VEmpty>
<VEntityField v-if="theme.metadata.deletionTimestamp">
<template #description> <ul
<VStatusDot v-tooltip="``" state="warning" animate /> v-else
</template> class="box-border h-full w-full divide-y divide-gray-100"
</VEntityField> role="list"
<VEntityField> >
<template #description> <li
<a v-for="(theme, index) in themes"
class="text-sm text-gray-400 hover:text-blue-600" :key="index"
:href="theme.spec.author.website" @click="handleSelectTheme(theme)"
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 <VEntity
v-close-popper :is-selected="
block theme.metadata.name === selectedTheme?.metadata?.name
type="danger" "
@click="handleUninstall(theme)"
> >
卸载 <template #start>
</VButton> <VEntityField>
<VButton <template #description>
v-close-popper <div class="w-32">
:disabled="theme.metadata.name === activatedTheme?.metadata?.name" <div
block class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
type="danger" >
@click="handleUninstall(theme, true)" <LazyImage
> :key="theme.metadata.name"
卸载并删除配置 :src="theme.spec.logo"
</VButton> :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="danger"
@click="handleUninstall(theme)"
>
卸载
</VButton>
<VButton
v-close-popper
:disabled="
theme.metadata.name === activatedTheme?.metadata?.name
"
block
type="danger"
@click="handleUninstall(theme, true)"
>
卸载并删除配置
</VButton>
</template>
</VEntity>
</li>
</ul>
</VTabItem>
<VTabItem id="uninstalled" label="未安装" class="-mx-[16px]">
<VEmpty v-if="!themes.length && !loading" title="当前没有未安装的主题">
<template #actions>
<VSpace>
<VButton :loading="loading" @click="handleFetchThemes">
刷新
</VButton>
</VSpace>
</template> </template>
</VEntity> </VEmpty>
</li>
</ul> <ul
v-else
class="box-border h-full w-full divide-y divide-gray-100"
role="list"
>
<li v-for="(theme, index) in themes" :key="index">
<VEntity>
<template #start>
<VEntityField>
<template #description>
<div class="w-32">
<div
class="group aspect-w-4 aspect-h-3 block w-full overflow-hidden rounded border bg-gray-100"
>
<LazyImage
:key="theme.metadata.name"
:src="theme.spec.logo"
:alt="theme.spec.displayName"
classes="pointer-events-none object-cover group-hover:opacity-75"
>
<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"
>
</VEntityField>
</template>
<template #end>
<VEntityField>
<template #description>
<a
class="text-sm text-gray-400 hover:text-blue-600"
:href="theme.spec.author.website"
target="_blank"
@click.stop
>
{{ theme.spec.author.name }}
</a>
</template>
</VEntityField>
<VEntityField>
<template #description>
<a
:href="theme.spec.repo"
class="text-gray-900 hover:text-blue-600"
target="_blank"
>
<IconGitHub />
</a>
</template>
</VEntityField>
<VEntityField v-permission="['system:themes:manage']">
<template #description>
<VButton
size="sm"
:disabled="creating"
@click="handleCreateTheme(theme)"
>
安装
</VButton>
</template>
</VEntityField>
</template>
</VEntity>
</li>
</ul>
</VTabItem>
</VTabs>
<template #footer> <template #footer>
<VSpace> <VSpace>
<VButton <VButton