mirror of https://github.com/halo-dev/halo
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
parent
a0512e43bc
commit
97f0d99538
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue