mirror of https://github.com/halo-dev/halo
feat: add stack widget for dashboard (#7525)
#### What type of PR is this? /area ui /kind feautre /milestone 2.21.x #### What this PR does / why we need it: This PR adds a core Dashboard Widget called Stack Widget, which is used to stack a batch of components in the same location and supports manual or automatic switching. <img width="737" alt="image" src="https://github.com/user-attachments/assets/de448cd8-cf62-4608-8523-88395298e734" /> <img width="827" alt="image" src="https://github.com/user-attachments/assets/fecaf637-9cb8-444a-888b-0ee2a9700bc7" /> #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 为仪表盘添加堆叠小部件 ```pull/7527/head
parent
3fa6532d9b
commit
315073406f
|
@ -0,0 +1,8 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="h-full text-base w-8 flex cursor-pointer items-center justify-center hover:opacity-80 text-white transition-all"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
DashboardWidgetDefinition,
|
DashboardWidgetDefinition,
|
||||||
} from "@halo-dev/console-shared";
|
} from "@halo-dev/console-shared";
|
||||||
import { computed, inject, ref, type ComputedRef } from "vue";
|
import { computed, inject, ref, type ComputedRef } from "vue";
|
||||||
|
import ActionButton from "./ActionButton.vue";
|
||||||
import WidgetConfigFormModal from "./WidgetConfigFormModal.vue";
|
import WidgetConfigFormModal from "./WidgetConfigFormModal.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -61,21 +62,18 @@ function handleSaveConfig(config: Record<string, unknown>) {
|
||||||
@update:config="handleSaveConfig"
|
@update:config="handleSaveConfig"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute hidden h-8 right-0 top-0 rounded-tr-lg bg-gray-100 overflow-hidden group-hover/grid-item:inline-flex items-center"
|
class="absolute z-[100] hidden h-8 right-0 top-0 rounded-tr-lg bg-gray-100 overflow-hidden group-hover/grid-item:inline-flex items-center"
|
||||||
>
|
>
|
||||||
<div
|
<ActionButton
|
||||||
v-if="widgetDefinition?.configFormKitSchema?.length"
|
v-if="widgetDefinition?.configFormKitSchema?.length"
|
||||||
class="h-full w-8 flex cursor-pointer items-center justify-center bg-black hover:bg-gray-800 text-white"
|
class="bg-black"
|
||||||
@click="configModalVisible = true"
|
@click="configModalVisible = true"
|
||||||
>
|
>
|
||||||
<IconSettings class="text-base" />
|
<IconSettings />
|
||||||
</div>
|
</ActionButton>
|
||||||
<div
|
<ActionButton class="bg-red-500" @click="emit('remove')">
|
||||||
class="h-full w-8 flex cursor-pointer items-center justify-center bg-red-500 hover:bg-red-600 text-white"
|
<IconCloseCircle />
|
||||||
@click="emit('remove')"
|
</ActionButton>
|
||||||
>
|
|
||||||
<IconCloseCircle class="text-base" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</grid-item>
|
</grid-item>
|
||||||
<WidgetConfigFormModal
|
<WidgetConfigFormModal
|
||||||
|
|
|
@ -67,6 +67,7 @@ const groupWidgetDefinitionsKeys = computed(() => {
|
||||||
:width="1380"
|
:width="1380"
|
||||||
:layer-closable="true"
|
:layer-closable="true"
|
||||||
:title="$t('core.dashboard_designer.widgets_modal.title')"
|
:title="$t('core.dashboard_designer.widgets_modal.title')"
|
||||||
|
mount-to-body
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<VTabbar
|
<VTabbar
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { markRaw } from "vue";
|
||||||
import CommentStatsWidget from "./presets/comments/CommentStatsWidget.vue";
|
import CommentStatsWidget from "./presets/comments/CommentStatsWidget.vue";
|
||||||
import PendingCommentsWidget from "./presets/comments/PendingCommentsWidget.vue";
|
import PendingCommentsWidget from "./presets/comments/PendingCommentsWidget.vue";
|
||||||
import QuickActionWidget from "./presets/core/quick-action/QuickActionWidget.vue";
|
import QuickActionWidget from "./presets/core/quick-action/QuickActionWidget.vue";
|
||||||
|
import StackWidget from "./presets/core/stack/StackWidget.vue";
|
||||||
import ViewsStatsWidget from "./presets/core/view-stats/ViewsStatsWidget.vue";
|
import ViewsStatsWidget from "./presets/core/view-stats/ViewsStatsWidget.vue";
|
||||||
import PostStatsWidget from "./presets/posts/PostStatsWidget.vue";
|
import PostStatsWidget from "./presets/posts/PostStatsWidget.vue";
|
||||||
import RecentPublishedWidget from "./presets/posts/RecentPublishedWidget.vue";
|
import RecentPublishedWidget from "./presets/posts/RecentPublishedWidget.vue";
|
||||||
|
@ -182,4 +183,16 @@ export const internalWidgetDefinitions: DashboardWidgetDefinition[] = [
|
||||||
minW: 3,
|
minW: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "core:stack",
|
||||||
|
component: markRaw(StackWidget),
|
||||||
|
group: "core.dashboard.widgets.groups.other",
|
||||||
|
defaultConfig: {},
|
||||||
|
defaultSize: {
|
||||||
|
w: 6,
|
||||||
|
h: 12,
|
||||||
|
minH: 1,
|
||||||
|
minW: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import WidgetCard from "@console/modules/dashboard/components/WidgetCard.vue";
|
||||||
|
import { IconSettings, VButton } from "@halo-dev/components";
|
||||||
|
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||||
|
import StackWidgetConfigModal from "./StackWidgetConfigModal.vue";
|
||||||
|
import IndexIndicator from "./components/IndexIndicator.vue";
|
||||||
|
import WidgetViewItem from "./components/WidgetViewItem.vue";
|
||||||
|
import type { StackWidgetConfig } from "./types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: StackWidgetConfig;
|
||||||
|
editMode?: boolean;
|
||||||
|
previewMode?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:config", config: StackWidgetConfig): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const configVisible = ref(false);
|
||||||
|
|
||||||
|
const index = ref(0);
|
||||||
|
|
||||||
|
function handleNavigate(direction: -1 | 1) {
|
||||||
|
const targetIndex = index.value + direction;
|
||||||
|
|
||||||
|
if (targetIndex < 0) {
|
||||||
|
index.value = props.config.widgets.length - 1;
|
||||||
|
} else if (targetIndex >= props.config.widgets.length) {
|
||||||
|
index.value = 0;
|
||||||
|
} else {
|
||||||
|
index.value = targetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto play
|
||||||
|
const autoPlayInterval = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
function startAutoPlay() {
|
||||||
|
if (!props.config.auto_play || configVisible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (autoPlayInterval.value) {
|
||||||
|
clearInterval(autoPlayInterval.value);
|
||||||
|
}
|
||||||
|
autoPlayInterval.value = setInterval(() => {
|
||||||
|
handleNavigate(1);
|
||||||
|
}, props.config.auto_play_interval || 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoPlay() {
|
||||||
|
if (autoPlayInterval.value) {
|
||||||
|
clearInterval(autoPlayInterval.value);
|
||||||
|
autoPlayInterval.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSave(config: StackWidgetConfig) {
|
||||||
|
emit("update:config", config);
|
||||||
|
configVisible.value = false;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (config.auto_play) {
|
||||||
|
startAutoPlay();
|
||||||
|
} else {
|
||||||
|
stopAutoPlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<WidgetCard
|
||||||
|
v-if="!config.widgets?.length"
|
||||||
|
:title="$t('core.dashboard.widgets.presets.stack.title')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center w-full h-full">
|
||||||
|
<VButton @click="configVisible = true">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.dashboard.widgets.presets.stack.operations.add_widget.button"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</VButton>
|
||||||
|
</div>
|
||||||
|
</WidgetCard>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="bg-white w-full h-full rounded-lg relative group/stack-item overflow-hidden"
|
||||||
|
@mouseenter="stopAutoPlay"
|
||||||
|
@mouseleave="startAutoPlay"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="editMode || previewMode"
|
||||||
|
class="flex absolute z-10 bg-white inset-0 rounded-t-lg flex-none justify-between h-10 items-center px-4 border-b border-[#eaecf0]"
|
||||||
|
>
|
||||||
|
<div class="inline-flex items-center gap-2">
|
||||||
|
<div class="text-base font-medium flex-1 shrink">
|
||||||
|
{{ $t("core.dashboard.widgets.presets.stack.title") }}
|
||||||
|
</div>
|
||||||
|
<IconSettings
|
||||||
|
v-if="editMode"
|
||||||
|
class="hover:text-gray-600 cursor-pointer"
|
||||||
|
@click="configVisible = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TransitionGroup name="fade">
|
||||||
|
<WidgetViewItem
|
||||||
|
v-for="(widget, i) in config.widgets"
|
||||||
|
v-show="index === i"
|
||||||
|
:key="widget.i"
|
||||||
|
:item="widget"
|
||||||
|
/>
|
||||||
|
</TransitionGroup>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute bottom-2 left-0 right-0 z-10 flex justify-center group-hover/stack-item:opacity-100 opacity-0 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<IndexIndicator
|
||||||
|
:index="index"
|
||||||
|
:total="config.widgets.length"
|
||||||
|
@prev="handleNavigate(-1)"
|
||||||
|
@next="handleNavigate(1)"
|
||||||
|
@update:index="index = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StackWidgetConfigModal
|
||||||
|
v-if="configVisible"
|
||||||
|
:config="config"
|
||||||
|
@close="configVisible = false"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -0,0 +1,205 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { randomUUID } from "@/utils/id";
|
||||||
|
import ActionButton from "@console/modules/dashboard/components/ActionButton.vue";
|
||||||
|
import WidgetHubModal from "@console/modules/dashboard/components/WidgetHubModal.vue";
|
||||||
|
import {
|
||||||
|
IconArrowDownLine,
|
||||||
|
IconArrowUpLine,
|
||||||
|
Toast,
|
||||||
|
VButton,
|
||||||
|
VModal,
|
||||||
|
VSpace,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import type { DashboardWidgetDefinition } from "@halo-dev/console-shared";
|
||||||
|
import { cloneDeep } from "lodash-es";
|
||||||
|
import { onMounted, ref, toRaw, useTemplateRef } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import WidgetEditableItem from "./components/WidgetEditableItem.vue";
|
||||||
|
import type { SimpleWidget, StackWidgetConfig } from "./types";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: StackWidgetConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
(e: "save", config: StackWidgetConfig): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const widgets = ref<SimpleWidget[]>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
widgets.value = toRaw(props.config.widgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
|
||||||
|
|
||||||
|
const widgetsHubModalVisible = ref(false);
|
||||||
|
|
||||||
|
function handleAddWidget(widgetDefinition: DashboardWidgetDefinition) {
|
||||||
|
if (widgetDefinition.id === "core:stack") {
|
||||||
|
Toast.error(
|
||||||
|
t("core.dashboard.widgets.presets.stack.config_modal.toast.nest_warning")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets.value = [
|
||||||
|
...(widgets.value || []),
|
||||||
|
{
|
||||||
|
i: randomUUID(),
|
||||||
|
id: widgetDefinition.id,
|
||||||
|
config: widgetDefinition.defaultConfig,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
widgetsHubModalVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave(data: { auto_play: boolean; auto_play_interval: number }) {
|
||||||
|
emit("save", {
|
||||||
|
auto_play: data.auto_play,
|
||||||
|
auto_play_interval: data.auto_play_interval,
|
||||||
|
widgets: widgets.value || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveWidget(widget: SimpleWidget) {
|
||||||
|
widgets.value = widgets.value?.filter((w) => w.i !== widget.i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateWidgetConfig(
|
||||||
|
widget: SimpleWidget,
|
||||||
|
config: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
widgets.value = widgets.value?.map((w) =>
|
||||||
|
w.i === widget.i ? { ...w, config } : w
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveWidget(widget: SimpleWidget, direction: -1 | 1) {
|
||||||
|
const items = cloneDeep(widgets.value) || [];
|
||||||
|
const currentIndex = items.findIndex((item) => item.i === widget.i);
|
||||||
|
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
const targetIndex = currentIndex + direction;
|
||||||
|
|
||||||
|
if (targetIndex < 0 || targetIndex >= items.length) return;
|
||||||
|
|
||||||
|
[items[currentIndex], items[targetIndex]] = [
|
||||||
|
items[targetIndex],
|
||||||
|
items[currentIndex],
|
||||||
|
];
|
||||||
|
|
||||||
|
widgets.value = [...items];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
ref="modal"
|
||||||
|
mount-to-body
|
||||||
|
:title="$t('core.dashboard.widgets.presets.stack.config_modal.title')"
|
||||||
|
:width="800"
|
||||||
|
:centered="false"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-5">
|
||||||
|
<FormKit
|
||||||
|
id="stack-widget-config-form"
|
||||||
|
v-slot="{ value }"
|
||||||
|
type="form"
|
||||||
|
name="stack-widget-config-form"
|
||||||
|
:preserve="true"
|
||||||
|
@submit="handleSave"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
name="auto_play"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
'core.dashboard.widgets.presets.stack.config_modal.fields.auto_play.label'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:value="config.auto_play || false"
|
||||||
|
/>
|
||||||
|
<FormKit
|
||||||
|
v-if="value?.auto_play"
|
||||||
|
type="number"
|
||||||
|
number
|
||||||
|
name="auto_play_interval"
|
||||||
|
validation="required"
|
||||||
|
:value="config.auto_play_interval || 3000"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
'core.dashboard.widgets.presets.stack.config_modal.fields.auto_play_interval.label'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="py-4 flex flex-col gap-4">
|
||||||
|
<label
|
||||||
|
class="formkit-label block text-sm font-medium text-gray-700 formkit-invalid:text-red-500"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"core.dashboard.widgets.presets.stack.config_modal.fields.widgets.label"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-col gap-2 border border-dashed p-2 rounded-lg">
|
||||||
|
<WidgetEditableItem
|
||||||
|
v-for="(widget, index) in widgets"
|
||||||
|
:key="widget.id"
|
||||||
|
:item="widget"
|
||||||
|
@remove="handleRemoveWidget(widget)"
|
||||||
|
@update:config="handleUpdateWidgetConfig(widget, $event)"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<ActionButton
|
||||||
|
v-if="index > 0"
|
||||||
|
class="bg-gray-200"
|
||||||
|
@click="handleMoveWidget(widget, -1)"
|
||||||
|
>
|
||||||
|
<IconArrowUpLine class="text-gray-600" />
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
v-if="index < (widgets?.length || 0) - 1"
|
||||||
|
class="bg-gray-200"
|
||||||
|
@click="handleMoveWidget(widget, 1)"
|
||||||
|
>
|
||||||
|
<IconArrowDownLine class="text-gray-600" />
|
||||||
|
</ActionButton>
|
||||||
|
</template>
|
||||||
|
</WidgetEditableItem>
|
||||||
|
|
||||||
|
<div class="flex justify-left">
|
||||||
|
<VButton @click="widgetsHubModalVisible = true">
|
||||||
|
{{ $t("core.common.buttons.add") }}
|
||||||
|
</VButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormKit>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<VSpace>
|
||||||
|
<VButton
|
||||||
|
type="secondary"
|
||||||
|
@click="$formkit.submit('stack-widget-config-form')"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.save") }}
|
||||||
|
</VButton>
|
||||||
|
<VButton @click="emit('close')">
|
||||||
|
{{ $t("core.common.buttons.cancel") }}
|
||||||
|
</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
|
||||||
|
<WidgetHubModal
|
||||||
|
v-if="widgetsHubModalVisible"
|
||||||
|
@close="widgetsHubModalVisible = false"
|
||||||
|
@add-widget="handleAddWidget"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { IconArrowLeft, IconArrowRight } from "@halo-dev/components";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "prev"): void;
|
||||||
|
(e: "next"): void;
|
||||||
|
(e: "update:index", index: number): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-full p-1 flex gap-4 items-center hover:shadow transition-all"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="total > 1"
|
||||||
|
class="w-7 h-7 flex items-center justify-center rounded-full bg-transparent hover:bg-gray-100 transition-all duration-200 focus:outline-none group"
|
||||||
|
@click="emit('prev')"
|
||||||
|
>
|
||||||
|
<IconArrowLeft class="text-gray-400 group-hover:text-gray-900" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
v-for="i in total"
|
||||||
|
:key="i"
|
||||||
|
class="group cursor-pointer"
|
||||||
|
@click="emit('update:index', i - 1)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full transition-all duration-200 ease-in-out transform"
|
||||||
|
:class="{
|
||||||
|
'bg-primary scale-150': index === i - 1,
|
||||||
|
'bg-gray-300 group-hover:bg-gray-400': index !== i - 1,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="total > 1"
|
||||||
|
class="w-7 h-7 flex items-center justify-center rounded-full bg-transparent hover:bg-gray-100 transition-all duration-200 focus:outline-none group"
|
||||||
|
@click="emit('next')"
|
||||||
|
>
|
||||||
|
<IconArrowRight class="text-gray-400 group-hover:text-gray-900" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import ActionButton from "@console/modules/dashboard/components/ActionButton.vue";
|
||||||
|
import WidgetConfigFormModal from "@console/modules/dashboard/components/WidgetConfigFormModal.vue";
|
||||||
|
import { IconCloseCircle, IconSettings } from "@halo-dev/components";
|
||||||
|
import type { DashboardWidgetDefinition } from "@halo-dev/console-shared";
|
||||||
|
import { computed, inject, ref, type ComputedRef } from "vue";
|
||||||
|
import type { SimpleWidget } from "../types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: SimpleWidget;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "remove"): void;
|
||||||
|
(e: "update:config", config: Record<string, unknown>): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
const availableWidgetDefinitions = inject<
|
||||||
|
ComputedRef<DashboardWidgetDefinition[]>
|
||||||
|
>("availableWidgetDefinitions");
|
||||||
|
|
||||||
|
const widgetDefinition = computed(() => {
|
||||||
|
return availableWidgetDefinitions?.value?.find(
|
||||||
|
(definition) => definition.id === props.item.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultSize = computed(() => {
|
||||||
|
return widgetDefinition.value?.defaultSize || { w: 1, h: 1 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const configModalVisible = ref(false);
|
||||||
|
|
||||||
|
function handleSaveConfig(config: Record<string, unknown>) {
|
||||||
|
emit("update:config", config);
|
||||||
|
configModalVisible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="currentUserHasPermission(widgetDefinition?.permissions)"
|
||||||
|
class="group/grid-item relative"
|
||||||
|
:style="{
|
||||||
|
width: `${defaultSize.w * 100}px`,
|
||||||
|
height: `${defaultSize.h * 36}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="widgetDefinition?.component"
|
||||||
|
edit-mode
|
||||||
|
:config="item.config"
|
||||||
|
@update:config="handleSaveConfig"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute hidden h-8 right-0 top-0 rounded-tr-lg bg-gray-100 overflow-hidden group-hover/grid-item:inline-flex items-center"
|
||||||
|
>
|
||||||
|
<slot name="actions" />
|
||||||
|
<ActionButton
|
||||||
|
v-if="widgetDefinition?.configFormKitSchema?.length"
|
||||||
|
class="bg-black"
|
||||||
|
@click="configModalVisible = true"
|
||||||
|
>
|
||||||
|
<IconSettings />
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton class="bg-red-500" @click="emit('remove')">
|
||||||
|
<IconCloseCircle />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WidgetConfigFormModal
|
||||||
|
v-if="
|
||||||
|
widgetDefinition &&
|
||||||
|
widgetDefinition.configFormKitSchema &&
|
||||||
|
configModalVisible
|
||||||
|
"
|
||||||
|
:widget-definition="widgetDefinition"
|
||||||
|
:config="item.config"
|
||||||
|
@close="configModalVisible = false"
|
||||||
|
@save="handleSaveConfig"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import type { DashboardWidgetDefinition } from "@halo-dev/console-shared";
|
||||||
|
import { computed, inject, type ComputedRef } from "vue";
|
||||||
|
import type { SimpleWidget } from "../types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: SimpleWidget;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
const availableWidgetDefinitions = inject<
|
||||||
|
ComputedRef<DashboardWidgetDefinition[]>
|
||||||
|
>("availableWidgetDefinitions");
|
||||||
|
|
||||||
|
const widgetDefinition = computed(() => {
|
||||||
|
return availableWidgetDefinitions?.value?.find(
|
||||||
|
(definition) => definition.id === props.item.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="currentUserHasPermission(widgetDefinition?.permissions)"
|
||||||
|
class="relative w-full h-full"
|
||||||
|
>
|
||||||
|
<component :is="widgetDefinition?.component" :config="item.config" />
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface SimpleWidget {
|
||||||
|
i: string;
|
||||||
|
id: string;
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackWidgetConfig {
|
||||||
|
auto_play?: boolean;
|
||||||
|
auto_play_interval?: number;
|
||||||
|
widgets: SimpleWidget[];
|
||||||
|
}
|
|
@ -38,6 +38,22 @@ core:
|
||||||
label: Enabled Items
|
label: Enabled Items
|
||||||
pending_comments:
|
pending_comments:
|
||||||
title: Pending Comments
|
title: Pending Comments
|
||||||
|
stack:
|
||||||
|
title: Widget Stack
|
||||||
|
operations:
|
||||||
|
add_widget:
|
||||||
|
button: Add Widget
|
||||||
|
config_modal:
|
||||||
|
title: Widget Stack Config
|
||||||
|
fields:
|
||||||
|
auto_play:
|
||||||
|
label: Auto play
|
||||||
|
auto_play_interval:
|
||||||
|
label: Auto play interval
|
||||||
|
widgets:
|
||||||
|
label: Widgets
|
||||||
|
toast:
|
||||||
|
nest_warning: You cannot add a stack widget to a stack widget
|
||||||
dashboard_designer:
|
dashboard_designer:
|
||||||
title: Edit Dashboard
|
title: Edit Dashboard
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -108,6 +108,22 @@ core:
|
||||||
title: Pending Comments
|
title: Pending Comments
|
||||||
views_stats:
|
views_stats:
|
||||||
title: Visits
|
title: Visits
|
||||||
|
stack:
|
||||||
|
title: Widget Stack
|
||||||
|
operations:
|
||||||
|
add_widget:
|
||||||
|
button: Add Widget
|
||||||
|
config_modal:
|
||||||
|
title: Widget Stack Config
|
||||||
|
fields:
|
||||||
|
auto_play:
|
||||||
|
label: Auto play
|
||||||
|
auto_play_interval:
|
||||||
|
label: Auto play interval
|
||||||
|
widgets:
|
||||||
|
label: Widgets
|
||||||
|
toast:
|
||||||
|
nest_warning: You cannot add a stack widget to a stack widget
|
||||||
dashboard_designer:
|
dashboard_designer:
|
||||||
title: Edit Dashboard
|
title: Edit Dashboard
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -104,6 +104,22 @@ core:
|
||||||
title: 浏览量
|
title: 浏览量
|
||||||
pending_comments:
|
pending_comments:
|
||||||
title: 新评论
|
title: 新评论
|
||||||
|
stack:
|
||||||
|
title: 堆叠部件
|
||||||
|
operations:
|
||||||
|
add_widget:
|
||||||
|
button: 添加部件
|
||||||
|
config_modal:
|
||||||
|
title: 堆叠部件配置
|
||||||
|
fields:
|
||||||
|
auto_play:
|
||||||
|
label: 自动切换
|
||||||
|
auto_play_interval:
|
||||||
|
label: 自动切换间隔
|
||||||
|
widgets:
|
||||||
|
label: 部件
|
||||||
|
toast:
|
||||||
|
nest_warning: 不能将堆叠部件添加到堆叠部件中
|
||||||
dashboard_designer:
|
dashboard_designer:
|
||||||
title: 编辑仪表盘
|
title: 编辑仪表盘
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -104,6 +104,22 @@ core:
|
||||||
title: 瀏覽量
|
title: 瀏覽量
|
||||||
pending_comments:
|
pending_comments:
|
||||||
title: 新評論
|
title: 新評論
|
||||||
|
stack:
|
||||||
|
title: 堆疊部件
|
||||||
|
operations:
|
||||||
|
add_widget:
|
||||||
|
button: 添加部件
|
||||||
|
config_modal:
|
||||||
|
title: 堆疊部件配置
|
||||||
|
fields:
|
||||||
|
auto_play:
|
||||||
|
label: 自動切換
|
||||||
|
auto_play_interval:
|
||||||
|
label: 自動切換間隔
|
||||||
|
widgets:
|
||||||
|
label: 部件
|
||||||
|
toast:
|
||||||
|
nest_warning: 不能將堆疊部件添加到堆疊部件中
|
||||||
dashboard_designer:
|
dashboard_designer:
|
||||||
title: 編輯儀表盤
|
title: 編輯儀表盤
|
||||||
actions:
|
actions:
|
||||||
|
|
Loading…
Reference in New Issue