feat: add empty state support for core modules

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/3445/head
Ryan Wang 2022-08-22 16:49:07 +08:00
parent 02997e1d55
commit a65649b1cd
9 changed files with 172 additions and 22 deletions

View File

@ -16,3 +16,4 @@ export * from "./components/switch";
export * from "./components/dialog"; export * from "./components/dialog";
export * from "./components/pagination"; export * from "./components/pagination";
export * from "./components/codemirror"; export * from "./components/codemirror";
export * from "./components/empty";

View File

@ -90,6 +90,6 @@ describe("Empty", () => {
expect(attributes.src).not.toEqual("/src/components/empty/Empty.svg"); expect(attributes.src).not.toEqual("/src/components/empty/Empty.svg");
expect(attributes.src).toEqual("./empty"); expect(attributes.src).toEqual("./empty");
expect(attributes.alt).toEqual("Empty Status") expect(attributes.alt).toEqual("Empty Status");
}); });
}); });

View File

@ -6,6 +6,7 @@ import {
IconSettings, IconSettings,
VButton, VButton,
VCard, VCard,
VEmpty,
VPageHeader, VPageHeader,
VPagination, VPagination,
VSpace, VSpace,
@ -78,7 +79,24 @@ useExtensionPointsState("PAGES", pagesPublicState);
></VTabbar> ></VTabbar>
</template> </template>
<div v-if="activeId === 'functional'"> <div v-if="activeId === 'functional'">
<VEmpty
v-if="!pagesPublicState.functionalPages.length"
message="当前没有功能页面,功能页面通常由各个插件提供,你可以尝试安装新插件以获得支持"
title="当前没有功能页面"
>
<template #actions>
<VSpace>
<VButton :route="{ name: 'Plugins' }" type="primary">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
安装插件
</VButton>
</VSpace>
</template>
</VEmpty>
<ul <ul
v-else
class="box-border h-full w-full divide-y divide-gray-100" class="box-border h-full w-full divide-y divide-gray-100"
role="list" role="list"
> >

View File

@ -1,9 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconAddCircle,
IconListSettings, IconListSettings,
useDialog, useDialog,
VButton, VButton,
VCard, VCard,
VEmpty,
VPageHeader, VPageHeader,
VSpace, VSpace,
} from "@halo-dev/components"; } from "@halo-dev/components";
@ -28,6 +30,7 @@ const menuItems = ref<MenuItem[]>([] as MenuItem[]);
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]); const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
const selectedMenu = ref<Menu | undefined>(); const selectedMenu = ref<Menu | undefined>();
const selectedMenuItem = ref<MenuItem | null>(null); const selectedMenuItem = ref<MenuItem | null>(null);
const loading = ref(false);
const menuListRef = ref(); const menuListRef = ref();
const menuItemEditingModal = ref(); const menuItemEditingModal = ref();
@ -35,6 +38,8 @@ const dialog = useDialog();
const handleFetchMenuItems = async () => { const handleFetchMenuItems = async () => {
try { try {
loading.value = true;
if (!selectedMenu.value?.spec.menuItems) { if (!selectedMenu.value?.spec.menuItems) {
return; return;
} }
@ -52,6 +57,8 @@ const handleFetchMenuItems = async () => {
menuTreeItems.value = buildMenuItemsTree(data.items); menuTreeItems.value = buildMenuItemsTree(data.items);
} catch (e) { } catch (e) {
console.error("Failed to fetch menu items", e); console.error("Failed to fetch menu items", e);
} finally {
loading.value = false;
} }
}; };
@ -181,7 +188,25 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
</div> </div>
</div> </div>
</template> </template>
<VEmpty
v-if="!menuItems.length && !loading"
message="你可以尝试刷新或者新建菜单项"
title="当前没有菜单项"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchMenuItems"> </VButton>
<VButton type="primary" @click="menuItemEditingModal = true">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新增菜单项
</VButton>
</VSpace>
</template>
</VEmpty>
<MenuItemListItem <MenuItemListItem
v-else
:menu-tree-items="menuTreeItems" :menu-tree-items="menuTreeItems"
@change="handleUpdateInBatch" @change="handleUpdateInBatch"
@delete="handleDelete" @delete="handleDelete"

View File

@ -4,6 +4,7 @@ import {
useDialog, useDialog,
VButton, VButton,
VCard, VCard,
VEmpty,
VSpace, VSpace,
} from "@halo-dev/components"; } from "@halo-dev/components";
import MenuEditingModal from "./MenuEditingModal.vue"; import MenuEditingModal from "./MenuEditingModal.vue";
@ -27,6 +28,7 @@ const emit = defineEmits<{
}>(); }>();
const menus = ref<Menu[]>([] as Menu[]); const menus = ref<Menu[]>([] as Menu[]);
const loading = ref(false);
const selectedMenuToUpdate = ref<Menu | null>(null); const selectedMenuToUpdate = ref<Menu | null>(null);
const menuEditingModal = ref<boolean>(false); const menuEditingModal = ref<boolean>(false);
@ -35,6 +37,8 @@ const dialog = useDialog();
const handleFetchMenus = async () => { const handleFetchMenus = async () => {
selectedMenuToUpdate.value = null; selectedMenuToUpdate.value = null;
try { try {
loading.value = true;
const { data } = await apiClient.extension.menu.listv1alpha1Menu(); const { data } = await apiClient.extension.menu.listv1alpha1Menu();
menus.value = data.items; menus.value = data.items;
@ -49,6 +53,8 @@ const handleFetchMenus = async () => {
} }
} catch (e) { } catch (e) {
console.error("Failed to fetch menus", e); console.error("Failed to fetch menus", e);
} finally {
loading.value = false;
} }
}; };
@ -114,6 +120,17 @@ defineExpose({
@close="handleFetchMenus" @close="handleFetchMenus"
/> />
<VCard :bodyClass="['!p-0']" title="菜单"> <VCard :bodyClass="['!p-0']" title="菜单">
<VEmpty
v-if="!menus.length && !loading"
message="你可以尝试刷新或者新建菜单"
title="当前没有菜单"
>
<template #actions>
<VSpace>
<VButton size="sm" @click="handleFetchMenus"> </VButton>
</VSpace>
</template>
</VEmpty>
<div class="divide-y divide-gray-100 bg-white"> <div class="divide-y divide-gray-100 bg-white">
<div <div
v-for="(menu, index) in menus" v-for="(menu, index) in menus"

View File

@ -1,9 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconAddCircle,
IconGitHub, IconGitHub,
IconMore, IconMore,
useDialog, useDialog,
VButton, VButton,
VEmpty,
VModal, VModal,
VSpace, VSpace,
VTag, VTag,
@ -33,17 +35,21 @@ const emit = defineEmits<{
}>(); }>();
const themes = ref<Theme[]>([]); const themes = ref<Theme[]>([]);
const loading = ref(false);
const themeInstall = ref(false); const themeInstall = ref(false);
const dialog = useDialog(); const dialog = useDialog();
const handleFetchThemes = async () => { const handleFetchThemes = async () => {
try { try {
loading.value = true;
const { data } = const { data } =
await apiClient.extension.theme.listthemeHaloRunV1alpha1Theme(); await apiClient.extension.theme.listthemeHaloRunV1alpha1Theme();
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);
} finally {
loading.value = false;
} }
}; };
@ -91,7 +97,25 @@ defineExpose({
title="已安装的主题" title="已安装的主题"
@update:visible="handleVisibleChange" @update:visible="handleVisibleChange"
> >
<ul class="flex flex-col divide-y divide-gray-100" role="list"> <VEmpty
v-if="!themes.length && !loading"
message="当前没有已安装的主题,你可以尝试刷新或者安装新主题"
title="当前没有已安装的主题"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchThemes"> </VButton>
<VButton type="primary" @click="themeInstall = true">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新增菜单项
</VButton>
</VSpace>
</template>
</VEmpty>
<ul v-else class="flex flex-col divide-y divide-gray-100" role="list">
<li <li
v-for="(theme, index) in themes" v-for="(theme, index) in themes"
:key="index" :key="index"

View File

@ -5,6 +5,7 @@ import { apiClient } from "@halo-dev/admin-shared";
import { useDialog } from "@halo-dev/components"; import { useDialog } from "@halo-dev/components";
interface useThemeLifeCycleReturn { interface useThemeLifeCycleReturn {
loading: Ref<boolean>;
activatedTheme: Ref<Theme>; activatedTheme: Ref<Theme>;
isActivated: ComputedRef<boolean>; isActivated: ComputedRef<boolean>;
handleActiveTheme: () => void; handleActiveTheme: () => void;
@ -12,6 +13,7 @@ interface useThemeLifeCycleReturn {
export function useThemeLifeCycle(theme: Ref<Theme>): useThemeLifeCycleReturn { export function useThemeLifeCycle(theme: Ref<Theme>): useThemeLifeCycleReturn {
const activatedTheme = ref<Theme>({} as Theme); const activatedTheme = ref<Theme>({} as Theme);
const loading = ref(false);
const isActivated = computed(() => { const isActivated = computed(() => {
return activatedTheme.value?.metadata?.name === theme.value?.metadata?.name; return activatedTheme.value?.metadata?.name === theme.value?.metadata?.name;
@ -21,6 +23,8 @@ export function useThemeLifeCycle(theme: Ref<Theme>): useThemeLifeCycleReturn {
const handleFetchActivatedTheme = async () => { const handleFetchActivatedTheme = async () => {
try { try {
loading.value = true;
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap( const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap(
"system" "system"
); );
@ -40,6 +44,8 @@ export function useThemeLifeCycle(theme: Ref<Theme>): useThemeLifeCycleReturn {
activatedTheme.value = themeData; activatedTheme.value = themeData;
} catch (e) { } catch (e) {
console.error("Failed to fetch active theme", e); console.error("Failed to fetch active theme", e);
} finally {
loading.value = false;
} }
}; };
@ -76,6 +82,7 @@ export function useThemeLifeCycle(theme: Ref<Theme>): useThemeLifeCycleReturn {
onMounted(handleFetchActivatedTheme); onMounted(handleFetchActivatedTheme);
return { return {
loading,
activatedTheme, activatedTheme,
isActivated, isActivated,
handleActiveTheme, handleActiveTheme,

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs // core libs
import type { ComputedRef, Ref } from "vue";
import { computed, provide, ref, watch, watchEffect } from "vue"; import { computed, provide, ref, watch, watchEffect } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
@ -8,7 +9,9 @@ import cloneDeep from "lodash.clonedeep";
// hooks // hooks
import { useThemeLifeCycle } from "../composables/use-theme"; import { useThemeLifeCycle } from "../composables/use-theme";
import { useSettingForm } from "@halo-dev/admin-shared"; // types
import type { FormKitSettingSpec } from "@halo-dev/admin-shared";
import { BasicLayout, useSettingForm } from "@halo-dev/admin-shared";
// components // components
import { import {
@ -17,16 +20,12 @@ import {
IconPalette, IconPalette,
VButton, VButton,
VCard, VCard,
VEmpty,
VPageHeader, VPageHeader,
VSpace, VSpace,
VTabbar, VTabbar,
} from "@halo-dev/components"; } from "@halo-dev/components";
import ThemeListModal from "../components/ThemeListModal.vue"; import ThemeListModal from "../components/ThemeListModal.vue";
import { BasicLayout } from "@halo-dev/admin-shared";
// types
import type { FormKitSettingSpec } from "@halo-dev/admin-shared";
import type { ComputedRef, Ref } from "vue";
import type { Theme } from "@halo-dev/api-client"; import type { Theme } from "@halo-dev/api-client";
interface ThemeTab { interface ThemeTab {
@ -53,7 +52,7 @@ const selectedTheme = ref<Theme>({} as Theme);
const themesModal = ref(false); const themesModal = ref(false);
const activeTab = ref(""); const activeTab = ref("");
const { isActivated, activatedTheme, handleActiveTheme } = const { loading, isActivated, activatedTheme, handleActiveTheme } =
useThemeLifeCycle(selectedTheme); useThemeLifeCycle(selectedTheme);
const settingName = computed(() => selectedTheme.value.spec?.settingName); const settingName = computed(() => selectedTheme.value.spec?.settingName);
@ -171,19 +170,45 @@ watch(
</VPageHeader> </VPageHeader>
<div class="m-0 md:m-4"> <div class="m-0 md:m-4">
<VCard :body-class="['!p-0']"> <VEmpty
<template #header> v-if="
<VTabbar !selectedTheme.metadata?.name &&
v-model:active-id="activeTab" !activatedTheme.metadata?.name &&
:items="tabs" !loading
class="w-full !rounded-none" "
type="outline" message="当前没有已激活或者选择的主题,你可以切换主题或者安装新主题"
@change="handleTabChange" title="当前没有已激活或已选择的主题"
></VTabbar> >
<template #actions>
<VSpace>
<VButton @click="themesModal = true">
安装主题
</VButton>
<VButton type="primary" @click="themesModal = true">
<template #icon>
<IconExchange class="h-full w-full" />
</template>
切换主题
</VButton>
</VSpace>
</template> </template>
</VCard> </VEmpty>
<div>
<RouterView :key="activeTab" /> <div v-else>
<VCard :body-class="['!p-0']">
<template #header>
<VTabbar
v-model:active-id="activeTab"
:items="tabs"
class="w-full !rounded-none"
type="outline"
@change="handleTabChange"
></VTabbar>
</template>
</VCard>
<div>
<RouterView :key="activeTab" />
</div>
</div> </div>
</div> </div>
</BasicLayout> </BasicLayout>

View File

@ -5,6 +5,7 @@ import {
IconPlug, IconPlug,
VButton, VButton,
VCard, VCard,
VEmpty,
VPageHeader, VPageHeader,
VPagination, VPagination,
VSpace, VSpace,
@ -17,11 +18,14 @@ import { apiClient } from "@halo-dev/admin-shared";
import type { Plugin } from "@halo-dev/api-client"; import type { Plugin } from "@halo-dev/api-client";
const plugins = ref<Plugin[]>([] as Plugin[]); const plugins = ref<Plugin[]>([] as Plugin[]);
const loading = ref(false);
const pluginInstall = ref(false); const pluginInstall = ref(false);
const keyword = ref(""); const keyword = ref("");
const handleFetchPlugins = async () => { const handleFetchPlugins = async () => {
try { try {
loading.value = true;
const fieldSelector: Array<string> = []; const fieldSelector: Array<string> = [];
if (keyword.value) { if (keyword.value) {
@ -38,6 +42,8 @@ const handleFetchPlugins = async () => {
plugins.value = data.items; plugins.value = data.items;
} catch (e) { } catch (e) {
console.error("Fail to fetch plugins", e); console.error("Fail to fetch plugins", e);
} finally {
loading.value = false;
} }
}; };
@ -211,7 +217,34 @@ onMounted(handleFetchPlugins);
</div> </div>
</div> </div>
</template> </template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<VEmpty
v-if="!plugins.length && !loading"
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
title="当前没有已安装的插件"
>
<template #actions>
<VSpace>
<VButton @click="handleFetchPlugins"></VButton>
<VButton
v-permission="['system:plugins:manage']"
type="secondary"
@click="pluginInstall = 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="(plugin, index) in plugins" :key="index"> <li v-for="(plugin, index) in plugins" :key="index">
<PluginListItem :plugin="plugin" /> <PluginListItem :plugin="plugin" />
</li> </li>