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,
|
||||
} from "@halo-dev/console-shared";
|
||||
import { computed, inject, ref, type ComputedRef } from "vue";
|
||||
import ActionButton from "./ActionButton.vue";
|
||||
import WidgetConfigFormModal from "./WidgetConfigFormModal.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -61,21 +62,18 @@ function handleSaveConfig(config: Record<string, unknown>) {
|
|||
@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"
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<IconSettings class="text-base" />
|
||||
</div>
|
||||
<div
|
||||
class="h-full w-8 flex cursor-pointer items-center justify-center bg-red-500 hover:bg-red-600 text-white"
|
||||
@click="emit('remove')"
|
||||
>
|
||||
<IconCloseCircle class="text-base" />
|
||||
</div>
|
||||
<IconSettings />
|
||||
</ActionButton>
|
||||
<ActionButton class="bg-red-500" @click="emit('remove')">
|
||||
<IconCloseCircle />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</grid-item>
|
||||
<WidgetConfigFormModal
|
||||
|
|
|
@ -67,6 +67,7 @@ const groupWidgetDefinitionsKeys = computed(() => {
|
|||
:width="1380"
|
||||
:layer-closable="true"
|
||||
:title="$t('core.dashboard_designer.widgets_modal.title')"
|
||||
mount-to-body
|
||||
@close="emit('close')"
|
||||
>
|
||||
<VTabbar
|
||||
|
|
|
@ -3,6 +3,7 @@ import { markRaw } from "vue";
|
|||
import CommentStatsWidget from "./presets/comments/CommentStatsWidget.vue";
|
||||
import PendingCommentsWidget from "./presets/comments/PendingCommentsWidget.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 PostStatsWidget from "./presets/posts/PostStatsWidget.vue";
|
||||
import RecentPublishedWidget from "./presets/posts/RecentPublishedWidget.vue";
|
||||
|
@ -182,4 +183,16 @@ export const internalWidgetDefinitions: DashboardWidgetDefinition[] = [
|
|||
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
|
||||
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:
|
||||
title: Edit Dashboard
|
||||
actions:
|
||||
|
|
|
@ -108,6 +108,22 @@ core:
|
|||
title: Pending Comments
|
||||
views_stats:
|
||||
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:
|
||||
title: Edit Dashboard
|
||||
actions:
|
||||
|
|
|
@ -104,6 +104,22 @@ core:
|
|||
title: 浏览量
|
||||
pending_comments:
|
||||
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:
|
||||
title: 编辑仪表盘
|
||||
actions:
|
||||
|
|
|
@ -104,6 +104,22 @@ core:
|
|||
title: 瀏覽量
|
||||
pending_comments:
|
||||
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:
|
||||
title: 編輯儀表盤
|
||||
actions:
|
||||
|
|
Loading…
Reference in New Issue