Track dashboard render runtime files
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface PageMeta {
|
||||
id: string;
|
||||
application_id?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
category: string;
|
||||
description: string;
|
||||
status: string;
|
||||
version: number;
|
||||
page_config: Record<string, any>;
|
||||
sort: number;
|
||||
sys_create_datetime: string;
|
||||
sys_update_datetime: string;
|
||||
}
|
||||
|
||||
export async function getPageByCodeApi(code: string) {
|
||||
return requestClient.get<PageMeta>(`/api/online_dev/page/code/${code}`);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardConfig } from './types';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElEmpty } from 'element-plus';
|
||||
import { GridItem, GridLayout } from 'grid-layout-plus';
|
||||
|
||||
import WidgetRenderer from './components/WidgetRenderer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
config: DashboardConfig | string;
|
||||
}>();
|
||||
|
||||
const dashboardConfig = ref<DashboardConfig | null>(null);
|
||||
|
||||
const parseConfig = () => {
|
||||
if (!props.config) {
|
||||
dashboardConfig.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof props.config === 'string') {
|
||||
try {
|
||||
dashboardConfig.value = JSON.parse(props.config);
|
||||
} catch {
|
||||
console.error('Invalid dashboard config JSON');
|
||||
dashboardConfig.value = null;
|
||||
}
|
||||
} else {
|
||||
dashboardConfig.value = props.config;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(parseConfig);
|
||||
watch(() => props.config, parseConfig);
|
||||
|
||||
const layout = computed(() => {
|
||||
if (!dashboardConfig.value) return [];
|
||||
return dashboardConfig.value.widgets.map((w) => ({
|
||||
i: w.i,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
}));
|
||||
});
|
||||
|
||||
const getWidget = (i: string) => {
|
||||
if (!dashboardConfig.value) return null;
|
||||
return dashboardConfig.value.widgets.find((w) => w.i === i);
|
||||
};
|
||||
|
||||
const getAnimationDelay = (i: string) => {
|
||||
if (!dashboardConfig.value) return 0;
|
||||
const index = dashboardConfig.value.widgets.findIndex((w) => w.i === i);
|
||||
return index * 80;
|
||||
};
|
||||
|
||||
const outerMarginStyle = computed(() => {
|
||||
const config = dashboardConfig.value;
|
||||
if (!config || config.showOuterMargin !== false) return {};
|
||||
const margin = config.margin || [12, 12];
|
||||
return {
|
||||
marginLeft: `-${margin[0]}px`,
|
||||
marginRight: `-${margin[0]}px`,
|
||||
marginTop: `-${margin[1]}px`,
|
||||
width: `calc(100% + ${margin[0] * 2}px)`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dashboard-renderer h-full"
|
||||
:style="
|
||||
dashboardConfig?.backgroundColor?.includes('gradient')
|
||||
? { background: dashboardConfig.backgroundColor }
|
||||
: { backgroundColor: dashboardConfig?.backgroundColor || '' }
|
||||
"
|
||||
>
|
||||
<div v-if="dashboardConfig && layout.length > 0">
|
||||
<GridLayout
|
||||
:layout="layout"
|
||||
:col-num="dashboardConfig.columns"
|
||||
:row-height="dashboardConfig.rowHeight"
|
||||
:margin="dashboardConfig.margin"
|
||||
:is-draggable="false"
|
||||
:is-resizable="false"
|
||||
:vertical-compact="true"
|
||||
:use-css-transforms="true"
|
||||
:style="outerMarginStyle"
|
||||
>
|
||||
<GridItem
|
||||
v-for="item in layout"
|
||||
:key="item.i"
|
||||
:i="item.i"
|
||||
:x="item.x"
|
||||
:y="item.y"
|
||||
:w="item.w"
|
||||
:h="item.h"
|
||||
class="dashboard-widget"
|
||||
>
|
||||
<WidgetRenderer
|
||||
v-if="getWidget(item.i)"
|
||||
:widget="getWidget(item.i)!"
|
||||
:is-design-mode="false"
|
||||
:animation-delay="getAnimationDelay(item.i)"
|
||||
/>
|
||||
</GridItem>
|
||||
</GridLayout>
|
||||
</div>
|
||||
|
||||
<ElEmpty
|
||||
v-else
|
||||
:description="$t('dashboard-design.noConfigTip')"
|
||||
class="py-20"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-renderer {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.dashboard-widget {
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
DashboardWidget,
|
||||
WidgetStyle,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { Loader2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useDashboardRuntime } from '../runtime';
|
||||
import { createRefreshTimer, fetchWidgetData } from '../utils/dataFetcher';
|
||||
|
||||
const props = defineProps<{
|
||||
animationDelay?: number; // 入场动画延迟(毫秒)
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 入场动画状态
|
||||
const isEntered = ref(false);
|
||||
// 数据更新动画状态
|
||||
const isUpdating = ref(false);
|
||||
|
||||
// 组件映射
|
||||
const widgetComponents: Record<
|
||||
string,
|
||||
ReturnType<typeof defineAsyncComponent>
|
||||
> = {
|
||||
'stat-card': defineAsyncComponent(() => import('./widgets/StatCard.vue')),
|
||||
'progress-card': defineAsyncComponent(
|
||||
() => import('./widgets/ProgressCard.vue'),
|
||||
),
|
||||
'todo-list': defineAsyncComponent(() => import('./widgets/TodoList.vue')),
|
||||
'notice-list': defineAsyncComponent(() => import('./widgets/NoticeList.vue')),
|
||||
'ranking-list': defineAsyncComponent(
|
||||
() => import('./widgets/RankingList.vue'),
|
||||
),
|
||||
'announcement-list': defineAsyncComponent(
|
||||
() => import('./widgets/AnnouncementList.vue'),
|
||||
),
|
||||
'quick-links': defineAsyncComponent(() => import('./widgets/QuickLinks.vue')),
|
||||
'welcome-card': defineAsyncComponent(
|
||||
() => import('./widgets/WelcomeCard.vue'),
|
||||
),
|
||||
calendar: defineAsyncComponent(() => import('./widgets/CalendarWidget.vue')),
|
||||
countdown: defineAsyncComponent(
|
||||
() => import('./widgets/CountdownWidget.vue'),
|
||||
),
|
||||
clock: defineAsyncComponent(() => import('./widgets/ClockWidget.vue')),
|
||||
weather: defineAsyncComponent(() => import('./widgets/WeatherWidget.vue')),
|
||||
'image-carousel': defineAsyncComponent(
|
||||
() => import('./widgets/ImageCarousel.vue'),
|
||||
),
|
||||
'data-table': defineAsyncComponent(() => import('./widgets/DataTable.vue')),
|
||||
iframe: defineAsyncComponent(() => import('./widgets/IframeWidget.vue')),
|
||||
'video-player': defineAsyncComponent(
|
||||
() => import('./widgets/VideoPlayer.vue'),
|
||||
),
|
||||
image: defineAsyncComponent(() => import('./widgets/ImageWidget.vue')),
|
||||
'approval-center': defineAsyncComponent(
|
||||
() => import('./widgets/ApprovalCenter.vue'),
|
||||
),
|
||||
'my-apps': defineAsyncComponent(() => import('./widgets/MyApps.vue')),
|
||||
'server-monitor': defineAsyncComponent(
|
||||
() => import('./widgets/ServerMonitor.vue'),
|
||||
),
|
||||
'filter-input': defineAsyncComponent(
|
||||
() => import('./widgets/FilterInput.vue'),
|
||||
),
|
||||
'filter-select': defineAsyncComponent(
|
||||
() => import('./widgets/FilterSelect.vue'),
|
||||
),
|
||||
'filter-date': defineAsyncComponent(
|
||||
() => import('./widgets/FilterDate.vue'),
|
||||
),
|
||||
'filter-date-range': defineAsyncComponent(
|
||||
() => import('./widgets/FilterDateRange.vue'),
|
||||
),
|
||||
};
|
||||
|
||||
const defaultWidgetStyle: WidgetStyle = {
|
||||
backgroundColor: '',
|
||||
borderWidth: 0,
|
||||
borderColor: '',
|
||||
borderStyle: 'solid',
|
||||
borderRadius: 8,
|
||||
shadowEnabled: true,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 4,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 1,
|
||||
padding: 16,
|
||||
titleShow: true,
|
||||
titleFontSize: 14,
|
||||
titleColor: '',
|
||||
titleAlign: 'left',
|
||||
titleFontWeight: 'normal',
|
||||
};
|
||||
|
||||
const { globalParams, globalParamsVersion } = useDashboardRuntime();
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
return widgetComponents[props.widget.type];
|
||||
});
|
||||
|
||||
// 从 paramBindings 解析出实际参数值
|
||||
function resolveParams(): Record<string, any> | undefined {
|
||||
const bindings = props.widget.dataSource?.paramBindings;
|
||||
if (!bindings || bindings.length === 0) return undefined;
|
||||
const params: Record<string, any> = {};
|
||||
for (const b of bindings) {
|
||||
if (b.paramName && b.globalKey) {
|
||||
const val = globalParams.value[b.globalKey];
|
||||
if (val !== undefined && val !== '') {
|
||||
params[b.paramName] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.keys(params).length > 0 ? params : undefined;
|
||||
}
|
||||
|
||||
// 动态数据
|
||||
const dynamicProps = ref<Record<string, any>>({});
|
||||
const isLoading = ref(false);
|
||||
const hasError = ref(false);
|
||||
|
||||
// 合并后的 widget(静态 props + 动态 props)
|
||||
const mergedWidget = computed(() => {
|
||||
return {
|
||||
...props.widget,
|
||||
props: {
|
||||
...props.widget.props,
|
||||
...dynamicProps.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 计算样式(只应用容器级别的样式,不影响组件内部)
|
||||
const widgetStyle = computed(() => {
|
||||
const style = { ...defaultWidgetStyle, ...props.widget.style };
|
||||
const css: Record<string, string> = {};
|
||||
|
||||
// 背景(默认使用主题背景色,支持渐变色)
|
||||
const bgValue = style.backgroundColor || 'var(--el-bg-color)';
|
||||
if (bgValue.includes('gradient')) {
|
||||
css.background = bgValue;
|
||||
} else {
|
||||
css.backgroundColor = bgValue;
|
||||
}
|
||||
|
||||
// 边框
|
||||
if (style.borderWidth && style.borderWidth > 0) {
|
||||
css.borderWidth = `${style.borderWidth}px`;
|
||||
css.borderStyle = style.borderStyle || 'solid';
|
||||
css.borderColor = style.borderColor || 'var(--el-border-color)';
|
||||
}
|
||||
|
||||
// 圆角
|
||||
if (style.borderRadius !== undefined) {
|
||||
css.borderRadius = `${style.borderRadius}px`;
|
||||
}
|
||||
|
||||
// 阴影
|
||||
if (style.shadowEnabled) {
|
||||
const color = style.shadowColor || 'rgba(0, 0, 0, 0.1)';
|
||||
const blur = style.shadowBlur || 4;
|
||||
const x = style.shadowOffsetX || 0;
|
||||
const y = style.shadowOffsetY || 1;
|
||||
css.boxShadow = `${x}px ${y}px ${blur}px ${color}`;
|
||||
} else {
|
||||
css.boxShadow = 'none';
|
||||
}
|
||||
|
||||
return css;
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
if (!props.widget.dataSource || props.widget.dataSource.type === 'static') {
|
||||
dynamicProps.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
|
||||
try {
|
||||
const params = resolveParams();
|
||||
const result = await fetchWidgetData(
|
||||
props.widget.dataSource,
|
||||
props.widget.props,
|
||||
params,
|
||||
);
|
||||
|
||||
// 数据更新动画
|
||||
if (isEntered.value && Object.keys(dynamicProps.value).length > 0) {
|
||||
isUpdating.value = true;
|
||||
setTimeout(() => {
|
||||
isUpdating.value = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
dynamicProps.value = result.props;
|
||||
} catch (error) {
|
||||
console.error('[WidgetRenderer] loadData error', error);
|
||||
hasError.value = true;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新定时器清理函数
|
||||
let cleanupTimer: (() => void) | null = null;
|
||||
|
||||
// 设置刷新定时器
|
||||
const setupRefreshTimer = () => {
|
||||
if (cleanupTimer) {
|
||||
cleanupTimer();
|
||||
cleanupTimer = null;
|
||||
}
|
||||
|
||||
// 支持 api 和 dataSource 类型的自动刷新
|
||||
const dsType = props.widget.dataSource?.type;
|
||||
if (!props.isDesignMode && (dsType === 'api' || dsType === 'dataSource')) {
|
||||
cleanupTimer = createRefreshTimer(props.widget.dataSource, loadData);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听数据源变化
|
||||
watch(
|
||||
() => props.widget.dataSource,
|
||||
() => {
|
||||
loadData();
|
||||
setupRefreshTimer();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 监听全局筛选参数变化,有 paramBindings 的组件重新加载数据
|
||||
watch(
|
||||
() => globalParamsVersion.value,
|
||||
() => {
|
||||
if (props.isDesignMode) return;
|
||||
const bindings = props.widget.dataSource?.paramBindings;
|
||||
if (bindings && bindings.length > 0) {
|
||||
loadData();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
setupRefreshTimer();
|
||||
|
||||
// 入场动画
|
||||
const delay = props.animationDelay || 0;
|
||||
setTimeout(() => {
|
||||
isEntered.value = true;
|
||||
}, delay);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cleanupTimer) {
|
||||
cleanupTimer();
|
||||
}
|
||||
});
|
||||
|
||||
// 动画类名
|
||||
const animationClass = computed(() => {
|
||||
return {
|
||||
'widget-enter': true,
|
||||
'widget-entered': isEntered.value,
|
||||
'widget-updating': isUpdating.value,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="widget-renderer relative h-full w-full overflow-hidden"
|
||||
:class="animationClass"
|
||||
:style="widgetStyle"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center bg-white/50"
|
||||
>
|
||||
<Loader2 class="text-primary h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="hasError && !isLoading" class="absolute right-2 top-2 z-10">
|
||||
<span class="text-xs text-red-500">{{
|
||||
$t('dashboard-design.loadDataError')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 组件内容 -->
|
||||
<component
|
||||
:is="currentComponent"
|
||||
v-if="currentComponent"
|
||||
:widget="mergedWidget"
|
||||
:is-design-mode="isDesignMode"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center text-gray-400"
|
||||
>
|
||||
{{ $t('dashboard-design.unknownWidget') }}: {{ widget.type }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes pulse-update {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.widget-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.widget-entered {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* 数据更新动画 */
|
||||
.widget-updating {
|
||||
animation: pulse-update 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 入场动画 */
|
||||
</style>
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import type { UserAnnouncement } from '#/api/core/announcement';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { CheckCheck, Megaphone } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElEmpty, ElScrollbar, ElTag } from 'element-plus';
|
||||
|
||||
import {
|
||||
getUserAnnouncementDetailApi,
|
||||
getUserAnnouncementListApi,
|
||||
} from '#/api/core/announcement';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 实际公告数据
|
||||
const announcements = ref<UserAnnouncement[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 详情弹窗
|
||||
const detailVisible = ref(false);
|
||||
const currentAnnouncement = ref<null | UserAnnouncement>(null);
|
||||
const detailLoading = ref(false);
|
||||
|
||||
// 未读数量
|
||||
const unreadCount = computed(
|
||||
() => announcements.value.filter((a) => !a.is_read).length,
|
||||
);
|
||||
|
||||
// 获取优先级配置
|
||||
const getPriorityConfig = (priority: number) => {
|
||||
switch (priority) {
|
||||
case 1: {
|
||||
return {
|
||||
type: 'warning' as const,
|
||||
label: $t('dashboard-design.widgets.announcement.priority.important'),
|
||||
};
|
||||
}
|
||||
case 2: {
|
||||
return {
|
||||
type: 'danger' as const,
|
||||
label: $t('dashboard-design.widgets.announcement.priority.urgent'),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
type: 'info' as const,
|
||||
label: $t('dashboard-design.widgets.announcement.priority.normal'),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateStr?: string) => {
|
||||
if (!dateStr) return '';
|
||||
// 处理 "2025-12-01 21:47:29" 格式,替换空格为T以兼容所有浏览器
|
||||
const date = new Date(dateStr.replace(' ', 'T'));
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60_000);
|
||||
const hours = Math.floor(diff / 3_600_000);
|
||||
const days = Math.floor(diff / 86_400_000);
|
||||
|
||||
if (minutes < 1)
|
||||
return $t('dashboard-design.widgets.announcement.time.justNow');
|
||||
if (minutes < 60)
|
||||
return `${minutes}${$t('dashboard-design.widgets.announcement.time.minutesAgo')}`;
|
||||
if (hours < 24)
|
||||
return `${hours}${$t('dashboard-design.widgets.announcement.time.hoursAgo')}`;
|
||||
if (days < 7)
|
||||
return `${days}${$t('dashboard-design.widgets.announcement.time.daysAgo')}`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// 加载公告数据
|
||||
const loadAnnouncements = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const limit = props.widget.props.limit || 5;
|
||||
const res = await getUserAnnouncementListApi({ page: 1, pageSize: limit });
|
||||
announcements.value = res.items || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load announcements:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看公告详情(会自动标记已读)
|
||||
const viewDetail = async (item: UserAnnouncement) => {
|
||||
detailVisible.value = true;
|
||||
detailLoading.value = true;
|
||||
|
||||
try {
|
||||
// 获取详情会自动标记已读
|
||||
const detail = await getUserAnnouncementDetailApi(item.id);
|
||||
currentAnnouncement.value = detail;
|
||||
// 更新列表中的已读状态
|
||||
item.is_read = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load announcement detail:', error);
|
||||
currentAnnouncement.value = item;
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 全部已读(逐个标记)
|
||||
const markAllRead = async () => {
|
||||
if (unreadCount.value === 0) return;
|
||||
|
||||
const unreadItems = announcements.value.filter((a) => !a.is_read);
|
||||
for (const item of unreadItems) {
|
||||
try {
|
||||
await getUserAnnouncementDetailApi(item.id);
|
||||
item.is_read = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to mark as read:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadAnnouncements();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announcement-list flex h-full flex-col p-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Megaphone class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
<ElTag v-if="unreadCount > 0" type="danger" size="small" round>
|
||||
{{ unreadCount }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="unreadCount > 0"
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
@click="markAllRead"
|
||||
>
|
||||
<CheckCheck class="mr-1 h-3.5 w-3.5" />
|
||||
{{ $t('dashboard-design.widgets.announcement.markAllRead') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElScrollbar class="flex-1">
|
||||
<div v-if="announcements.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="item in announcements"
|
||||
:key="item.id"
|
||||
class="cursor-pointer rounded-md p-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
:class="{ 'opacity-60': item.is_read }"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<ElTag
|
||||
v-if="item.is_top"
|
||||
type="danger"
|
||||
size="small"
|
||||
effect="dark"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
{{ $t('dashboard-design.widgets.announcement.top') }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
:type="getPriorityConfig(item.priority).type"
|
||||
size="small"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
{{ getPriorityConfig(item.priority).label }}
|
||||
</ElTag>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.summary"
|
||||
class="text-muted-foreground mt-1 line-clamp-2 text-xs"
|
||||
>
|
||||
{{ item.summary }}
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground mt-1 flex items-center justify-between text-xs"
|
||||
>
|
||||
<span>{{ item.publisher_name }}</span>
|
||||
<span>{{ formatTime(item.publish_time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-else
|
||||
:description="$t('dashboard-design.widgets.announcement.noData')"
|
||||
:image-size="60"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
|
||||
<!-- 公告详情弹窗 -->
|
||||
<ZqDialog
|
||||
v-model="detailVisible"
|
||||
:title="currentAnnouncement?.title"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<div class="min-h-[500px]" v-loading="detailLoading">
|
||||
<!-- <div v-if="detailLoading" class="py-8 text-center text-gray-400">
|
||||
{{ $t('dashboard-design.widgets.announcement.loading') }}
|
||||
</div> -->
|
||||
<div v-if="currentAnnouncement" class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<ElTag
|
||||
v-if="currentAnnouncement.is_top"
|
||||
type="danger"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ $t('dashboard-design.widgets.announcement.top') }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
:type="getPriorityConfig(currentAnnouncement.priority).type"
|
||||
size="small"
|
||||
>
|
||||
{{ getPriorityConfig(currentAnnouncement.priority).label }}
|
||||
</ElTag>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
formatTime(currentAnnouncement.publish_time)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- 摘要 -->
|
||||
<div
|
||||
v-if="currentAnnouncement.summary"
|
||||
class="border-l-4 border-[var(--el-color-primary)] bg-[var(--el-fill-color-light)] py-3 pl-4 pr-3 text-sm text-[var(--el-text-color-regular)]"
|
||||
>
|
||||
{{ currentAnnouncement.summary }}
|
||||
</div>
|
||||
<!-- 富文本内容 -->
|
||||
<div
|
||||
class="announcement-content prose max-w-none"
|
||||
v-html="
|
||||
currentAnnouncement.content ||
|
||||
$t('dashboard-design.widgets.announcement.noContent')
|
||||
"
|
||||
></div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
{{ $t('dashboard-design.widgets.announcement.publisher')
|
||||
}}{{ currentAnnouncement.publisher_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
|
||||
.announcement-content :deep(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.announcement-content :deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.announcement-content :deep(td),
|
||||
.announcement-content :deep(th) {
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.announcement-content :deep(blockquote) {
|
||||
border-left: 4px solid var(--el-border-color);
|
||||
padding-left: 16px;
|
||||
margin: 8px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.announcement-content :deep(a) {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
ChevronRight,
|
||||
ClipboardCheck,
|
||||
ClipboardList,
|
||||
FilePen,
|
||||
Play,
|
||||
Send,
|
||||
UserCheck,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 审批中心菜单项
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
key: 'initiated',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.initiated'),
|
||||
icon: Send,
|
||||
color: 'rgba(59, 130, 246, 0.85)',
|
||||
path: '/workflow/initiated',
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.pending'),
|
||||
icon: ClipboardList,
|
||||
color: 'rgba(245, 158, 11, 0.85)',
|
||||
path: '/workflow/pending',
|
||||
},
|
||||
{
|
||||
key: 'handling',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.handling'),
|
||||
icon: FilePen,
|
||||
color: 'rgba(139, 92, 246, 0.85)',
|
||||
path: '/workflow/pending',
|
||||
},
|
||||
{
|
||||
key: 'handled',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.handled'),
|
||||
icon: ClipboardCheck,
|
||||
color: 'rgba(14, 165, 233, 0.85)',
|
||||
path: '/workflow/handled',
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.copy'),
|
||||
icon: UserCheck,
|
||||
color: 'rgba(249, 115, 22, 0.85)',
|
||||
path: '/workflow/copy',
|
||||
},
|
||||
{
|
||||
key: 'start',
|
||||
title: $t('dashboard-design.widgets.approvalCenter.start'),
|
||||
icon: Play,
|
||||
color: 'rgba(20, 184, 166, 0.85)',
|
||||
path: '/workflow/start',
|
||||
},
|
||||
]);
|
||||
|
||||
// 路由前缀
|
||||
const routePrefix = computed(
|
||||
() => props.widget.props.routePrefix || '/app/workflow_center',
|
||||
);
|
||||
|
||||
// 点击菜单项
|
||||
const handleClick = (item: { path: string }) => {
|
||||
window.open(`${routePrefix.value}${item.path}`, '_blank');
|
||||
};
|
||||
|
||||
// 点击更多
|
||||
const handleMore = () => {
|
||||
window.open(`${routePrefix.value}/workflow/pending`, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="approval-center flex h-full flex-col p-4">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ widget.props.title }}</span>
|
||||
<button
|
||||
v-if="widget.props.showMore"
|
||||
type="button"
|
||||
class="text-muted-foreground hover:text-primary flex items-center gap-0.5 text-xs transition-colors"
|
||||
@click="handleMore"
|
||||
>
|
||||
{{ $t('dashboard-design.widgets.approvalCenter.more') }}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- 菜单网格 -->
|
||||
<div class="flex flex-1 items-center justify-around gap-2">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
class="flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<div
|
||||
class="flex h-11 w-11 items-center justify-center rounded-full"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
>
|
||||
<component :is="item.icon" class="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span class="text-muted-foreground whitespace-nowrap text-xs">{{
|
||||
item.title
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Calendar } from '@vben/icons';
|
||||
|
||||
import { ElCalendar } from 'element-plus';
|
||||
|
||||
defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const selectedDate = ref(new Date());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="calendar-widget flex h-full flex-col p-3">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Calendar class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<ElCalendar v-model="selectedDate" class="compact-calendar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
|
||||
.compact-calendar {
|
||||
--el-calendar-border: none;
|
||||
}
|
||||
|
||||
:deep(.el-calendar) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-calendar__header) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.el-calendar__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-calendar-table thead th) {
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-calendar-table .el-calendar-day) {
|
||||
height: 32px;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-calendar-table td.is-selected .el-calendar-day) {
|
||||
color: white;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-calendar-table td.is-today .el-calendar-day) {
|
||||
font-weight: bold;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const locale = computed(() => preferences.app.locale);
|
||||
|
||||
const currentTime = ref('');
|
||||
const currentDate = ref('');
|
||||
|
||||
let timer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
|
||||
// 处理时区
|
||||
const date = now;
|
||||
if (props.widget.props.timezone && props.widget.props.timezone !== 'local') {
|
||||
try {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
timeZone: props.widget.props.timezone,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: props.widget.props.showSeconds ? '2-digit' : undefined,
|
||||
hour12: !props.widget.props.format24,
|
||||
};
|
||||
currentTime.value = now.toLocaleTimeString(locale.value, options);
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
timeZone: props.widget.props.timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
};
|
||||
currentDate.value = now.toLocaleDateString(locale.value, dateOptions);
|
||||
return;
|
||||
} catch {
|
||||
// 时区无效,使用本地时间
|
||||
}
|
||||
}
|
||||
|
||||
// 本地时间
|
||||
const hours = props.widget.props.format24
|
||||
? String(date.getHours()).padStart(2, '0')
|
||||
: String(date.getHours() % 12 || 12).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
const ampm = props.widget.props.format24
|
||||
? ''
|
||||
: (date.getHours() >= 12
|
||||
? ' PM'
|
||||
: ' AM');
|
||||
|
||||
currentTime.value = props.widget.props.showSeconds
|
||||
? `${hours}:${minutes}:${seconds}${ampm}`
|
||||
: `${hours}:${minutes}${ampm}`;
|
||||
|
||||
const weekdays = $t(
|
||||
'dashboard-design.widgets.clock.weekdays',
|
||||
) as unknown as string[];
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const weekday = weekdays[date.getDay()];
|
||||
|
||||
const yearSuffix = $t('dashboard-design.widgets.clock.year');
|
||||
const monthSuffix = $t('dashboard-design.widgets.clock.month');
|
||||
const daySuffix = $t('dashboard-design.widgets.clock.day');
|
||||
|
||||
currentDate.value = `${year}${yearSuffix}${month}${monthSuffix}${day}${daySuffix} ${weekday}`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateTime();
|
||||
timer = setInterval(updateTime, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="clock-widget flex h-full flex-col items-center justify-center p-3"
|
||||
>
|
||||
<div v-if="widget.props.title" class="text-muted-foreground mb-2 text-sm">
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
|
||||
<div class="time-display">
|
||||
{{ currentTime }}
|
||||
</div>
|
||||
|
||||
<div v-if="widget.props.showDate" class="date-display">
|
||||
{{ currentDate }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.time-display {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.date-display {
|
||||
margin-top: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 剩余时间
|
||||
const remainingTime = ref({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
finished: false,
|
||||
});
|
||||
|
||||
let timer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
const calculateRemaining = () => {
|
||||
const targetTime = new Date(props.widget.props.targetTime).getTime();
|
||||
const now = Date.now();
|
||||
const diff = targetTime - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
remainingTime.value = {
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
finished: true,
|
||||
};
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
remainingTime.value = { days, hours, minutes, seconds, finished: false };
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calculateRemaining();
|
||||
timer = setInterval(calculateRemaining, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
|
||||
const padZero = (num: number) => String(num).padStart(2, '0');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="countdown-widget flex h-full flex-col p-3">
|
||||
<div
|
||||
v-if="widget.props.title"
|
||||
class="text-muted-foreground mb-2 text-sm font-medium"
|
||||
>
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<template v-if="!remainingTime.finished">
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="widget.props.showDays">
|
||||
<div class="time-block">
|
||||
<div class="time-value">{{ remainingTime.days }}</div>
|
||||
<div class="time-label">
|
||||
{{ $t('dashboard-design.widgets.countdown.day') }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="time-separator">:</span>
|
||||
</template>
|
||||
|
||||
<template v-if="widget.props.showHours">
|
||||
<div class="time-block">
|
||||
<div class="time-value">{{ padZero(remainingTime.hours) }}</div>
|
||||
<div class="time-label">
|
||||
{{ $t('dashboard-design.widgets.countdown.hour') }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="time-separator">:</span>
|
||||
</template>
|
||||
|
||||
<template v-if="widget.props.showMinutes">
|
||||
<div class="time-block">
|
||||
<div class="time-value">{{ padZero(remainingTime.minutes) }}</div>
|
||||
<div class="time-label">
|
||||
{{ $t('dashboard-design.widgets.countdown.minute') }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="widget.props.showSeconds" class="time-separator">:</span>
|
||||
</template>
|
||||
|
||||
<template v-if="widget.props.showSeconds">
|
||||
<div class="time-block">
|
||||
<div class="time-value">{{ padZero(remainingTime.seconds) }}</div>
|
||||
<div class="time-label">
|
||||
{{ $t('dashboard-design.widgets.countdown.second') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="text-muted-foreground text-lg">
|
||||
{{
|
||||
widget.props.finishedText ||
|
||||
$t('dashboard-design.widgets.countdown.finished')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.time-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.time-label {
|
||||
margin-top: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
margin-bottom: 16px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,285 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElEmpty, ElTable, ElTableColumn } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const columns = computed(() => props.widget.props.columns || []);
|
||||
const tableData = computed(() => props.widget.props.data || []);
|
||||
|
||||
// 表格高度配置
|
||||
const tableHeight = computed(() => {
|
||||
if (props.widget.props.maxHeight) return undefined;
|
||||
return props.widget.props.height || '100%';
|
||||
});
|
||||
|
||||
// 状态颜色映射
|
||||
const getStatusClass = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
[$t('dashboard-design.widgets.dataTable.status.normal')]: 'status-success',
|
||||
[$t('dashboard-design.widgets.dataTable.status.warning')]: 'status-warning',
|
||||
[$t('dashboard-design.widgets.dataTable.status.error')]: 'status-danger',
|
||||
[$t('dashboard-design.widgets.dataTable.status.success')]: 'status-success',
|
||||
[$t('dashboard-design.widgets.dataTable.status.failed')]: 'status-danger',
|
||||
[$t('dashboard-design.widgets.dataTable.status.processing')]: 'status-info',
|
||||
};
|
||||
return statusMap[status] || '';
|
||||
};
|
||||
|
||||
// 统计类型国际化标签
|
||||
const summaryTypeLabels = computed(() => ({
|
||||
sum: $t('dashboard-design.widgets.dataTable.summary.sum'),
|
||||
avg: $t('dashboard-design.widgets.dataTable.summary.avg'),
|
||||
count: $t('dashboard-design.widgets.dataTable.summary.count'),
|
||||
max: $t('dashboard-design.widgets.dataTable.summary.max'),
|
||||
min: $t('dashboard-design.widgets.dataTable.summary.min'),
|
||||
}));
|
||||
|
||||
// 表尾统计方法
|
||||
const getSummaryMethod = (param: { columns: any[]; data: any[] }) => {
|
||||
const { columns: tableCols, data } = param;
|
||||
const sums: string[] = [];
|
||||
const summaryColumns = props.widget.props.summaryColumns || [];
|
||||
const summaryType = props.widget.props.summaryType || 'sum';
|
||||
const precision = props.widget.props.summaryPrecision ?? 2;
|
||||
|
||||
tableCols.forEach((column, index) => {
|
||||
// 第一列显示统计类型名称
|
||||
if (index === 0) {
|
||||
sums[index] =
|
||||
summaryTypeLabels.value[
|
||||
summaryType as keyof typeof summaryTypeLabels.value
|
||||
] || summaryType;
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找该列的统计配置
|
||||
const summaryConfig = summaryColumns.find(
|
||||
(sc: any) => sc.field === column.property,
|
||||
);
|
||||
|
||||
if (!summaryConfig || !summaryConfig.enabled) {
|
||||
sums[index] = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取该列的所有数值
|
||||
const values = data.map((item) => Number(item[column.property]));
|
||||
const validValues = values.filter((value) => !Number.isNaN(value));
|
||||
|
||||
if (validValues.length === 0) {
|
||||
sums[index] = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
let result: number;
|
||||
switch (summaryType) {
|
||||
case 'avg': {
|
||||
result =
|
||||
validValues.reduce((acc, val) => acc + val, 0) / validValues.length;
|
||||
break;
|
||||
}
|
||||
case 'count': {
|
||||
result = validValues.length;
|
||||
break;
|
||||
}
|
||||
case 'max': {
|
||||
result = Math.max(...validValues);
|
||||
break;
|
||||
}
|
||||
case 'min': {
|
||||
result = Math.min(...validValues);
|
||||
break;
|
||||
}
|
||||
case 'sum': {
|
||||
result = validValues.reduce((acc, val) => acc + val, 0);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result = validValues.reduce((acc, val) => acc + val, 0);
|
||||
}
|
||||
}
|
||||
|
||||
sums[index] = result.toFixed(precision);
|
||||
});
|
||||
|
||||
return sums;
|
||||
};
|
||||
|
||||
// 合并单元格方法
|
||||
const getSpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
|
||||
const mergeConfig = props.widget.props.mergeConfig;
|
||||
if (!mergeConfig || !mergeConfig.enabled) return;
|
||||
|
||||
const mergeRules = mergeConfig.rules || [];
|
||||
|
||||
for (const rule of mergeRules) {
|
||||
if (rule.type === 'row' && rule.field === column.property) {
|
||||
// 行合并:相同值的相邻行合并
|
||||
const data = tableData.value;
|
||||
const currentValue = row[rule.field];
|
||||
|
||||
// 检查是否是合并组的第一行
|
||||
if (rowIndex === 0 || data[rowIndex - 1][rule.field] !== currentValue) {
|
||||
let rowspan = 1;
|
||||
for (let i = rowIndex + 1; i < data.length; i++) {
|
||||
if (data[i][rule.field] === currentValue) {
|
||||
rowspan++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { rowspan, colspan: 1 };
|
||||
} else {
|
||||
// 被合并的行
|
||||
return { rowspan: 0, colspan: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.type === 'column' && rowIndex === rule.rowIndex) {
|
||||
// 列合并
|
||||
if (columnIndex === rule.startCol) {
|
||||
return { rowspan: 1, colspan: rule.colspan || 1 };
|
||||
} else if (
|
||||
columnIndex > rule.startCol &&
|
||||
columnIndex < rule.startCol + (rule.colspan || 1)
|
||||
) {
|
||||
return { rowspan: 0, colspan: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-table-widget flex h-full w-full flex-col p-3">
|
||||
<div
|
||||
v-if="widget.props.title"
|
||||
class="text-muted-foreground mb-2 text-sm font-medium"
|
||||
>
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<ElTable
|
||||
:data="tableData"
|
||||
:stripe="widget.props.stripe !== false"
|
||||
:border="widget.props.border"
|
||||
:size="widget.props.size || 'default'"
|
||||
:height="tableHeight"
|
||||
:max-height="widget.props.maxHeight"
|
||||
:highlight-current-row="widget.props.highlightCurrentRow"
|
||||
:show-header="widget.props.showHeader !== false"
|
||||
:empty-text="widget.props.emptyText || $t('common.noData')"
|
||||
:show-summary="widget.props.showSummary"
|
||||
:summary-method="
|
||||
widget.props.showSummary ? getSummaryMethod : undefined
|
||||
"
|
||||
:span-method="
|
||||
widget.props.mergeConfig?.enabled ? getSpanMethod : undefined
|
||||
"
|
||||
:header-cell-style="{
|
||||
textAlign: widget.props.headerAlign || 'left',
|
||||
...(widget.props.headerBgColor?.includes('gradient')
|
||||
? { background: widget.props.headerBgColor }
|
||||
: {
|
||||
backgroundColor:
|
||||
widget.props.headerBgColor || 'var(--el-fill-color-light)',
|
||||
}),
|
||||
}"
|
||||
:cell-style="{
|
||||
textAlign: widget.props.cellAlign || 'left',
|
||||
}"
|
||||
v-loading="widget.props.loading"
|
||||
>
|
||||
<ElTableColumn
|
||||
v-if="widget.props.showIndex"
|
||||
type="index"
|
||||
label="#"
|
||||
width="50"
|
||||
/>
|
||||
<ElTableColumn
|
||||
v-for="col in columns"
|
||||
:key="col.prop"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:width="col.width"
|
||||
:min-width="col.minWidth"
|
||||
:align="col.align || widget.props.cellAlign || 'left'"
|
||||
:header-align="col.headerAlign || widget.props.headerAlign || 'left'"
|
||||
:fixed="col.fixed"
|
||||
:sortable="col.sortable"
|
||||
:show-overflow-tooltip="col.showOverflowTooltip !== false"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
v-if="col.prop === 'status'"
|
||||
:class="getStatusClass(row[col.prop])"
|
||||
>
|
||||
{{ row[col.prop] }}
|
||||
</span>
|
||||
<span v-else>{{ row[col.prop] }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<template #empty>
|
||||
<ElEmpty
|
||||
:description="widget.props.emptyText || $t('common.noData')"
|
||||
:image-size="80"
|
||||
/>
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.data-table-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table-widget :deep(.el-table) {
|
||||
width: 100% !important;
|
||||
--el-table-header-bg-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.data-table-widget :deep(.el-table__inner-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-table-widget :deep(.el-table__header-wrapper),
|
||||
.data-table-widget :deep(.el-table__body-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.status-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
|
||||
import { useDashboardRuntime } from '../../runtime';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const { updateGlobalParam } = useDashboardRuntime();
|
||||
const dateValue = ref<string>(props.widget.props.defaultValue || '');
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
const placeholder = computed(
|
||||
() =>
|
||||
props.widget.props.placeholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterDatePlaceholder'),
|
||||
);
|
||||
const paramKey = computed(() => props.widget.props.paramKey || '');
|
||||
const dateFormat = computed(
|
||||
() => props.widget.props.dateFormat || 'YYYY-MM-DD',
|
||||
);
|
||||
|
||||
const labelPosition = computed(
|
||||
() => props.widget.props.labelPosition || 'left',
|
||||
);
|
||||
const labelWidth = computed(() => props.widget.props.labelWidth ?? 80);
|
||||
const labelAlign = computed(() => props.widget.props.labelAlign || 'right');
|
||||
const componentSize = computed(
|
||||
() => props.widget.props.componentSize || 'default',
|
||||
);
|
||||
const showBorder = computed(() => props.widget.props.showBorder ?? false);
|
||||
const borderRadius = computed(() => props.widget.props.borderRadius ?? 4);
|
||||
|
||||
const isTopLabel = computed(() => labelPosition.value === 'top');
|
||||
const isHiddenLabel = computed(() => labelPosition.value === 'hidden');
|
||||
|
||||
const containerClass = computed(() => [
|
||||
'filter-widget h-full w-full',
|
||||
isTopLabel.value ? 'flex flex-col' : 'flex items-center',
|
||||
showBorder.value ? 'filter-widget--bordered' : '',
|
||||
]);
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
padding: isTopLabel.value ? '8px 12px' : '0 12px',
|
||||
borderRadius: showBorder.value ? `${borderRadius.value}px` : undefined,
|
||||
}));
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
width: isTopLabel.value ? 'auto' : `${labelWidth.value}px`,
|
||||
textAlign: labelAlign.value as 'center' | 'left' | 'right',
|
||||
flexShrink: 0,
|
||||
marginBottom: isTopLabel.value ? '4px' : undefined,
|
||||
marginRight: isTopLabel.value ? undefined : '8px',
|
||||
}));
|
||||
|
||||
function handleChange(val: any) {
|
||||
if (props.isDesignMode) return;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val || '');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.widget.props.defaultValue,
|
||||
(val) => {
|
||||
if (!props.isDesignMode && val !== undefined) {
|
||||
dateValue.value = val;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClass" :style="containerStyle">
|
||||
<span
|
||||
v-if="label && !isHiddenLabel"
|
||||
class="text-foreground text-sm font-medium"
|
||||
:style="labelStyle"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<ElDatePicker
|
||||
v-model="dateValue"
|
||||
type="date"
|
||||
:placeholder="placeholder"
|
||||
:disabled="isDesignMode"
|
||||
:format="dateFormat"
|
||||
:value-format="dateFormat"
|
||||
:size="componentSize"
|
||||
clearable
|
||||
class="!flex-1"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<span
|
||||
v-if="isDesignMode && !paramKey"
|
||||
class="ml-2 flex-shrink-0 text-xs text-orange-500"
|
||||
>
|
||||
{{ $t('dashboard-design.filter.noParamKey') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-widget--bordered {
|
||||
border: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.filter-widget.flex-col :deep(.el-input),
|
||||
.filter-widget.flex-col :deep(.el-select),
|
||||
.filter-widget.flex-col :deep(.el-date-editor) {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElDatePicker } from 'element-plus';
|
||||
|
||||
import { useDashboardRuntime } from '../../runtime';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const { updateGlobalParam } = useDashboardRuntime();
|
||||
const dateRange = ref<[string, string] | []>(
|
||||
props.widget.props.defaultValue || [],
|
||||
);
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
const startPlaceholder = computed(
|
||||
() =>
|
||||
props.widget.props.startPlaceholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterStartDate'),
|
||||
);
|
||||
const endPlaceholder = computed(
|
||||
() =>
|
||||
props.widget.props.endPlaceholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterEndDate'),
|
||||
);
|
||||
const startParamKey = computed(
|
||||
() => props.widget.props.startParamKey || '',
|
||||
);
|
||||
const endParamKey = computed(
|
||||
() => props.widget.props.endParamKey || '',
|
||||
);
|
||||
const dateFormat = computed(
|
||||
() => props.widget.props.dateFormat || 'YYYY-MM-DD',
|
||||
);
|
||||
|
||||
const labelPosition = computed(
|
||||
() => props.widget.props.labelPosition || 'left',
|
||||
);
|
||||
const labelWidth = computed(() => props.widget.props.labelWidth ?? 80);
|
||||
const labelAlign = computed(() => props.widget.props.labelAlign || 'right');
|
||||
const componentSize = computed(
|
||||
() => props.widget.props.componentSize || 'default',
|
||||
);
|
||||
const showBorder = computed(() => props.widget.props.showBorder ?? false);
|
||||
const borderRadius = computed(() => props.widget.props.borderRadius ?? 4);
|
||||
|
||||
const isTopLabel = computed(() => labelPosition.value === 'top');
|
||||
const isHiddenLabel = computed(() => labelPosition.value === 'hidden');
|
||||
|
||||
const containerClass = computed(() => [
|
||||
'filter-widget h-full w-full',
|
||||
isTopLabel.value ? 'flex flex-col' : 'flex items-center',
|
||||
showBorder.value ? 'filter-widget--bordered' : '',
|
||||
]);
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
padding: isTopLabel.value ? '8px 12px' : '0 12px',
|
||||
borderRadius: showBorder.value ? `${borderRadius.value}px` : undefined,
|
||||
}));
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
width: isTopLabel.value ? 'auto' : `${labelWidth.value}px`,
|
||||
textAlign: labelAlign.value as 'center' | 'left' | 'right',
|
||||
flexShrink: 0,
|
||||
marginBottom: isTopLabel.value ? '4px' : undefined,
|
||||
marginRight: isTopLabel.value ? undefined : '8px',
|
||||
}));
|
||||
|
||||
function handleChange(val: [string, string] | null) {
|
||||
if (props.isDesignMode) return;
|
||||
if (val && val.length === 2) {
|
||||
if (startParamKey.value) {
|
||||
updateGlobalParam(startParamKey.value, val[0]);
|
||||
}
|
||||
if (endParamKey.value) {
|
||||
updateGlobalParam(endParamKey.value, val[1]);
|
||||
}
|
||||
} else {
|
||||
if (startParamKey.value) {
|
||||
updateGlobalParam(startParamKey.value, '');
|
||||
}
|
||||
if (endParamKey.value) {
|
||||
updateGlobalParam(endParamKey.value, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.widget.props.defaultValue,
|
||||
(val) => {
|
||||
if (!props.isDesignMode && Array.isArray(val) && val.length === 2) {
|
||||
dateRange.value = val as [string, string];
|
||||
if (startParamKey.value) {
|
||||
updateGlobalParam(startParamKey.value, val[0]);
|
||||
}
|
||||
if (endParamKey.value) {
|
||||
updateGlobalParam(endParamKey.value, val[1]);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const hasParamKey = computed(
|
||||
() => startParamKey.value || endParamKey.value,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClass" :style="containerStyle">
|
||||
<span
|
||||
v-if="label && !isHiddenLabel"
|
||||
class="text-foreground text-sm font-medium"
|
||||
:style="labelStyle"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<ElDatePicker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
:start-placeholder="startPlaceholder"
|
||||
:end-placeholder="endPlaceholder"
|
||||
:disabled="isDesignMode"
|
||||
:format="dateFormat"
|
||||
:value-format="dateFormat"
|
||||
:size="componentSize"
|
||||
clearable
|
||||
class="!flex-1"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<span
|
||||
v-if="isDesignMode && !hasParamKey"
|
||||
class="ml-2 flex-shrink-0 text-xs text-orange-500"
|
||||
>
|
||||
{{ $t('dashboard-design.filter.noParamKey') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-widget--bordered {
|
||||
border: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.filter-widget.flex-col :deep(.el-input),
|
||||
.filter-widget.flex-col :deep(.el-select),
|
||||
.filter-widget.flex-col :deep(.el-date-editor) {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Search } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElInput } from 'element-plus';
|
||||
|
||||
import { useDashboardRuntime } from '../../runtime';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const { updateGlobalParam } = useDashboardRuntime();
|
||||
const inputValue = ref(props.widget.props.defaultValue || '');
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
const placeholder = computed(
|
||||
() =>
|
||||
props.widget.props.placeholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterInputPlaceholder'),
|
||||
);
|
||||
const paramKey = computed(() => props.widget.props.paramKey || '');
|
||||
|
||||
const labelPosition = computed(
|
||||
() => props.widget.props.labelPosition || 'left',
|
||||
);
|
||||
const labelWidth = computed(() => props.widget.props.labelWidth ?? 80);
|
||||
const labelAlign = computed(() => props.widget.props.labelAlign || 'right');
|
||||
const componentSize = computed(
|
||||
() => props.widget.props.componentSize || 'default',
|
||||
);
|
||||
const showBorder = computed(() => props.widget.props.showBorder ?? false);
|
||||
const borderRadius = computed(() => props.widget.props.borderRadius ?? 4);
|
||||
|
||||
const isTopLabel = computed(() => labelPosition.value === 'top');
|
||||
const isHiddenLabel = computed(() => labelPosition.value === 'hidden');
|
||||
|
||||
const containerClass = computed(() => [
|
||||
'filter-widget h-full w-full',
|
||||
isTopLabel.value ? 'flex flex-col' : 'flex items-center',
|
||||
showBorder.value ? 'filter-widget--bordered' : '',
|
||||
]);
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
padding: isTopLabel.value ? '8px 12px' : '0 12px',
|
||||
borderRadius: showBorder.value ? `${borderRadius.value}px` : undefined,
|
||||
}));
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
width: isTopLabel.value ? 'auto' : `${labelWidth.value}px`,
|
||||
textAlign: labelAlign.value as 'center' | 'left' | 'right',
|
||||
flexShrink: 0,
|
||||
marginBottom: isTopLabel.value ? '4px' : undefined,
|
||||
marginRight: isTopLabel.value ? undefined : '8px',
|
||||
}));
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function handleInput(val: string | number) {
|
||||
if (props.isDesignMode) return;
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
if (props.isDesignMode) return;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, '');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.widget.props.defaultValue,
|
||||
(val) => {
|
||||
if (!props.isDesignMode && val !== undefined) {
|
||||
inputValue.value = val;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClass" :style="containerStyle">
|
||||
<span
|
||||
v-if="label && !isHiddenLabel"
|
||||
class="text-foreground text-sm font-medium"
|
||||
:style="labelStyle"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<ElInput
|
||||
v-model="inputValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="isDesignMode"
|
||||
clearable
|
||||
:size="componentSize"
|
||||
class="flex-1"
|
||||
@input="handleInput"
|
||||
@clear="handleClear"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="h-4 w-4 opacity-50" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<span
|
||||
v-if="isDesignMode && !paramKey"
|
||||
class="ml-2 flex-shrink-0 text-xs text-orange-500"
|
||||
>
|
||||
{{ $t('dashboard-design.filter.noParamKey') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-widget--bordered {
|
||||
border: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.filter-widget.flex-col :deep(.el-input),
|
||||
.filter-widget.flex-col :deep(.el-select),
|
||||
.filter-widget.flex-col :deep(.el-date-editor) {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
import { useDashboardRuntime } from '../../runtime';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const { updateGlobalParam } = useDashboardRuntime();
|
||||
const selectValue = ref<any>(props.widget.props.defaultValue || '');
|
||||
const dynamicOptions = ref<{ label: string; value: any }[]>([]);
|
||||
const loadingOptions = ref(false);
|
||||
|
||||
const label = computed(() => props.widget.props.label || '');
|
||||
const placeholder = computed(
|
||||
() =>
|
||||
props.widget.props.placeholder ||
|
||||
$t('dashboard-design.material.defaultProps.filterSelectPlaceholder'),
|
||||
);
|
||||
const paramKey = computed(() => props.widget.props.paramKey || '');
|
||||
const multiple = computed(() => props.widget.props.multiple || false);
|
||||
const clearable = computed(() => props.widget.props.clearable !== false);
|
||||
|
||||
const labelPosition = computed(
|
||||
() => props.widget.props.labelPosition || 'left',
|
||||
);
|
||||
const labelWidth = computed(() => props.widget.props.labelWidth ?? 80);
|
||||
const labelAlign = computed(() => props.widget.props.labelAlign || 'right');
|
||||
const componentSize = computed(
|
||||
() => props.widget.props.componentSize || 'default',
|
||||
);
|
||||
const showBorder = computed(() => props.widget.props.showBorder ?? false);
|
||||
const borderRadius = computed(() => props.widget.props.borderRadius ?? 4);
|
||||
|
||||
const isTopLabel = computed(() => labelPosition.value === 'top');
|
||||
const isHiddenLabel = computed(() => labelPosition.value === 'hidden');
|
||||
|
||||
const containerClass = computed(() => [
|
||||
'filter-widget h-full w-full',
|
||||
isTopLabel.value ? 'flex flex-col' : 'flex items-center',
|
||||
showBorder.value ? 'filter-widget--bordered' : '',
|
||||
]);
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
padding: isTopLabel.value ? '8px 12px' : '0 12px',
|
||||
borderRadius: showBorder.value ? `${borderRadius.value}px` : undefined,
|
||||
}));
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
width: isTopLabel.value ? 'auto' : `${labelWidth.value}px`,
|
||||
textAlign: labelAlign.value as 'center' | 'left' | 'right',
|
||||
flexShrink: 0,
|
||||
marginBottom: isTopLabel.value ? '4px' : undefined,
|
||||
marginRight: isTopLabel.value ? undefined : '8px',
|
||||
}));
|
||||
|
||||
const options = computed(() => {
|
||||
if (props.widget.props.optionSource === 'dataSource') {
|
||||
return dynamicOptions.value;
|
||||
}
|
||||
return props.widget.props.options || [];
|
||||
});
|
||||
|
||||
async function loadDynamicOptions() {
|
||||
const code = props.widget.props.optionDataSourceCode;
|
||||
if (!code) return;
|
||||
try {
|
||||
loadingOptions.value = true;
|
||||
const response = await requestClient.get(
|
||||
`/api/core/data-source/execute/${code}`,
|
||||
);
|
||||
const rawData = response?.data ?? response;
|
||||
const data = Array.isArray(rawData) ? rawData : [];
|
||||
const labelField = props.widget.props.optionLabelField || 'label';
|
||||
const valueField = props.widget.props.optionValueField || 'value';
|
||||
dynamicOptions.value = data.map((item: any) => ({
|
||||
label: String(item[labelField] ?? ''),
|
||||
value: item[valueField],
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load filter options:', error);
|
||||
} finally {
|
||||
loadingOptions.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(val: any) {
|
||||
if (props.isDesignMode) return;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
if (props.isDesignMode) return;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, multiple.value ? [] : '');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.widget.props.defaultValue,
|
||||
(val) => {
|
||||
if (!props.isDesignMode && val !== undefined) {
|
||||
selectValue.value = val;
|
||||
if (paramKey.value) {
|
||||
updateGlobalParam(paramKey.value, val);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.widget.props.optionSource === 'dataSource') {
|
||||
loadDynamicOptions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="containerClass" :style="containerStyle">
|
||||
<span
|
||||
v-if="label && !isHiddenLabel"
|
||||
class="text-foreground text-sm font-medium"
|
||||
:style="labelStyle"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<ElSelect
|
||||
v-model="selectValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="isDesignMode"
|
||||
:multiple="multiple"
|
||||
:clearable="clearable"
|
||||
:loading="loadingOptions"
|
||||
:size="componentSize"
|
||||
class="flex-1"
|
||||
@change="handleChange"
|
||||
@clear="handleClear"
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<span
|
||||
v-if="isDesignMode && !paramKey"
|
||||
class="ml-2 flex-shrink-0 text-xs text-orange-500"
|
||||
>
|
||||
{{ $t('dashboard-design.filter.noParamKey') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-widget--bordered {
|
||||
border: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.filter-widget.flex-col :deep(.el-input),
|
||||
.filter-widget.flex-col :deep(.el-select),
|
||||
.filter-widget.flex-col :deep(.el-date-editor) {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AlertCircle, ExternalLink, RefreshCw } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
||||
const isLoading = ref(true);
|
||||
const hasError = ref(false);
|
||||
|
||||
const iframeUrl = computed(() => props.widget.props.url || '');
|
||||
|
||||
const handleLoad = () => {
|
||||
isLoading.value = false;
|
||||
hasError.value = false;
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
isLoading.value = false;
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
if (iframeRef.value) {
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
iframeRef.value.src = iframeUrl.value;
|
||||
}
|
||||
};
|
||||
|
||||
const openInNewTab = () => {
|
||||
if (iframeUrl.value) {
|
||||
window.open(iframeUrl.value, '_blank');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="iframe-widget flex h-full flex-col">
|
||||
<!-- 标题栏 -->
|
||||
<div v-if="widget.props.title" class="iframe-header">
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
class="action-btn"
|
||||
:title="$t('dashboard-design.widgets.iframe.refresh')"
|
||||
@click="refresh"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
:title="$t('dashboard-design.widgets.iframe.openNew')"
|
||||
@click="openInNewTab"
|
||||
>
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- iframe 容器 -->
|
||||
<div class="iframe-container relative min-h-0 flex-1">
|
||||
<!-- 设计模式下显示占位 -->
|
||||
<div v-if="isDesignMode" class="design-placeholder">
|
||||
<ExternalLink class="h-8 w-8 text-gray-400" />
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
{{ $t('dashboard-design.widgets.iframe.placeholder') }}
|
||||
</div>
|
||||
<div class="mt-1 max-w-full truncate px-4 text-xs text-gray-400">
|
||||
{{ iframeUrl || $t('dashboard-design.widgets.iframe.noUrl') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实际 iframe -->
|
||||
<template v-else>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
{{ $t('dashboard-design.widgets.iframe.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="hasError && !isLoading" class="error-overlay">
|
||||
<AlertCircle class="h-8 w-8 text-red-400" />
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
{{ $t('dashboard-design.widgets.iframe.loadFailed') }}
|
||||
</div>
|
||||
<button class="text-primary mt-2 text-xs" @click="refresh">
|
||||
{{ $t('dashboard-design.widgets.iframe.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- iframe -->
|
||||
<iframe
|
||||
v-show="!hasError"
|
||||
ref="iframeRef"
|
||||
:src="iframeUrl"
|
||||
:class="{ 'with-border': widget.props.showBorder }"
|
||||
:allowfullscreen="widget.props.allowFullscreen"
|
||||
frameborder="0"
|
||||
class="iframe-content"
|
||||
@load="handleLoad"
|
||||
@error="handleError"
|
||||
></iframe>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.iframe-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.design-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.loading-overlay,
|
||||
.error-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--el-border-color-lighter);
|
||||
border-top-color: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-content.with-border {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
</style>
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { getFileUrl } from '#/composables/useFileUrl';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const currentIndex = ref(0);
|
||||
let autoplayTimer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
// 处理后的图片列表(响应式)
|
||||
const images = ref<any[]>([]);
|
||||
|
||||
// 异步解析图片URL
|
||||
async function resolveImageUrl(url: string): Promise<string> {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('file://')) {
|
||||
const fileId = url.slice(7);
|
||||
return await getFileUrl(fileId);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// 加载图片列表
|
||||
async function loadImages() {
|
||||
const rawImages = props.widget.props.images || [];
|
||||
const resolvedImages = await Promise.all(
|
||||
rawImages.map(async (img: any) => ({
|
||||
...img,
|
||||
url: await resolveImageUrl(img.url || ''),
|
||||
})),
|
||||
);
|
||||
images.value = resolvedImages;
|
||||
}
|
||||
|
||||
// 监听图片列表变化
|
||||
watch(
|
||||
() => props.widget.props.images,
|
||||
() => {
|
||||
loadImages();
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const nextSlide = () => {
|
||||
if (images.value.length === 0) return;
|
||||
currentIndex.value = (currentIndex.value + 1) % images.value.length;
|
||||
};
|
||||
|
||||
const prevSlide = () => {
|
||||
if (images.value.length === 0) return;
|
||||
currentIndex.value =
|
||||
(currentIndex.value - 1 + images.value.length) % images.value.length;
|
||||
};
|
||||
|
||||
const goToSlide = (index: number) => {
|
||||
currentIndex.value = index;
|
||||
};
|
||||
|
||||
const startAutoplay = () => {
|
||||
if (autoplayTimer) {
|
||||
clearInterval(autoplayTimer);
|
||||
}
|
||||
if (props.widget.props.autoplay && !props.isDesignMode) {
|
||||
const interval = props.widget.props.interval || 3000;
|
||||
autoplayTimer = setInterval(nextSlide, interval);
|
||||
}
|
||||
};
|
||||
|
||||
const stopAutoplay = () => {
|
||||
if (autoplayTimer) {
|
||||
clearInterval(autoplayTimer);
|
||||
autoplayTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageClick = (image: any) => {
|
||||
if (image.link && !props.isDesignMode) {
|
||||
window.open(image.link, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startAutoplay();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoplay();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.widget.props.autoplay,
|
||||
() => {
|
||||
if (props.widget.props.autoplay) {
|
||||
startAutoplay();
|
||||
} else {
|
||||
stopAutoplay();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="image-carousel relative h-full w-full overflow-hidden"
|
||||
@mouseenter="stopAutoplay"
|
||||
@mouseleave="startAutoplay"
|
||||
>
|
||||
<!-- 图片容器 -->
|
||||
<div
|
||||
class="carousel-track flex h-full transition-transform duration-500 ease-in-out"
|
||||
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(image, index) in images"
|
||||
:key="index"
|
||||
class="carousel-slide h-full w-full flex-shrink-0"
|
||||
:class="{ 'cursor-pointer': image.link && !isDesignMode }"
|
||||
@click="handleImageClick(image)"
|
||||
>
|
||||
<img
|
||||
:src="image.url"
|
||||
:alt="
|
||||
image.title ||
|
||||
`${$t('dashboard-design.widgets.image.title')} ${Number(index) + 1}`
|
||||
"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<div v-if="image.title" class="slide-title">
|
||||
{{ image.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左右箭头 -->
|
||||
<template v-if="widget.props.showArrow && images.length > 1">
|
||||
<button class="arrow-btn arrow-left" @click.stop="prevSlide">
|
||||
<ChevronLeft class="h-5 w-5" />
|
||||
</button>
|
||||
<button class="arrow-btn arrow-right" @click.stop="nextSlide">
|
||||
<ChevronRight class="h-5 w-5" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- 指示器 -->
|
||||
<div
|
||||
v-if="widget.props.showIndicator && images.length > 1"
|
||||
class="indicators"
|
||||
>
|
||||
<button
|
||||
v-for="(_, index) in images"
|
||||
:key="index"
|
||||
class="indicator"
|
||||
:class="{ active: currentIndex === Number(index) }"
|
||||
@click.stop="goToSlide(Number(index))"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="images.length === 0"
|
||||
class="flex h-full w-full items-center justify-center text-gray-400"
|
||||
>
|
||||
{{ $t('dashboard-design.widgets.image.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.carousel-track {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.slide-title {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
background: linear-gradient(transparent, rgb(0 0 0 / 60%));
|
||||
}
|
||||
|
||||
.arrow-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
background: rgb(255 255 255 / 80%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.image-carousel:hover .arrow-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.arrow-btn:hover {
|
||||
background: rgb(255 255 255 / 100%);
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.arrow-right {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.indicators {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
cursor: pointer;
|
||||
background: rgb(255 255 255 / 50%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
width: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
import { getFileUrl } from '#/composables/useFileUrl';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const widgetProps = computed(() => props.widget.props);
|
||||
|
||||
// 主图URL(响应式)
|
||||
const imageSrc = ref('');
|
||||
|
||||
// 预览图片列表(响应式)
|
||||
const previewList = ref<string[]>([]);
|
||||
|
||||
// 异步解析图片URL
|
||||
async function resolveImageUrl(url: string): Promise<string> {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('file://')) {
|
||||
const fileId = url.slice(7);
|
||||
return await getFileUrl(fileId);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// 加载图片URL
|
||||
async function loadImageUrls() {
|
||||
// 加载主图
|
||||
imageSrc.value = await resolveImageUrl(widgetProps.value.src || '');
|
||||
|
||||
// 加载预览列表
|
||||
const list = widgetProps.value.previewSrcList || [];
|
||||
previewList.value =
|
||||
list.length === 0 && imageSrc.value
|
||||
? [imageSrc.value]
|
||||
: await Promise.all(list.map((url: string) => resolveImageUrl(url)));
|
||||
}
|
||||
|
||||
// 监听图片源变化
|
||||
watch(
|
||||
() => [widgetProps.value.src, widgetProps.value.previewSrcList],
|
||||
() => {
|
||||
loadImageUrls();
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-widget flex h-full w-full flex-col">
|
||||
<!-- 标题 -->
|
||||
<div
|
||||
v-if="widgetProps.title"
|
||||
class="mb-2 flex-shrink-0 text-sm font-medium"
|
||||
style="color: var(--el-text-color-primary)"
|
||||
>
|
||||
{{ widgetProps.title }}
|
||||
</div>
|
||||
|
||||
<!-- 图片容器 -->
|
||||
<div class="relative min-h-0 flex-1 overflow-hidden rounded">
|
||||
<ElImage
|
||||
:src="imageSrc"
|
||||
:alt="widgetProps.alt || '图片'"
|
||||
:fit="widgetProps.fit || 'cover'"
|
||||
:lazy="widgetProps.lazy !== false"
|
||||
:preview-src-list="isDesignMode ? [] : previewList"
|
||||
:z-index="widgetProps.zIndex || 2000"
|
||||
:hide-on-click-modal="widgetProps.hideOnClickModal || false"
|
||||
class="h-full w-full"
|
||||
:preview-teleported="true"
|
||||
>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center"
|
||||
style="background-color: var(--el-fill-color-light)"
|
||||
>
|
||||
<span style="color: var(--el-text-color-secondary)">
|
||||
加载失败
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center"
|
||||
style="background-color: var(--el-fill-color-lighter)"
|
||||
>
|
||||
<span style="color: var(--el-text-color-placeholder)">
|
||||
加载中...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-widget :deep(.el-image) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-widget :deep(.el-image__inner) {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.image-widget:hover :deep(.el-image__inner) {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
import type { ApplicationListItem } from '#/api/core/application';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { AppWindow, ChevronRight, IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElEmpty, ElScrollbar } from 'element-plus';
|
||||
|
||||
import { getApplicationListApi } from '#/api/core/application';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 应用列表
|
||||
const appList = ref<ApplicationListItem[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 最大显示数量
|
||||
const maxCount = computed(() => props.widget.props.maxCount || 8);
|
||||
|
||||
// 显示的应用列表
|
||||
const displayApps = computed(() => appList.value.slice(0, maxCount.value));
|
||||
|
||||
// 加载已发布的应用列表
|
||||
const loadApps = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getApplicationListApi({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
status: 'published',
|
||||
});
|
||||
appList.value = res.items || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load applications:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 点击应用
|
||||
const handleClick = (app: ApplicationListItem) => {
|
||||
window.open(`${window.location.origin}/app/${app.code}`, '_blank');
|
||||
};
|
||||
|
||||
// 点击更多
|
||||
const handleMore = () => {
|
||||
window.open(`${window.location.origin}/application`, '_blank');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadApps();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="my-apps flex h-full flex-col p-4" v-loading="loading">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ widget.props.title }}</span>
|
||||
<button
|
||||
v-if="widget.props.showMore"
|
||||
type="button"
|
||||
class="text-muted-foreground hover:text-primary flex items-center gap-0.5 text-xs transition-colors"
|
||||
@click="handleMore"
|
||||
>
|
||||
{{ $t('dashboard-design.widgets.myApps.more') }}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- 应用网格 -->
|
||||
<ElScrollbar class="flex-1">
|
||||
<div
|
||||
v-if="displayApps.length > 0"
|
||||
class="flex flex-wrap gap-4"
|
||||
>
|
||||
<div
|
||||
v-for="app in displayApps"
|
||||
:key="app.id"
|
||||
class="flex cursor-pointer flex-col items-center gap-2 transition-transform hover:scale-105 m-4"
|
||||
style="width: 72px"
|
||||
@click="handleClick(app)"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl"
|
||||
style="background: linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary))"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="app.icon"
|
||||
:icon="app.icon"
|
||||
class="h-6 w-6 text-white"
|
||||
/>
|
||||
<AppWindow v-else class="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<span class="text-muted-foreground w-full truncate text-center text-xs">{{ app.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-else-if="!loading"
|
||||
:description="$t('dashboard-design.widgets.myApps.noData')"
|
||||
:image-size="60"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,269 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import type { Message } from '#/api/core/message';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Bell, CheckCheck, ExternalLink } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElDialog, ElEmpty, ElScrollbar, ElTag } from 'element-plus';
|
||||
|
||||
import {
|
||||
getMessageListApi,
|
||||
markAllAsReadApi,
|
||||
markAsReadApi,
|
||||
} from '#/api/core/message';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 实际消息数据
|
||||
const messages = ref<Message[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 详情弹窗
|
||||
const detailVisible = ref(false);
|
||||
const currentMessage = ref<Message | null>(null);
|
||||
|
||||
// 未读数量
|
||||
const unreadCount = computed(
|
||||
() => messages.value.filter((m) => m.status === 'unread').length,
|
||||
);
|
||||
|
||||
// 获取消息类型配置
|
||||
const getTypeConfig = (type: string) => {
|
||||
switch (type) {
|
||||
case 'announcement': {
|
||||
return {
|
||||
type: 'success' as const,
|
||||
label: $t('message.typeMap.announcement'),
|
||||
};
|
||||
}
|
||||
case 'system': {
|
||||
return { type: 'info' as const, label: $t('message.typeMap.system') };
|
||||
}
|
||||
case 'todo': {
|
||||
return { type: 'warning' as const, label: $t('message.typeMap.todo') };
|
||||
}
|
||||
case 'workflow': {
|
||||
return {
|
||||
type: 'primary' as const,
|
||||
label: $t('message.typeMap.workflow'),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return { type: 'info' as const, label: $t('message.type') };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到审批链接
|
||||
const handleGoToLink = (message: Message) => {
|
||||
if (!message.link_type || !message.link_id) return;
|
||||
|
||||
// 根据 link_type 跳转到不同页面
|
||||
let url = '';
|
||||
if (message.link_type === 'workflow_task') {
|
||||
// 跳转到待办任务页面,带上任务ID
|
||||
url = `/app/workflow_center/workflow/pending?id=${message.link_id}`;
|
||||
} else if (message.link_type === 'workflow_instance') {
|
||||
// 跳转到我发起的页面,带上实例ID
|
||||
url = `/app/workflow_center/workflow/initiated?id=${message.link_id}`;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
// 处理 "2025-12-01 21:47:29" 格式,替换空格为T以兼容所有浏览器
|
||||
const date = new Date(dateStr.replace(' ', 'T'));
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60_000);
|
||||
const hours = Math.floor(diff / 3_600_000);
|
||||
const days = Math.floor(diff / 86_400_000);
|
||||
|
||||
if (minutes < 1)
|
||||
return $t('dashboard-design.widgets.announcement.time.justNow');
|
||||
if (minutes < 60)
|
||||
return `${minutes}${$t('dashboard-design.widgets.announcement.time.minutesAgo')}`;
|
||||
if (hours < 24)
|
||||
return `${hours}${$t('dashboard-design.widgets.announcement.time.hoursAgo')}`;
|
||||
if (days < 7)
|
||||
return `${days}${$t('dashboard-design.widgets.announcement.time.daysAgo')}`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// 加载消息数据
|
||||
const loadMessages = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const limit = props.widget.props.limit || 5;
|
||||
const res = await getMessageListApi({ page: 1, pageSize: limit });
|
||||
messages.value = res.items || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看消息详情
|
||||
const viewDetail = async (msg: Message) => {
|
||||
currentMessage.value = msg;
|
||||
detailVisible.value = true;
|
||||
|
||||
// 标记为已读
|
||||
if (msg.status === 'unread') {
|
||||
try {
|
||||
await markAsReadApi(msg.id);
|
||||
msg.status = 'read';
|
||||
} catch (error) {
|
||||
console.error('Failed to mark as read:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 全部已读
|
||||
const markAllRead = async () => {
|
||||
if (unreadCount.value === 0) return;
|
||||
|
||||
try {
|
||||
await markAllAsReadApi();
|
||||
messages.value.forEach((msg) => {
|
||||
msg.status = 'read';
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to mark all as read:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMessages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notice-list flex h-full flex-col p-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Bell class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
<ElTag v-if="unreadCount > 0" type="danger" size="small" round>
|
||||
{{ unreadCount }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="unreadCount > 0"
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
@click="markAllRead"
|
||||
>
|
||||
<CheckCheck class="mr-1 h-3.5 w-3.5" />
|
||||
{{ $t('dashboard-design.widgets.announcement.markAllRead') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElScrollbar class="flex-1">
|
||||
<div v-if="messages.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="flex cursor-pointer items-start gap-2 rounded-md p-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
:class="{ 'opacity-60': msg.status === 'read' }"
|
||||
@click="viewDetail(msg)"
|
||||
>
|
||||
<ElTag
|
||||
:type="getTypeConfig(msg.msg_type).type"
|
||||
size="small"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
{{ getTypeConfig(msg.msg_type).label }}
|
||||
</ElTag>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm">{{ msg.title }}</div>
|
||||
<div class="text-muted-foreground mt-1 flex items-center text-xs">
|
||||
<span v-if="msg.content" class="flex-1 truncate">{{
|
||||
msg.content
|
||||
}}</span>
|
||||
<span class="ml-2 flex-shrink-0">{{
|
||||
formatTime(msg.created_at)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-else
|
||||
:description="
|
||||
$t('message.noData') ||
|
||||
$t('dashboard-design.widgets.announcement.noData')
|
||||
"
|
||||
:image-size="60"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
|
||||
<!-- 消息详情弹窗 -->
|
||||
<ElDialog
|
||||
v-model="detailVisible"
|
||||
:title="currentMessage?.title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="currentMessage" class="min-h-[500px] space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<ElTag
|
||||
:type="getTypeConfig(currentMessage.msg_type).type"
|
||||
size="small"
|
||||
>
|
||||
{{ getTypeConfig(currentMessage.msg_type).label }}
|
||||
</ElTag>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
formatTime(currentMessage.created_at)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="whitespace-pre-wrap text-sm leading-relaxed">
|
||||
{{
|
||||
currentMessage.content ||
|
||||
$t('dashboard-design.widgets.announcement.noContent')
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- 审批链接按钮 -->
|
||||
<div
|
||||
v-if="currentMessage.link_type && currentMessage.link_id"
|
||||
class="mt-4"
|
||||
>
|
||||
<ElButton type="text" @click="handleGoToLink(currentMessage)">
|
||||
<ExternalLink class="mr-1 h-3.5 w-3.5" />
|
||||
查看详情
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentMessage.sender_name"
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
{{ $t('message.sender') || '发送者:'
|
||||
}}{{ currentMessage.sender_name }}
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { ElProgress } from 'element-plus';
|
||||
|
||||
defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="progress-card flex h-full flex-col justify-between p-4">
|
||||
<div class="text-muted-foreground text-sm">{{ widget.props.title }}</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<ElProgress
|
||||
type="dashboard"
|
||||
:percentage="widget.props.percentage"
|
||||
:status="widget.props.status || undefined"
|
||||
:stroke-width="widget.props.strokeWidth"
|
||||
:show-text="widget.props.showText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Grid, IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const appContextStore = useAppContextStore();
|
||||
|
||||
// 计算网格样式
|
||||
const gridStyle = computed(() => {
|
||||
const cols = props.widget.props.columns || 4;
|
||||
const rows = props.widget.props.rows || 2;
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
||||
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
|
||||
};
|
||||
});
|
||||
|
||||
// 计算总格子数
|
||||
const totalCells = computed(() => {
|
||||
const cols = props.widget.props.columns || 4;
|
||||
const rows = props.widget.props.rows || 2;
|
||||
return cols * rows;
|
||||
});
|
||||
|
||||
// 获取指定位置的菜单
|
||||
const getMenuAt = (index: number) => {
|
||||
const menus = props.widget.props.menus || [];
|
||||
return menus[index] || null;
|
||||
};
|
||||
|
||||
// 图标颜色样式
|
||||
const iconColorStyle = computed(() => {
|
||||
const color = props.widget.props.iconColor;
|
||||
return color ? { color } : {};
|
||||
});
|
||||
|
||||
// 获取 item 背景样式(支持渐变色)
|
||||
const getItemBgStyle = (menu: any) => {
|
||||
if (!menu?.bgColor) return {};
|
||||
if (menu.bgColor.includes('gradient')) {
|
||||
return { background: menu.bgColor };
|
||||
}
|
||||
return { backgroundColor: menu.bgColor };
|
||||
};
|
||||
|
||||
// 点击菜单项
|
||||
const handleClick = (menu: any) => {
|
||||
if (!menu || !menu.path) return;
|
||||
|
||||
// 外链
|
||||
if (menu.path.startsWith('http://') || menu.path.startsWith('https://')) {
|
||||
window.open(menu.path, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
// 路由跳转(子应用模式下始终使用 /app/{code} 前缀)
|
||||
const code = appContextStore.appCode;
|
||||
const targetPath = code ? `/app/${code}${menu.path}` : menu.path;
|
||||
router.push(targetPath);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quick-links flex h-full flex-col p-3">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Grid class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="grid flex-1 gap-2" :style="gridStyle">
|
||||
<template v-for="idx in totalCells" :key="idx">
|
||||
<div
|
||||
v-if="getMenuAt(idx - 1)"
|
||||
class="flex cursor-pointer flex-col items-center justify-center gap-1 rounded-lg p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:style="getItemBgStyle(getMenuAt(idx - 1))"
|
||||
@click="handleClick(getMenuAt(idx - 1))"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="getMenuAt(idx - 1)?.icon"
|
||||
:icon="getMenuAt(idx - 1).icon"
|
||||
class="h-6 w-6"
|
||||
:class="{ 'text-primary': !widget.props.iconColor }"
|
||||
:style="iconColorStyle"
|
||||
/>
|
||||
<Grid
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
:class="{ 'text-primary': !widget.props.iconColor }"
|
||||
:style="iconColorStyle"
|
||||
/>
|
||||
<span
|
||||
class="text-muted-foreground w-full truncate text-center text-xs"
|
||||
>{{ getMenuAt(idx - 1)?.title }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center gap-1 rounded-lg p-2"
|
||||
>
|
||||
<!-- 空位占位 -->
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { Award } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElScrollbar } from 'element-plus';
|
||||
|
||||
defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const getRankClass = (rank: number) => {
|
||||
switch (rank) {
|
||||
case 1: {
|
||||
return 'bg-yellow-500 text-white';
|
||||
}
|
||||
case 2: {
|
||||
return 'bg-gray-400 text-white';
|
||||
}
|
||||
case 3: {
|
||||
return 'bg-amber-600 text-white';
|
||||
}
|
||||
default: {
|
||||
return 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
if (value >= 10_000) {
|
||||
return `${(value / 10_000).toFixed(1)}${$t('dashboard-design.widgets.ranking.tenThousand')}`;
|
||||
}
|
||||
return value.toLocaleString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ranking-list flex h-full flex-col p-3">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Award class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
</div>
|
||||
<ElScrollbar class="flex-1">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="item in widget.props.items"
|
||||
:key="item.rank"
|
||||
class="flex items-center gap-3 rounded-md p-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-xs font-medium"
|
||||
:class="getRankClass(item.rank)"
|
||||
>
|
||||
{{ item.rank }}
|
||||
</div>
|
||||
<div class="flex-1 truncate text-sm">{{ item.name }}</div>
|
||||
<div class="text-muted-foreground text-sm font-medium">
|
||||
{{ formatValue(item.value) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
RealtimeStats,
|
||||
ServerMonitorResponse,
|
||||
} from '#/api/core/server-monitor';
|
||||
import type { DashboardWidget } from '#/components/dashboard-design';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Cpu, Database, HardDrive, Network } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElProgress } from 'element-plus';
|
||||
|
||||
import {
|
||||
getRealtimeStatsApi,
|
||||
getServerOverviewApi,
|
||||
} from '#/api/core/server-monitor';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const serverData = ref<null | ServerMonitorResponse>(null);
|
||||
const realtimeData = ref<null | RealtimeStats>(null);
|
||||
let timer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
// 刷新间隔
|
||||
const refreshInterval = computed(
|
||||
() => props.widget.props.refreshInterval || 5000,
|
||||
);
|
||||
|
||||
// 区域背景色
|
||||
function getAreaStyle(colorProp: string) {
|
||||
const color = props.widget.props[colorProp];
|
||||
if (!color) return {};
|
||||
if (color.includes('gradient')) return { background: color };
|
||||
return { backgroundColor: color };
|
||||
}
|
||||
|
||||
// 格式化字节
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
// 格式化内存(后端返回 GB)
|
||||
function formatMemory(gb: number): string {
|
||||
if (gb === 0) return '0 GB';
|
||||
if (gb < 1) return `${(gb * 1024).toFixed(0)} MB`;
|
||||
return `${gb.toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
// 格式化速度
|
||||
function formatSpeed(bytesPerSecond: number): string {
|
||||
return `${formatBytes(bytesPerSecond)}/s`;
|
||||
}
|
||||
|
||||
// 使用率颜色
|
||||
function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) return 'var(--el-color-danger)';
|
||||
if (percent >= 70) return 'var(--el-color-warning)';
|
||||
return 'var(--el-color-success)';
|
||||
}
|
||||
|
||||
// 格式化运行时间
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86_400);
|
||||
const hours = Math.floor((seconds % 86_400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const parts = [];
|
||||
if (days > 0)
|
||||
parts.push(`${days}${$t('dashboard-design.widgets.serverMonitor.days')}`);
|
||||
if (hours > 0)
|
||||
parts.push(`${hours}${$t('dashboard-design.widgets.serverMonitor.hours')}`);
|
||||
if (minutes > 0)
|
||||
parts.push(
|
||||
`${minutes}${$t('dashboard-design.widgets.serverMonitor.minutes')}`,
|
||||
);
|
||||
return parts.join(' ') || '-';
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData(showLoading = false) {
|
||||
if (showLoading) loading.value = true;
|
||||
try {
|
||||
const [overview, realtime] = await Promise.all([
|
||||
getServerOverviewApi(),
|
||||
getRealtimeStatsApi(),
|
||||
]);
|
||||
serverData.value = overview;
|
||||
realtimeData.value = realtime;
|
||||
} catch (error) {
|
||||
console.error('Failed to load server monitor data:', error);
|
||||
} finally {
|
||||
if (showLoading) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (timer) return;
|
||||
timer = setInterval(() => {
|
||||
getRealtimeStatsApi()
|
||||
.then((data) => {
|
||||
realtimeData.value = data;
|
||||
})
|
||||
.catch(() => {});
|
||||
}, refreshInterval.value);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData(true);
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="server-monitor flex h-full flex-col p-4" v-loading="loading">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ widget.props.title }}</span>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ serverData?.basic_info?.hostname || '-' }}
|
||||
({{ serverData?.basic_info?.ip_address || '-' }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 指标网格 -->
|
||||
<div class="grid max-h-[150px] flex-1 grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<!-- CPU -->
|
||||
<div
|
||||
class="flex flex-col justify-between rounded-lg p-4"
|
||||
:class="{ 'bg-secondary/50': !widget.props.cpuBgColor }"
|
||||
:style="getAreaStyle('cpuBgColor')"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md"
|
||||
style="background: rgba(59, 130, 246, 0.15)"
|
||||
>
|
||||
<Cpu class="h-4 w-4" style="color: var(--el-color-primary)" />
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">CPU</span>
|
||||
</div>
|
||||
<div class="mb-1 text-xl font-bold">
|
||||
{{ realtimeData?.cpu_percent?.toFixed(1) || '0.0' }}%
|
||||
</div>
|
||||
<ElProgress
|
||||
:percentage="Number(realtimeData?.cpu_percent?.toFixed(1) || 0)"
|
||||
:color="getProgressColor(realtimeData?.cpu_percent || 0)"
|
||||
:show-text="false"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ serverData?.cpu_info?.physical_cores || 0
|
||||
}}{{ $t('dashboard-design.widgets.serverMonitor.core') }}
|
||||
{{ serverData?.cpu_info?.total_cores || 0
|
||||
}}{{ $t('dashboard-design.widgets.serverMonitor.thread') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存 -->
|
||||
<div
|
||||
class="flex flex-col justify-between rounded-lg p-4"
|
||||
:class="{ 'bg-secondary/50': !widget.props.memoryBgColor }"
|
||||
:style="getAreaStyle('memoryBgColor')"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md"
|
||||
style="background: rgba(34, 197, 94, 0.15)"
|
||||
>
|
||||
<Database class="h-4 w-4" style="color: var(--el-color-success)" />
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.memory')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mb-1 text-xl font-bold">
|
||||
{{ realtimeData?.memory_percent?.toFixed(1) || '0.0' }}%
|
||||
</div>
|
||||
<ElProgress
|
||||
:percentage="Number(realtimeData?.memory_percent?.toFixed(1) || 0)"
|
||||
:color="getProgressColor(realtimeData?.memory_percent || 0)"
|
||||
:show-text="false"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ formatMemory(realtimeData?.memory_details?.used || 0) }}
|
||||
/ {{ formatMemory(realtimeData?.memory_details?.total || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 磁盘 -->
|
||||
<div
|
||||
class="flex flex-col justify-between rounded-lg p-4"
|
||||
:class="{ 'bg-secondary/50': !widget.props.diskBgColor }"
|
||||
:style="getAreaStyle('diskBgColor')"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md"
|
||||
style="background: rgba(139, 92, 246, 0.15)"
|
||||
>
|
||||
<HardDrive class="h-4 w-4" style="color: var(--el-color-warning)" />
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.disk')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-0.5 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.read')
|
||||
}}</span>
|
||||
<span class="text-xs font-medium">{{
|
||||
formatSpeed(realtimeData?.disk_io?.read_speed || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.write')
|
||||
}}</span>
|
||||
<span class="text-xs font-medium">{{
|
||||
formatSpeed(realtimeData?.disk_io?.write_speed || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('dashboard-design.widgets.serverMonitor.totalRW') }}:
|
||||
{{ formatBytes(realtimeData?.disk_total?.read_bytes || 0) }} /
|
||||
{{ formatBytes(realtimeData?.disk_total?.write_bytes || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网络 -->
|
||||
<div
|
||||
class="flex flex-col justify-between rounded-lg p-4"
|
||||
:class="{ 'bg-secondary/50': !widget.props.networkBgColor }"
|
||||
:style="getAreaStyle('networkBgColor')"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md"
|
||||
style="background: rgba(249, 115, 22, 0.15)"
|
||||
>
|
||||
<Network class="h-4 w-4" style="color: var(--el-color-danger)" />
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.network')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-0.5 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.upload')
|
||||
}}</span>
|
||||
<span class="text-xs font-medium">{{
|
||||
formatSpeed(realtimeData?.network_io?.upload_speed || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.serverMonitor.download')
|
||||
}}</span>
|
||||
<span class="text-xs font-medium">{{
|
||||
formatSpeed(realtimeData?.network_io?.download_speed || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
{{ $t('dashboard-design.widgets.serverMonitor.uptime') }}:
|
||||
{{ formatUptime(serverData?.boot_time?.uptime_seconds || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
Activity,
|
||||
Award,
|
||||
Bell,
|
||||
CreditCard,
|
||||
TrendingUp,
|
||||
Users,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
showTitle?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 图标映射
|
||||
const iconMap: Record<string, any> = {
|
||||
TrendingUp,
|
||||
CreditCard,
|
||||
Users,
|
||||
Activity,
|
||||
Bell,
|
||||
Award,
|
||||
};
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
return iconMap[props.widget.props.iconName] || TrendingUp;
|
||||
});
|
||||
|
||||
const trendClass = computed(() => {
|
||||
const trend = props.widget.props.trend || 0;
|
||||
if (trend > 0) return 'text-green-500';
|
||||
if (trend < 0) return 'text-red-500';
|
||||
return 'text-gray-500';
|
||||
});
|
||||
|
||||
const trendIcon = computed(() => {
|
||||
const trend = props.widget.props.trend || 0;
|
||||
if (trend > 0) return '↑';
|
||||
if (trend < 0) return '↓';
|
||||
return '';
|
||||
});
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
if (value >= 10_000) {
|
||||
return `${(value / 10_000).toFixed(1)}${$t('dashboard-design.widgets.ranking.tenThousand')}`;
|
||||
}
|
||||
return value.toLocaleString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stat-card flex h-full flex-col justify-between p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-muted-foreground text-sm">
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
<div class="mt-2 text-2xl font-bold">
|
||||
{{ widget.props.prefix }}{{ formatValue(widget.props.value)
|
||||
}}{{ widget.props.suffix }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg"
|
||||
:style="{ backgroundColor: `${widget.props.iconColor}20` }"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent"
|
||||
class="h-5 w-5"
|
||||
:style="{ color: widget.props.iconColor }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-1 text-sm">
|
||||
<span :class="trendClass">
|
||||
{{ trendIcon }} {{ Math.abs(widget.props.trend || 0) }}%
|
||||
</span>
|
||||
<span class="text-muted-foreground">{{ widget.props.trendLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { CheckSquare } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElCheckbox, ElScrollbar, ElTag } from 'element-plus';
|
||||
|
||||
defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const getPriorityType = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': {
|
||||
return 'danger';
|
||||
}
|
||||
case 'low': {
|
||||
return 'info';
|
||||
}
|
||||
case 'medium': {
|
||||
return 'warning';
|
||||
}
|
||||
default: {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityLabel = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': {
|
||||
return $t('dashboard-design.widgets.todo.priority.high');
|
||||
}
|
||||
case 'low': {
|
||||
return $t('dashboard-design.widgets.todo.priority.low');
|
||||
}
|
||||
case 'medium': {
|
||||
return $t('dashboard-design.widgets.todo.priority.medium');
|
||||
}
|
||||
default: {
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="todo-list flex h-full flex-col p-3">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<CheckSquare class="text-muted-foreground h-4 w-4" />
|
||||
<span class="text-muted-foreground text-sm font-medium">{{
|
||||
widget.props.title
|
||||
}}</span>
|
||||
</div>
|
||||
<ElScrollbar class="flex-1">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="item in widget.props.items"
|
||||
:key="item.id"
|
||||
class="flex items-center gap-2 rounded-md p-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<ElCheckbox :model-value="item.done" size="small" />
|
||||
<span
|
||||
class="flex-1 text-sm"
|
||||
:class="{ 'text-muted-foreground line-through': item.done }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<ElTag :type="getPriorityType(item.priority)" size="small">
|
||||
{{ getPriorityLabel(item.priority) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { Maximize, Pause, Play, Volume2, VolumeX } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { getFileUrl } from '#/composables/useFileUrl';
|
||||
|
||||
const props = defineProps<{
|
||||
isDesignMode?: boolean;
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const isPlaying = ref(false);
|
||||
const isMuted = ref(false);
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const progress = ref(0);
|
||||
|
||||
// 视频和封面URL(响应式)
|
||||
const videoUrl = ref('');
|
||||
const posterUrl = ref('');
|
||||
|
||||
// 异步解析URL
|
||||
async function resolveUrl(url: string): Promise<string> {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('file://')) {
|
||||
const fileId = url.slice(7);
|
||||
return await getFileUrl(fileId);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// 加载URL
|
||||
async function loadUrls() {
|
||||
videoUrl.value = await resolveUrl(props.widget.props.url || '');
|
||||
posterUrl.value = await resolveUrl(props.widget.props.poster || '');
|
||||
}
|
||||
|
||||
// 监听URL变化
|
||||
watch(
|
||||
() => [props.widget.props.url, props.widget.props.poster],
|
||||
() => {
|
||||
loadUrls();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const togglePlay = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
if (isPlaying.value) {
|
||||
videoRef.value.pause();
|
||||
} else {
|
||||
videoRef.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (!videoRef.value) return;
|
||||
videoRef.value.muted = !videoRef.value.muted;
|
||||
isMuted.value = videoRef.value.muted;
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
videoRef.value.requestFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (!videoRef.value) return;
|
||||
currentTime.value = videoRef.value.currentTime;
|
||||
progress.value = (currentTime.value / duration.value) * 100;
|
||||
};
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
if (!videoRef.value) return;
|
||||
duration.value = videoRef.value.duration;
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
isPlaying.value = true;
|
||||
};
|
||||
|
||||
const handlePause = () => {
|
||||
isPlaying.value = false;
|
||||
};
|
||||
|
||||
const handleSeek = (e: MouseEvent) => {
|
||||
if (!videoRef.value) return;
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
videoRef.value.currentTime = percent * duration.value;
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (videoRef.value) {
|
||||
isMuted.value = props.widget.props.muted || false;
|
||||
videoRef.value.muted = isMuted.value;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.widget.props.url,
|
||||
() => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.load();
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
progress.value = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="video-player flex h-full flex-col">
|
||||
<div
|
||||
v-if="widget.props.title"
|
||||
class="text-muted-foreground p-2 text-sm font-medium"
|
||||
>
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
|
||||
<div class="video-container relative min-h-0 flex-1 bg-black">
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="videoUrl"
|
||||
:poster="posterUrl"
|
||||
:autoplay="widget.props.autoplay && !isDesignMode"
|
||||
:loop="widget.props.loop"
|
||||
:controls="false"
|
||||
class="h-full w-full object-contain"
|
||||
@timeupdate="handleTimeUpdate"
|
||||
@loadedmetadata="handleLoadedMetadata"
|
||||
@play="handlePlay"
|
||||
@pause="handlePause"
|
||||
></video>
|
||||
|
||||
<!-- 自定义控制栏 -->
|
||||
<div v-if="!widget.props.controls" class="custom-controls">
|
||||
<button class="control-btn" @click="togglePlay">
|
||||
<Play v-if="!isPlaying" class="h-5 w-5" />
|
||||
<Pause v-else class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div class="progress-bar" @click="handleSeek">
|
||||
<div class="progress-fill" :style="{ width: `${progress}%` }"></div>
|
||||
</div>
|
||||
|
||||
<span class="time-display">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</span>
|
||||
|
||||
<button class="control-btn" @click="toggleMute">
|
||||
<VolumeX v-if="isMuted" class="h-4 w-4" />
|
||||
<Volume2 v-else class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button class="control-btn" @click="toggleFullscreen">
|
||||
<Maximize class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 原生控制栏 -->
|
||||
<video
|
||||
v-if="widget.props.controls"
|
||||
ref="videoRef"
|
||||
:src="videoUrl"
|
||||
:poster="posterUrl"
|
||||
:autoplay="widget.props.autoplay && !isDesignMode"
|
||||
:loop="widget.props.loop"
|
||||
:muted="widget.props.muted"
|
||||
controls
|
||||
class="h-full w-full object-contain"
|
||||
></video>
|
||||
|
||||
<!-- 设计模式遮罩 -->
|
||||
<div v-if="isDesignMode" class="design-overlay">
|
||||
<Play class="h-12 w-12 text-white/80" />
|
||||
<div class="mt-2 text-sm text-white/80">
|
||||
{{ $t('dashboard-design.widgets.video.placeholder') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.video-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-controls {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(transparent, rgb(0 0 0 / 70%));
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.video-container:hover .custom-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: rgb(255 255 255 / 30%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--el-color-primary);
|
||||
transition: width 0.1s;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
min-width: 80px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.design-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
}
|
||||
</style>
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Cloud,
|
||||
CloudDrizzle,
|
||||
CloudFog,
|
||||
CloudRain,
|
||||
CloudSnow,
|
||||
CloudSun,
|
||||
Sun,
|
||||
Zap,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
// 实时天气数据
|
||||
const weatherData = ref<null | {
|
||||
humidity: number;
|
||||
temperature: number;
|
||||
weatherCode: number;
|
||||
windDirection: number;
|
||||
windSpeed: number;
|
||||
}>(null);
|
||||
const error = ref(false);
|
||||
const locatedCityName = ref('');
|
||||
let timer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
// 显示的城市名:优先手动配置,其次自动定位
|
||||
const displayCityName = computed(() => {
|
||||
return props.widget.props.cityName || locatedCityName.value || '-';
|
||||
});
|
||||
|
||||
// 反向地理编码获取城市名
|
||||
async function reverseGeocode(lat: number, lon: number) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=zh`,
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
locatedCityName.value =
|
||||
data?.city || data?.locality || data?.principalSubdivision || '';
|
||||
} catch {
|
||||
// 反向编码失败不影响天气显示
|
||||
}
|
||||
}
|
||||
|
||||
// WMO Weather Code 映射
|
||||
function getWeatherInfo(code: number): {
|
||||
color: string;
|
||||
icon: any;
|
||||
label: string;
|
||||
} {
|
||||
// 晴天
|
||||
if (code === 0)
|
||||
return {
|
||||
icon: Sun,
|
||||
color: '#f59e0b',
|
||||
label: $t('dashboard-design.widgets.weather.codes.clear'),
|
||||
};
|
||||
// 少云/多云
|
||||
if (code === 1)
|
||||
return {
|
||||
icon: CloudSun,
|
||||
color: '#60a5fa',
|
||||
label: $t('dashboard-design.widgets.weather.codes.mainlyClear'),
|
||||
};
|
||||
if (code === 2)
|
||||
return {
|
||||
icon: CloudSun,
|
||||
color: '#60a5fa',
|
||||
label: $t('dashboard-design.widgets.weather.codes.partlyCloudy'),
|
||||
};
|
||||
if (code === 3)
|
||||
return {
|
||||
icon: Cloud,
|
||||
color: '#9ca3af',
|
||||
label: $t('dashboard-design.widgets.weather.codes.overcast'),
|
||||
};
|
||||
// 雾
|
||||
if (code === 45 || code === 48)
|
||||
return {
|
||||
icon: CloudFog,
|
||||
color: '#9ca3af',
|
||||
label: $t('dashboard-design.widgets.weather.codes.fog'),
|
||||
};
|
||||
// 毛毛雨
|
||||
if (code >= 51 && code <= 57)
|
||||
return {
|
||||
icon: CloudDrizzle,
|
||||
color: '#60a5fa',
|
||||
label: $t('dashboard-design.widgets.weather.codes.drizzle'),
|
||||
};
|
||||
// 雨
|
||||
if (code >= 61 && code <= 67)
|
||||
return {
|
||||
icon: CloudRain,
|
||||
color: '#3b82f6',
|
||||
label: $t('dashboard-design.widgets.weather.codes.rain'),
|
||||
};
|
||||
// 雪
|
||||
if (code >= 71 && code <= 77)
|
||||
return {
|
||||
icon: CloudSnow,
|
||||
color: '#a5b4fc',
|
||||
label: $t('dashboard-design.widgets.weather.codes.snow'),
|
||||
};
|
||||
// 阵雨
|
||||
if (code >= 80 && code <= 82)
|
||||
return {
|
||||
icon: CloudRain,
|
||||
color: '#3b82f6',
|
||||
label: $t('dashboard-design.widgets.weather.codes.showers'),
|
||||
};
|
||||
// 阵雪
|
||||
if (code >= 85 && code <= 86)
|
||||
return {
|
||||
icon: CloudSnow,
|
||||
color: '#a5b4fc',
|
||||
label: $t('dashboard-design.widgets.weather.codes.snowShowers'),
|
||||
};
|
||||
// 雷暴
|
||||
if (code >= 95 && code <= 99)
|
||||
return {
|
||||
icon: Zap,
|
||||
color: '#eab308',
|
||||
label: $t('dashboard-design.widgets.weather.codes.thunderstorm'),
|
||||
};
|
||||
return {
|
||||
icon: Sun,
|
||||
color: '#f59e0b',
|
||||
label: $t('dashboard-design.widgets.weather.codes.clear'),
|
||||
};
|
||||
}
|
||||
|
||||
// 风向角度转文字
|
||||
function getWindDirection(degree: number): string {
|
||||
const dirs = [
|
||||
$t('dashboard-design.widgets.weather.windDir.n'),
|
||||
$t('dashboard-design.widgets.weather.windDir.ne'),
|
||||
$t('dashboard-design.widgets.weather.windDir.e'),
|
||||
$t('dashboard-design.widgets.weather.windDir.se'),
|
||||
$t('dashboard-design.widgets.weather.windDir.s'),
|
||||
$t('dashboard-design.widgets.weather.windDir.sw'),
|
||||
$t('dashboard-design.widgets.weather.windDir.w'),
|
||||
$t('dashboard-design.widgets.weather.windDir.nw'),
|
||||
];
|
||||
const index = Math.round(degree / 45) % 8;
|
||||
return dirs[index] || '';
|
||||
}
|
||||
|
||||
// 当前天气信息
|
||||
const currentWeather = computed(() => {
|
||||
if (!weatherData.value) return null;
|
||||
const info = getWeatherInfo(weatherData.value.weatherCode);
|
||||
return {
|
||||
...info,
|
||||
temperature: Math.round(weatherData.value.temperature),
|
||||
humidity: weatherData.value.humidity,
|
||||
wind: `${getWindDirection(weatherData.value.windDirection)} ${weatherData.value.windSpeed.toFixed(0)} km/h`,
|
||||
};
|
||||
});
|
||||
|
||||
// 获取天气数据
|
||||
async function fetchWeather() {
|
||||
const lat = props.widget.props.latitude;
|
||||
const lon = props.widget.props.longitude;
|
||||
if (!lat || !lon) return;
|
||||
|
||||
try {
|
||||
error.value = false;
|
||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m&timezone=auto`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('API error');
|
||||
const data = await res.json();
|
||||
if (data.current) {
|
||||
weatherData.value = {
|
||||
temperature: data.current.temperature_2m,
|
||||
humidity: data.current.relative_humidity_2m,
|
||||
weatherCode: data.current.weather_code,
|
||||
windSpeed: data.current.wind_speed_10m,
|
||||
windDirection: data.current.wind_direction_10m,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
error.value = true;
|
||||
console.error('Failed to fetch weather data');
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
const interval = (props.widget.props.refreshInterval || 30) * 60 * 1000; // 分钟转毫秒
|
||||
timer = setInterval(fetchWeather, interval);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听经纬度变化重新获取
|
||||
watch(
|
||||
() => [props.widget.props.latitude, props.widget.props.longitude],
|
||||
() => {
|
||||
fetchWeather();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
// 如果开启自动定位且没有经纬度
|
||||
if (props.widget.props.autoLocate && !props.widget.props.latitude) {
|
||||
try {
|
||||
const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
const lat = pos.coords.latitude;
|
||||
const lon = pos.coords.longitude;
|
||||
// 并行获取天气数据和城市名
|
||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m&timezone=auto`;
|
||||
const [res] = await Promise.all([fetch(url), reverseGeocode(lat, lon)]);
|
||||
const data = await res.json();
|
||||
if (data.current) {
|
||||
weatherData.value = {
|
||||
temperature: data.current.temperature_2m,
|
||||
humidity: data.current.relative_humidity_2m,
|
||||
weatherCode: data.current.weather_code,
|
||||
windSpeed: data.current.wind_speed_10m,
|
||||
windDirection: data.current.wind_direction_10m,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// 定位失败,使用默认经纬度
|
||||
await fetchWeather();
|
||||
}
|
||||
} else {
|
||||
await fetchWeather();
|
||||
}
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="weather-widget flex h-full flex-col p-3">
|
||||
<div
|
||||
v-if="widget.props.title"
|
||||
class="text-muted-foreground mb-2 text-sm font-medium"
|
||||
>
|
||||
{{ widget.props.title }}
|
||||
</div>
|
||||
|
||||
<!-- 加载/错误状态 -->
|
||||
<div
|
||||
v-if="!currentWeather && !error"
|
||||
class="flex flex-1 items-center justify-center"
|
||||
>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.weather.loading')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="error && !currentWeather"
|
||||
class="flex flex-1 items-center justify-center"
|
||||
>
|
||||
<span class="text-muted-foreground text-xs">{{
|
||||
$t('dashboard-design.widgets.weather.error')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 天气数据 -->
|
||||
<div v-else-if="currentWeather" class="flex flex-1 items-center gap-4">
|
||||
<!-- 天气图标和温度 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<component
|
||||
:is="currentWeather.icon"
|
||||
class="h-12 w-12"
|
||||
:style="{ color: currentWeather.color }"
|
||||
/>
|
||||
<div>
|
||||
<div class="temperature">{{ currentWeather.temperature }}°</div>
|
||||
<div class="weather-text">
|
||||
{{ currentWeather.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<div class="weather-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{
|
||||
$t('dashboard-design.widgets.weather.city')
|
||||
}}</span>
|
||||
<span class="detail-value">{{ displayCityName }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{
|
||||
$t('dashboard-design.widgets.weather.humidity')
|
||||
}}</span>
|
||||
<span class="detail-value">{{ currentWeather.humidity }}%</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">{{
|
||||
$t('dashboard-design.widgets.weather.wind')
|
||||
}}</span>
|
||||
<span class="detail-value">{{ currentWeather.wind }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.temperature {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.weather-text {
|
||||
margin-top: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.weather-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: 32px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardWidget } from '../../types';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Smile } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { UserAvatar } from '#/components/user-avatar';
|
||||
|
||||
defineProps<{
|
||||
widget: DashboardWidget;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
// 用户名
|
||||
const userName = computed(() => userStore.userInfo?.realName || '');
|
||||
|
||||
const currentTime = ref(new Date());
|
||||
let timer: null | ReturnType<typeof setInterval> = null;
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
currentTime.value = new Date();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
|
||||
const greeting = computed(() => {
|
||||
const hour = currentTime.value.getHours();
|
||||
if (hour < 6) return $t('dashboard-design.widgets.welcome.greeting.night');
|
||||
if (hour < 9) return $t('dashboard-design.widgets.welcome.greeting.morning');
|
||||
if (hour < 12)
|
||||
return $t('dashboard-design.widgets.welcome.greeting.morning2');
|
||||
if (hour < 14) return $t('dashboard-design.widgets.welcome.greeting.noon');
|
||||
if (hour < 18)
|
||||
return $t('dashboard-design.widgets.welcome.greeting.afternoon');
|
||||
if (hour < 22) return $t('dashboard-design.widgets.welcome.greeting.evening');
|
||||
return $t('dashboard-design.widgets.welcome.greeting.night');
|
||||
});
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
return currentTime.value.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return currentTime.value.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="welcome-card flex h-full items-center justify-between p-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 用户头像 -->
|
||||
<UserAvatar
|
||||
v-if="userInfo"
|
||||
:name="userName"
|
||||
:avatar="userInfo.avatar"
|
||||
:size="48"
|
||||
:font-size="20"
|
||||
:show-popover="false"
|
||||
:shadow="false"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-3),
|
||||
var(--el-color-primary)
|
||||
);
|
||||
"
|
||||
>
|
||||
<Smile class="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-semibold">
|
||||
{{ greeting }},{{ userName
|
||||
}}{{ widget.props.title ? `,${widget.props.title}` : '' }}
|
||||
</div>
|
||||
<div class="text-muted-foreground text-sm">
|
||||
{{ widget.props.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="widget.props.showTime" class="text-right">
|
||||
<div class="text-2xl font-bold tabular-nums">{{ formattedTime }}</div>
|
||||
<div class="text-muted-foreground text-sm">{{ formattedDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 背景色由 WidgetRenderer 控制 */
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
export { default as DashboardRenderer } from './DashboardRenderer.vue';
|
||||
export type {
|
||||
DashboardConfig,
|
||||
DashboardWidget,
|
||||
DataSourceConfig,
|
||||
DataSourceType,
|
||||
WidgetStyle,
|
||||
WidgetType,
|
||||
} from './types';
|
||||
export { createRefreshTimer, fetchWidgetData } from './utils/dataFetcher';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
const globalParams = ref<Record<string, any>>({});
|
||||
const globalParamsVersion = ref(0);
|
||||
|
||||
function updateGlobalParam(key: string, value: any) {
|
||||
globalParams.value = { ...globalParams.value, [key]: value };
|
||||
globalParamsVersion.value++;
|
||||
}
|
||||
|
||||
function clearGlobalParams() {
|
||||
globalParams.value = {};
|
||||
globalParamsVersion.value++;
|
||||
}
|
||||
|
||||
export function useDashboardRuntime() {
|
||||
return {
|
||||
clearGlobalParams,
|
||||
globalParams,
|
||||
globalParamsVersion,
|
||||
updateGlobalParam,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
export type DataSourceType = 'api' | 'dataSource' | 'static' | 'upload';
|
||||
|
||||
export interface FieldMapping {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface ParamBinding {
|
||||
paramName: string;
|
||||
globalKey: string;
|
||||
}
|
||||
|
||||
export interface DataSourceConfig {
|
||||
type: DataSourceType;
|
||||
dataSourceCode?: string;
|
||||
apiUrl?: string;
|
||||
apiMethod?: 'GET' | 'POST';
|
||||
apiHeaders?: Record<string, string>;
|
||||
apiParams?: Record<string, any>;
|
||||
apiBody?: Record<string, any>;
|
||||
dataPath?: string;
|
||||
fieldMappings?: FieldMapping[];
|
||||
paramBindings?: ParamBinding[];
|
||||
refreshInterval?: number;
|
||||
refreshEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type WidgetType =
|
||||
| 'announcement-list'
|
||||
| 'approval-center'
|
||||
| 'calendar'
|
||||
| 'chart-area'
|
||||
| 'chart-bar'
|
||||
| 'chart-funnel'
|
||||
| 'chart-gauge'
|
||||
| 'chart-heatmap'
|
||||
| 'chart-kline'
|
||||
| 'chart-line'
|
||||
| 'chart-pie'
|
||||
| 'chart-radar'
|
||||
| 'chart-ring'
|
||||
| 'chart-sankey'
|
||||
| 'chart-scatter'
|
||||
| 'clock'
|
||||
| 'countdown'
|
||||
| 'data-table'
|
||||
| 'filter-date'
|
||||
| 'filter-date-range'
|
||||
| 'filter-input'
|
||||
| 'filter-select'
|
||||
| 'form-render'
|
||||
| 'iframe'
|
||||
| 'image'
|
||||
| 'image-carousel'
|
||||
| 'my-apps'
|
||||
| 'notice-list'
|
||||
| 'progress-card'
|
||||
| 'quick-links'
|
||||
| 'ranking-list'
|
||||
| 'server-monitor'
|
||||
| 'stat-card'
|
||||
| 'todo-list'
|
||||
| 'video-player'
|
||||
| 'weather'
|
||||
| 'welcome-card';
|
||||
|
||||
export interface WidgetStyle {
|
||||
backgroundColor?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundSize?: 'auto' | 'contain' | 'cover';
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
borderStyle?: 'dashed' | 'dotted' | 'none' | 'solid';
|
||||
borderRadius?: number;
|
||||
shadowEnabled?: boolean;
|
||||
shadowColor?: string;
|
||||
shadowBlur?: number;
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
padding?: number;
|
||||
titleShow?: boolean;
|
||||
titleFontSize?: number;
|
||||
titleColor?: string;
|
||||
titleAlign?: 'center' | 'left' | 'right';
|
||||
titleFontWeight?: 'bold' | 'normal';
|
||||
}
|
||||
|
||||
export interface DashboardWidget {
|
||||
id: string;
|
||||
type: WidgetType;
|
||||
i: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
minW?: number;
|
||||
minH?: number;
|
||||
maxW?: number;
|
||||
maxH?: number;
|
||||
title?: string;
|
||||
props: Record<string, any>;
|
||||
style?: WidgetStyle;
|
||||
dataSource?: DataSourceConfig;
|
||||
}
|
||||
|
||||
export interface DashboardConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
columns: number;
|
||||
rowHeight: number;
|
||||
margin: [number, number];
|
||||
backgroundColor?: string;
|
||||
showOuterMargin?: boolean;
|
||||
widgets: DashboardWidget[];
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import type {
|
||||
DataSourceConfig,
|
||||
FieldMapping,
|
||||
} from '../types';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 根据路径获取对象中的值
|
||||
* @param obj 对象
|
||||
* @param path 路径,如 'data.list' 或 'data.items[0].name'
|
||||
*/
|
||||
export function getValueByPath(obj: any, path: string): any {
|
||||
if (!obj || !path) return obj;
|
||||
|
||||
const keys = path.replaceAll(/\[(\d+)\]/g, '.$1').split('.');
|
||||
let result = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (result === null || result === undefined) return undefined;
|
||||
result = result[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径设置对象中的值
|
||||
*/
|
||||
export function setValueByPath(obj: any, path: string, value: any): void {
|
||||
if (!obj || !path) return;
|
||||
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i]!;
|
||||
if (current[key] === undefined) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]!] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用字段映射
|
||||
*/
|
||||
export function applyFieldMappings(
|
||||
data: any,
|
||||
mappings: FieldMapping[] | undefined,
|
||||
targetProps: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
if (!mappings || mappings.length === 0) {
|
||||
return { ...targetProps, ...data };
|
||||
}
|
||||
|
||||
const result = { ...targetProps };
|
||||
|
||||
for (const mapping of mappings) {
|
||||
const value = getValueByPath(data, mapping.source);
|
||||
if (value !== undefined) {
|
||||
setValueByPath(result, mapping.target, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据获取工具
|
||||
*/
|
||||
export async function fetchWidgetData(
|
||||
dataSource: DataSourceConfig | undefined,
|
||||
defaultProps: Record<string, any>,
|
||||
params?: Record<string, any>,
|
||||
): Promise<{ data: any; props: Record<string, any> }> {
|
||||
if (!dataSource || dataSource.type === 'static') {
|
||||
return { data: null, props: defaultProps };
|
||||
}
|
||||
|
||||
// 通用数据源类型
|
||||
if (dataSource.type === 'dataSource' && dataSource.dataSourceCode) {
|
||||
try {
|
||||
const response = await requestClient.get(
|
||||
`/api/core/data-source/execute/${dataSource.dataSourceCode}`,
|
||||
{ params: params && Object.keys(params).length > 0 ? params : undefined },
|
||||
);
|
||||
|
||||
// 后端返回格式是 {data: ...},需要提取 data 字段
|
||||
const rawData = response?.data ?? response;
|
||||
|
||||
// 数据源返回的数据可能是数组或对象(图表数据源返回 {xAxisData, seriesData} 格式)
|
||||
const extractedData = dataSource.dataPath
|
||||
? getValueByPath(rawData, dataSource.dataPath)
|
||||
: rawData;
|
||||
|
||||
// 检查是否是图表数据格式(包含 xAxisData 或 seriesData 或 indicator 或 value)
|
||||
if (
|
||||
extractedData &&
|
||||
typeof extractedData === 'object' &&
|
||||
!Array.isArray(extractedData) &&
|
||||
('xAxisData' in extractedData ||
|
||||
'seriesData' in extractedData ||
|
||||
'indicator' in extractedData ||
|
||||
'yAxisData' in extractedData ||
|
||||
('value' in extractedData && 'max' in extractedData))
|
||||
) {
|
||||
// 图表数据格式,直接合并到 props
|
||||
const mappedProps = { ...defaultProps, ...extractedData };
|
||||
// 如果有额外的字段映射,也应用
|
||||
if (dataSource.fieldMappings && dataSource.fieldMappings.length > 0) {
|
||||
return {
|
||||
data: extractedData,
|
||||
props: applyFieldMappings(
|
||||
extractedData,
|
||||
dataSource.fieldMappings,
|
||||
mappedProps,
|
||||
),
|
||||
};
|
||||
}
|
||||
return { data: extractedData, props: mappedProps };
|
||||
}
|
||||
|
||||
// 普通数据,应用字段映射
|
||||
const mappedProps = applyFieldMappings(
|
||||
extractedData,
|
||||
dataSource.fieldMappings,
|
||||
defaultProps,
|
||||
);
|
||||
|
||||
return { data: extractedData, props: mappedProps };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data source:', error);
|
||||
return { data: null, props: defaultProps };
|
||||
}
|
||||
}
|
||||
|
||||
// API 类型
|
||||
if (dataSource.type === 'api' && dataSource.apiUrl) {
|
||||
try {
|
||||
const method = dataSource.apiMethod || 'GET';
|
||||
const params = dataSource.apiParams || {};
|
||||
const body = dataSource.apiBody || {};
|
||||
const headers = dataSource.apiHeaders || {};
|
||||
|
||||
let response: any;
|
||||
|
||||
response = await (method === 'GET'
|
||||
? requestClient.get(dataSource.apiUrl, {
|
||||
params,
|
||||
headers,
|
||||
})
|
||||
: requestClient.post(dataSource.apiUrl, body, {
|
||||
params,
|
||||
headers,
|
||||
}));
|
||||
|
||||
// 根据 dataPath 提取数据
|
||||
const extractedData = dataSource.dataPath
|
||||
? getValueByPath(response, dataSource.dataPath)
|
||||
: response;
|
||||
|
||||
// 应用字段映射
|
||||
const mappedProps = applyFieldMappings(
|
||||
extractedData,
|
||||
dataSource.fieldMappings,
|
||||
defaultProps,
|
||||
);
|
||||
|
||||
return { data: extractedData, props: mappedProps };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch widget data:', error);
|
||||
return { data: null, props: defaultProps };
|
||||
}
|
||||
}
|
||||
|
||||
return { data: null, props: defaultProps };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自动刷新定时器
|
||||
*/
|
||||
export function createRefreshTimer(
|
||||
dataSource: DataSourceConfig | undefined,
|
||||
callback: () => void,
|
||||
): (() => void) | null {
|
||||
if (
|
||||
!dataSource?.refreshEnabled ||
|
||||
!dataSource.refreshInterval ||
|
||||
dataSource.refreshInterval <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timer = setInterval(callback, dataSource.refreshInterval * 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件支持的字段映射目标
|
||||
*/
|
||||
export function getWidgetFieldTargets(
|
||||
widgetType: string,
|
||||
): { key: string; label: string }[] {
|
||||
const commonFields = [
|
||||
{ key: 'title', label: $t('dashboard-design.attribute.fieldLabels.title') },
|
||||
];
|
||||
|
||||
const fieldMap: Record<string, { key: string; label: string }[]> = {
|
||||
'stat-card': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'value',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.value'),
|
||||
},
|
||||
{
|
||||
key: 'trend',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.trend'),
|
||||
},
|
||||
{
|
||||
key: 'trendLabel',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.trendLabel'),
|
||||
},
|
||||
{
|
||||
key: 'prefix',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.prefix'),
|
||||
},
|
||||
{
|
||||
key: 'suffix',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.suffix'),
|
||||
},
|
||||
],
|
||||
'progress-card': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'percentage',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.percentage'),
|
||||
},
|
||||
],
|
||||
'chart-line': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'xAxisData',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.xAxisData'),
|
||||
},
|
||||
{
|
||||
key: 'seriesData',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.seriesData'),
|
||||
},
|
||||
],
|
||||
'chart-bar': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'xAxisData',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.xAxisData'),
|
||||
},
|
||||
{
|
||||
key: 'seriesData',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.seriesData'),
|
||||
},
|
||||
],
|
||||
'chart-pie': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'seriesData',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.seriesData'),
|
||||
},
|
||||
],
|
||||
'chart-gauge': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'value',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.currentValue'),
|
||||
},
|
||||
{ key: 'min', label: $t('dashboard-design.attribute.fieldLabels.min') },
|
||||
{ key: 'max', label: $t('dashboard-design.attribute.fieldLabels.max') },
|
||||
],
|
||||
'todo-list': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'items',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.listData'),
|
||||
},
|
||||
],
|
||||
'notice-list': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'items',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.listData'),
|
||||
},
|
||||
],
|
||||
'ranking-list': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'items',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.listData'),
|
||||
},
|
||||
],
|
||||
'quick-links': [
|
||||
...commonFields,
|
||||
{
|
||||
key: 'links',
|
||||
label: $t('dashboard-design.attribute.fieldLabels.linkData'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return fieldMap[widgetType] || commonFields;
|
||||
}
|
||||
@@ -0,0 +1,894 @@
|
||||
{
|
||||
"title": "Dashboard Design",
|
||||
"preview": "Preview",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Save successful",
|
||||
"clear": "Clear",
|
||||
"clearConfirm": "Are you sure you want to clear the canvas? This action cannot be undone.",
|
||||
"clearSuccess": "Cleared",
|
||||
"export": "Export",
|
||||
"exportSuccess": "Export successful",
|
||||
"import": "Import",
|
||||
"importTitle": "Import Configuration",
|
||||
"importPlaceholder": "Please paste JSON configuration content...",
|
||||
"importSuccess": "Import successful",
|
||||
"importError": "Invalid configuration format",
|
||||
"importEmpty": "Please enter configuration content",
|
||||
"viewCode": "View Code",
|
||||
"jsonPreview": "JSON Preview",
|
||||
"copyCode": "Copy Code",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"copyError": "Copy failed",
|
||||
"undoSuccess": "Undone",
|
||||
"redoSuccess": "Redone",
|
||||
"copyWidgetSuccess": "Copied",
|
||||
"pasteWidgetSuccess": "Pasted",
|
||||
"deleteWidgetSuccess": "Deleted",
|
||||
"noWidgetsTip": "Please add widgets first",
|
||||
"noConfigTip": "No dashboard configuration",
|
||||
"loadingData": "Loading data...",
|
||||
"loadDataError": "Failed to load data",
|
||||
"unknownWidget": "Unknown widget type",
|
||||
"widgetCount": "{count} widgets",
|
||||
"dragTip": "Drag widgets from the left to here",
|
||||
"add": "Add",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"reset": "Reset",
|
||||
"clean": "Clear",
|
||||
"canvas": {
|
||||
"title": "Design Canvas",
|
||||
"adaptive": "Adaptive",
|
||||
"actualSize": "Actual Size",
|
||||
"settings": "Canvas Settings",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"months": {
|
||||
"jan": "Jan",
|
||||
"feb": "Feb",
|
||||
"mar": "Mar",
|
||||
"apr": "Apr",
|
||||
"may": "May",
|
||||
"jun": "Jun",
|
||||
"jul": "Jul",
|
||||
"aug": "Aug",
|
||||
"sep": "Sep",
|
||||
"oct": "Oct",
|
||||
"nov": "Nov",
|
||||
"dec": "Dec"
|
||||
},
|
||||
"weekdaysShort": {
|
||||
"mon": "Mon",
|
||||
"tue": "Tue",
|
||||
"wed": "Wed",
|
||||
"thu": "Thu",
|
||||
"fri": "Fri",
|
||||
"sat": "Sat",
|
||||
"sun": "Sun"
|
||||
},
|
||||
"material": {
|
||||
"title": "Material Library",
|
||||
"search": "Search widgets...",
|
||||
"category": {
|
||||
"common": "Common",
|
||||
"chart": "Chart",
|
||||
"filter": "Filter",
|
||||
"map": "Map",
|
||||
"media": "Media",
|
||||
"other": "Other"
|
||||
},
|
||||
"widgets": {
|
||||
"statCard": "Stat Card",
|
||||
"progressCard": "Progress Card",
|
||||
"chartLine": "Line Chart",
|
||||
"chartBar": "Bar Chart",
|
||||
"chartPie": "Pie Chart",
|
||||
"chartGauge": "Gauge Chart",
|
||||
"chartArea": "Area Chart",
|
||||
"chartRadar": "Radar Chart",
|
||||
"chartFunnel": "Funnel Chart",
|
||||
"chartScatter": "Scatter Chart",
|
||||
"chartRing": "Ring Progress",
|
||||
"chartHeatmap": "Heatmap",
|
||||
"chartKline": "K-line Chart",
|
||||
"chartSankey": "Sankey Chart",
|
||||
"todoList": "Todo List",
|
||||
"noticeList": "Message Notification",
|
||||
"announcementList": "Announcement List",
|
||||
"rankingList": "Ranking List",
|
||||
"quickLinks": "Quick Links",
|
||||
"welcomeCard": "Welcome Card",
|
||||
"calendar": "Calendar",
|
||||
"countdown": "Countdown",
|
||||
"clock": "Clock",
|
||||
"weather": "Weather",
|
||||
"imageCarousel": "Image Carousel",
|
||||
"dataTable": "Data Table",
|
||||
"iframe": "iframe Embed",
|
||||
"videoPlayer": "Video Player",
|
||||
"image": "Image",
|
||||
"formRender": "Form Render",
|
||||
"approvalCenter": "Approval Center",
|
||||
"myApps": "My Apps",
|
||||
"serverMonitor": "Server Monitor",
|
||||
"filterInput": "Input Filter",
|
||||
"filterSelect": "Select Filter",
|
||||
"filterDate": "Date Filter",
|
||||
"filterDateRange": "Date Range"
|
||||
},
|
||||
"defaultProps": {
|
||||
"statTitle": "Statistic Data",
|
||||
"trendLabel": "vs Yesterday",
|
||||
"progressTitle": "Completion Progress",
|
||||
"visitTrend": "Visit Trend",
|
||||
"visits": "Visits",
|
||||
"downloads": "Downloads",
|
||||
"salesStat": "Sales Statistics",
|
||||
"trafficSource": "Traffic Source",
|
||||
"searchEngine": "Search Engine",
|
||||
"directAccess": "Direct Access",
|
||||
"emailMarketing": "Email Marketing",
|
||||
"unionAds": "Union Ads",
|
||||
"videoAds": "Video Ads",
|
||||
"sysLoad": "System Load",
|
||||
"abilityEval": "Ability Evaluation",
|
||||
"sales": "Sales",
|
||||
"mgmt": "Mgmt",
|
||||
"tech": "Tech",
|
||||
"cs": "CS",
|
||||
"rd": "R&D",
|
||||
"mkt": "Mkt",
|
||||
"budget": "Budget",
|
||||
"actual": "Actual",
|
||||
"convFunnel": "Conversion Funnel",
|
||||
"visit": "Visit",
|
||||
"consult": "Consult",
|
||||
"intent": "Intent",
|
||||
"order": "Order",
|
||||
"deal": "Deal",
|
||||
"dataDist": "Data Distribution",
|
||||
"height": "Height (cm)",
|
||||
"weight": "Weight (kg)",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"kpiDone": "KPI Completion",
|
||||
"salesVolume": "Sales Volume",
|
||||
"orderVolume": "Order Volume",
|
||||
"customerCount": "Customer Count",
|
||||
"weekVisitHeat": "Weekly Visit Heat",
|
||||
"stockTrend": "Stock Trend",
|
||||
"todoTitle": "To-do Items",
|
||||
"todoItem1": "Complete project report",
|
||||
"todoItem2": "Team weekly meeting",
|
||||
"todoItem3": "Code review",
|
||||
"latestNotice": "Message Notification",
|
||||
"latestAnnouncement": "Latest Announcements",
|
||||
"salesRanking": "Sales Ranking",
|
||||
"welcomeTitle": "Welcome back",
|
||||
"welcomeSubtitle": "Today is a good day",
|
||||
"countdownTitle": "Event Countdown",
|
||||
"todayWeather": "Today's Weather",
|
||||
"dataList": "Data List",
|
||||
"externalPage": "External Page",
|
||||
"itemName": "Item",
|
||||
"imageTitle": "Image",
|
||||
"myDashboard": "My Dashboard",
|
||||
"formRender": "Form Data",
|
||||
"approvalCenter": "Approval Center",
|
||||
"myApps": "My Apps",
|
||||
"serverMonitor": "Server Monitor",
|
||||
"filterInput": "Keyword",
|
||||
"filterInputPlaceholder": "Enter keyword to filter...",
|
||||
"filterSelect": "Select Filter",
|
||||
"filterSelectPlaceholder": "Please select...",
|
||||
"filterDate": "Date",
|
||||
"filterDatePlaceholder": "Select date",
|
||||
"filterDateRange": "Date Range",
|
||||
"filterStartDate": "Start Date",
|
||||
"filterEndDate": "End Date"
|
||||
}
|
||||
},
|
||||
"attribute": {
|
||||
"title": "Attribute Panel",
|
||||
"canvasConfig": "Canvas Config",
|
||||
"widgetConfig": "Widget Config",
|
||||
"styleConfig": "Style Config",
|
||||
"dataConfig": "Data Config",
|
||||
"globalSettings": "Global Settings",
|
||||
"dashboardName": "Dashboard Name",
|
||||
"gridLayout": "Grid Layout",
|
||||
"columns": "Columns",
|
||||
"rowHeight": "Row Height (px)",
|
||||
"widgetMargin": "Widget Margin (px)",
|
||||
"horizontal": "Horizontal",
|
||||
"vertical": "Vertical",
|
||||
"background": "Background",
|
||||
"backgroundColor": "Background Color",
|
||||
"reset": "Reset",
|
||||
"display": "Display",
|
||||
"outerMargin": "Outer Margin",
|
||||
"outerMarginTip": "Show outer margin during rendering",
|
||||
"widgetTitle": "Title",
|
||||
"layout": "Layout",
|
||||
"widthGrid": "Width (Grid)",
|
||||
"heightGrid": "Height (Grid)",
|
||||
"layoutTip": "Tip: Resize widgets directly on the canvas",
|
||||
"iconConfig": "Icon Config",
|
||||
"icon": "Icon",
|
||||
"iconColor": "Icon Color",
|
||||
"status": "Status",
|
||||
"strokeWidth": "Stroke Width",
|
||||
"showText": "Show Text",
|
||||
"border": "Border",
|
||||
"borderWidth": "Border Width",
|
||||
"borderColor": "Border Color",
|
||||
"borderStyleLabel": "Border Style",
|
||||
"borderRadius": "Corner Radius",
|
||||
"shadow": "Shadow",
|
||||
"enableShadow": "Enable Shadow",
|
||||
"shadowColor": "Shadow Color",
|
||||
"shadowBlur": "Blur Radius",
|
||||
"chartLayout": "Chart Layout",
|
||||
"margin": {
|
||||
"left": "Left Margin (%)",
|
||||
"right": "Right Margin (%)",
|
||||
"top": "Top Margin (%)",
|
||||
"bottom": "Bottom Margin (%)"
|
||||
},
|
||||
"tabs": {
|
||||
"basic": "Basic",
|
||||
"data": "Data",
|
||||
"style": "Style"
|
||||
},
|
||||
"placeholder": {
|
||||
"name": "Please enter name",
|
||||
"title": "Please enter title",
|
||||
"remark": "Please enter description"
|
||||
},
|
||||
"iconOptions": {
|
||||
"trending": "Trending",
|
||||
"creditCard": "Credit Card",
|
||||
"users": "Users",
|
||||
"activity": "Activity",
|
||||
"bell": "Bell",
|
||||
"award": "Award"
|
||||
},
|
||||
"progressStatus": {
|
||||
"default": "Default",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"exception": "Exception"
|
||||
},
|
||||
"colorTheme": {
|
||||
"label": "Color Theme",
|
||||
"default": "Default",
|
||||
"fresh": "Fresh",
|
||||
"business": "Business",
|
||||
"tech": "Tech",
|
||||
"warm": "Warm"
|
||||
},
|
||||
"chart": {
|
||||
"smooth": "Smooth Curve",
|
||||
"showArea": "Show Area",
|
||||
"showSymbol": "Show Symbol",
|
||||
"symbolSize": "Symbol Size",
|
||||
"lineWidth": "Line Width",
|
||||
"axis": "Axis",
|
||||
"xAxisName": "X-Axis Name",
|
||||
"yAxisName": "Y-Axis Name",
|
||||
"nameLocation": "Name Location",
|
||||
"legend": "Legend",
|
||||
"showLegend": "Show Legend",
|
||||
"legendPosition": "Legend Position",
|
||||
"barWidth": "Bar Width",
|
||||
"barRadius": "Bar Radius",
|
||||
"horizontal": "Horizontal",
|
||||
"stack": "Stack",
|
||||
"showBackground": "Show Background",
|
||||
"pieType": "Pie Type",
|
||||
"showLabel": "Show Label",
|
||||
"labelPosition": "Label Position",
|
||||
"minValue": "Min Value",
|
||||
"maxValue": "Max Value",
|
||||
"splitNumber": "Split Number",
|
||||
"showProgress": "Show Progress",
|
||||
"showMA5": "Show MA5",
|
||||
"showMA10": "Show MA10",
|
||||
"shape": "Shape",
|
||||
"sort": "Sort",
|
||||
"orient": "Orient",
|
||||
"pointSize": "Point Size",
|
||||
"numerical": "Numerical Config",
|
||||
"currentValue": "Current Value",
|
||||
"min": "Min",
|
||||
"max": "Max",
|
||||
"unit": "Unit",
|
||||
"maConfig": "MA Config",
|
||||
"prefix": "Prefix",
|
||||
"suffix": "Suffix",
|
||||
"trendConfig": "Trend Config",
|
||||
"trendValue": "Trend Value (%)",
|
||||
"trendTip": "Positive for increase, negative for decrease",
|
||||
"trendLabel": "Trend Description",
|
||||
"seriesNamePrefix": "Series",
|
||||
"itemNamePrefix": "Item",
|
||||
"placeholder": {
|
||||
"xAxis": "e.g. Month",
|
||||
"yAxis": "e.g. Sales",
|
||||
"unit": "e.g. %",
|
||||
"title": "Please enter title",
|
||||
"xAxis2": "Separated by commas, e.g. Jan, Feb, Mar",
|
||||
"seriesData": "Separated by commas, e.g. 100, 200, 300",
|
||||
"seriesName": "Series Name",
|
||||
"prefix": "e.g. $",
|
||||
"suffix": "e.g. items, %",
|
||||
"trendLabel": "e.g. vs Yesterday",
|
||||
"subtitle": "Please enter subtitle"
|
||||
},
|
||||
"location": {
|
||||
"start": "Start",
|
||||
"middle": "Middle",
|
||||
"end": "End",
|
||||
"top": "Top",
|
||||
"bottom": "Bottom",
|
||||
"left": "Left",
|
||||
"right": "Right"
|
||||
},
|
||||
"pieTypes": {
|
||||
"pie": "Pie",
|
||||
"ring": "Ring",
|
||||
"rose": "Rose"
|
||||
},
|
||||
"labelPositions": {
|
||||
"outside": "Outside",
|
||||
"inside": "Inside"
|
||||
},
|
||||
"barWidthOptions": {
|
||||
"auto": "Auto",
|
||||
"thin": "Thin (30%)",
|
||||
"medium": "Medium (50%)",
|
||||
"thick": "Thick (70%)"
|
||||
},
|
||||
"shapes": {
|
||||
"polygon": "Polygon",
|
||||
"circle": "Circle"
|
||||
},
|
||||
"sortOptions": {
|
||||
"descending": "Descending",
|
||||
"ascending": "Ascending",
|
||||
"none": "None"
|
||||
},
|
||||
"orientOptions": {
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal"
|
||||
},
|
||||
"dataEditMode": "Edit Mode",
|
||||
"dataEditForm": "Form",
|
||||
"dataEditJson": "JSON",
|
||||
"jsonPlaceholder": "JSON format data",
|
||||
"jsonTip": "Edit JSON directly, automatically applied on blur",
|
||||
"xAxisData": "X-Axis Data",
|
||||
"seriesDataLabel": "Series Data",
|
||||
"addSeries": "Add Series",
|
||||
"dataItem": "Data Item",
|
||||
"addItem": "Add Data Item",
|
||||
"radarTip": "Radar chart data is complex, JSON mode is recommended",
|
||||
"scatterTip": "Scatter chart data is coordinate points, JSON mode is recommended",
|
||||
"ringTip": "Ring progress data JSON mode is recommended",
|
||||
"heatmapTip": "Heatmap data JSON mode is recommended",
|
||||
"klineTip": "K-line data JSON mode is recommended",
|
||||
"sankeyTip": "Sankey data JSON mode is recommended",
|
||||
"gaugeTip": "Gauge data set 'Current Value' in Basic Config"
|
||||
},
|
||||
"dataSource": {
|
||||
"dataType": "Data Type",
|
||||
"static": "Static Data",
|
||||
"uploadImage": "Image Upload",
|
||||
"uploadVideo": "Video Upload",
|
||||
"dataSource": "Data Source",
|
||||
"api": "API Interface",
|
||||
"config": "Data Source Config",
|
||||
"select": "Select Data Source",
|
||||
"selectPlaceholder": "Please select data source",
|
||||
"mapping": "Field Mapping",
|
||||
"sourceField": "Source Field",
|
||||
"targetField": "Target",
|
||||
"addMapping": "Add Mapping",
|
||||
"mappingTip": "Tip: Data source mapping is configured in 'Data Source Management', additional mappings here override default.",
|
||||
"refresh": "Refresh Config",
|
||||
"autoRefresh": "Auto Refresh",
|
||||
"interval": "Refresh Interval",
|
||||
"intervalUnit": "Unit: seconds, min 5 seconds",
|
||||
"apiConfig": "API Config",
|
||||
"apiUrl": "Request URL",
|
||||
"apiUrlPlaceholder": "e.g. /api/dashboard/stats",
|
||||
"apiMethod": "Request Method",
|
||||
"dataPath": "Data Path",
|
||||
"dataPathPlaceholder": "e.g. data.list",
|
||||
"dataPathTip": "Path to extract data from response",
|
||||
"enableRefresh": "Enable Refresh",
|
||||
"intervalSeconds": "Refresh Interval (s)",
|
||||
"paramBinding": "Param Binding",
|
||||
"paramName": "Data Source Param",
|
||||
"globalParam": "Filter Param",
|
||||
"addBinding": "Add Binding",
|
||||
"paramBindingTip": "Bind filter component parameters to data source parameters for interactive filtering. Select data source parameter on the left, select or enter filter parameter key on the right.",
|
||||
"required": "Required"
|
||||
},
|
||||
"widget": {
|
||||
"todo": {
|
||||
"items": "Todo Items",
|
||||
"add": "Add Todo",
|
||||
"done": "Done",
|
||||
"newItem": "New Todo",
|
||||
"priority": {
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low"
|
||||
}
|
||||
},
|
||||
"notice": {
|
||||
"tip": "Notice list automatically fetched from system messages, no manual configuration needed",
|
||||
"limit": "Display Count"
|
||||
},
|
||||
"announcement": {
|
||||
"tip": "Announcement list automatically fetched from system announcements, no manual configuration needed",
|
||||
"limit": "Display Count"
|
||||
},
|
||||
"ranking": {
|
||||
"items": "Ranking Items",
|
||||
"add": "Add Ranking",
|
||||
"newItem": "New Member"
|
||||
},
|
||||
"quickLinks": {
|
||||
"title": "Quick Links",
|
||||
"tip": "Click grid to select menu, icon automatically uses menu config",
|
||||
"iconColorDefault": "Default Theme Color",
|
||||
"selectMenu": "Select Menu"
|
||||
},
|
||||
"welcome": {
|
||||
"subtitle": "Subtitle",
|
||||
"subtitlePlaceholder": "Please enter subtitle",
|
||||
"showTime": "Show Time"
|
||||
},
|
||||
"countdown": {
|
||||
"config": "Countdown Settings",
|
||||
"targetTime": "Target Time",
|
||||
"targetTimePlaceholder": "Select target time",
|
||||
"showDays": "Show Days",
|
||||
"showHours": "Show Hours",
|
||||
"showMinutes": "Show Minutes",
|
||||
"showSeconds": "Show Seconds",
|
||||
"finishedText": "Finished Text",
|
||||
"finishedTextPlaceholder": "e.g. Finished"
|
||||
},
|
||||
"clock": {
|
||||
"config": "Clock Settings",
|
||||
"showDate": "Show Date",
|
||||
"showSeconds": "Show Seconds",
|
||||
"format24": "24H Format",
|
||||
"timezone": "Timezone",
|
||||
"timezoneOptions": {
|
||||
"local": "Local Time",
|
||||
"utc": "UTC",
|
||||
"beijing": "Beijing Time",
|
||||
"tokyo": "Tokyo Time",
|
||||
"newyork": "New York Time",
|
||||
"london": "London Time"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"config": "Weather Config",
|
||||
"cityName": "City Name",
|
||||
"cityNamePlaceholder": "e.g. Beijing",
|
||||
"presetCity": "Preset City",
|
||||
"presetCityPlaceholder": "Quick select city",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"autoLocate": "Auto Locate",
|
||||
"refreshInterval": "Refresh Interval (min)",
|
||||
"refreshIntervalTip": "Unit: minutes, minimum 5 minutes"
|
||||
},
|
||||
"carousel": {
|
||||
"config": "Carousel Settings",
|
||||
"autoplay": "Autoplay",
|
||||
"interval": "Interval",
|
||||
"intervalUnit": "ms",
|
||||
"showIndicator": "Show Indicator",
|
||||
"showArrow": "Show Arrow",
|
||||
"imageList": "Image List",
|
||||
"addImage": "Add Image",
|
||||
"selectFromGallery": "From Gallery",
|
||||
"selectedImages": "Selected Images",
|
||||
"selectTip": "Please select images from gallery",
|
||||
"newItem": "New Image"
|
||||
},
|
||||
"table": {
|
||||
"config": "Table Settings",
|
||||
"stripe": "Stripe",
|
||||
"border": "Border",
|
||||
"showIndex": "Show Index",
|
||||
"showHeader": "Show Header",
|
||||
"highlightCurrentRow": "Highlight Current Row",
|
||||
"alignment": "Alignment",
|
||||
"headerAlign": "Header Align",
|
||||
"cellAlign": "Cell Align",
|
||||
"heightConfig": "Height Config",
|
||||
"height": "Fixed Height",
|
||||
"heightPlaceholder": "e.g. 300px or 100%",
|
||||
"heightTip": "Supports pixels or percentage",
|
||||
"maxHeight": "Max Height",
|
||||
"maxHeightPlaceholder": "e.g. 500",
|
||||
"maxHeightTip": "Shows scrollbar when exceeded",
|
||||
"customization": "Customization",
|
||||
"emptyText": "Empty Text",
|
||||
"emptyTextPlaceholder": "No data",
|
||||
"headerBgColor": "Header Background",
|
||||
"summaryConfig": "Summary Config",
|
||||
"showSummary": "Show Footer Summary",
|
||||
"summaryType": "Summary Type",
|
||||
"summaryTypes": {
|
||||
"sum": "Sum",
|
||||
"avg": "Average",
|
||||
"count": "Count",
|
||||
"max": "Maximum",
|
||||
"min": "Minimum"
|
||||
},
|
||||
"summaryPrecision": "Decimal Places",
|
||||
"summaryColumns": "Summary Columns",
|
||||
"noColumnsConfigured": "Please configure table columns first",
|
||||
"mergeConfig": "Merge Config",
|
||||
"enableMerge": "Enable Merge",
|
||||
"mergeRules": "Merge Rules",
|
||||
"mergeTypes": {
|
||||
"row": "Row Merge",
|
||||
"column": "Column Merge"
|
||||
},
|
||||
"selectMergeField": "Select merge field",
|
||||
"rowIndex": "Row Index",
|
||||
"startCol": "Start Column",
|
||||
"colspan": "Column Span",
|
||||
"addMergeRule": "Add Merge Rule",
|
||||
"size": "Size",
|
||||
"sizeOptions": {
|
||||
"default": "Default",
|
||||
"large": "Large",
|
||||
"small": "Small"
|
||||
},
|
||||
"columnConfig": "Column Config",
|
||||
"columnConfigJson": "Column Config JSON",
|
||||
"tableData": "Table Data",
|
||||
"tableDataJson": "Table Data JSON",
|
||||
"complexDataTip": "Table data structure is complex, JSON mode is recommended"
|
||||
},
|
||||
"iframe": {
|
||||
"config": "Display Settings",
|
||||
"showBorder": "Show Border",
|
||||
"allowFullscreen": "Allow Fullscreen",
|
||||
"pageAddress": "Page Address"
|
||||
},
|
||||
"video": {
|
||||
"config": "Playback Settings",
|
||||
"autoplay": "Autoplay",
|
||||
"loop": "Loop",
|
||||
"muted": "Muted",
|
||||
"controls": "Native Controls",
|
||||
"videoAddress": "Video Address",
|
||||
"poster": "Poster",
|
||||
"posterPlaceholder": "Poster Image URL (Optional)",
|
||||
"selectFromFileLib": "From File Library"
|
||||
},
|
||||
"image": {
|
||||
"title": "Image",
|
||||
"config": "Image Settings",
|
||||
"alt": "Alt Text",
|
||||
"altPlaceholder": "Alt text when image cannot be displayed",
|
||||
"fit": "Fit Mode",
|
||||
"fitOptions": {
|
||||
"cover": "Cover",
|
||||
"contain": "Contain",
|
||||
"fill": "Fill",
|
||||
"none": "None",
|
||||
"scaleDown": "Scale Down"
|
||||
},
|
||||
"lazy": "Lazy Load",
|
||||
"previewConfig": "Preview Settings",
|
||||
"zIndex": "Z-Index",
|
||||
"closeOnClickModal": "Close on Click Modal",
|
||||
"url": "Image URL",
|
||||
"urlPlaceholder": "Please enter image URL",
|
||||
"link": "Link URL",
|
||||
"previewList": "Preview List",
|
||||
"previewListTip": "List of images to preview when clicked, current image if empty",
|
||||
"addImage": "Add Image",
|
||||
"mainImage": "Main Image"
|
||||
},
|
||||
"common": {
|
||||
"staticDataTip": "Current component uses static data, can be modified in Basic Config"
|
||||
},
|
||||
"formRender": {
|
||||
"formConfig": "Form Config",
|
||||
"formCode": "Select Form",
|
||||
"formCodePlaceholder": "Please select a form",
|
||||
"containerType": "Container Type",
|
||||
"drawer": "Drawer",
|
||||
"dialog": "Dialog",
|
||||
"displayConfig": "Display Config",
|
||||
"showToolbar": "Show Toolbar",
|
||||
"showPagination": "Show Pagination",
|
||||
"pageSize": "Page Size",
|
||||
"buttonConfig": "Button Config",
|
||||
"showAdd": "Add Button",
|
||||
"showView": "View Button",
|
||||
"showEdit": "Edit Button",
|
||||
"showDelete": "Delete Button"
|
||||
}
|
||||
},
|
||||
"fieldLabels": {
|
||||
"title": "Title",
|
||||
"value": "Value",
|
||||
"time": "Time",
|
||||
"trend": "Trend %",
|
||||
"trendLabel": "Trend Desc",
|
||||
"prefix": "Prefix",
|
||||
"suffix": "Suffix",
|
||||
"percentage": "Percentage",
|
||||
"xAxisData": "X-Axis Data",
|
||||
"seriesData": "Series Data",
|
||||
"currentValue": "Current Value",
|
||||
"min": "Min",
|
||||
"max": "Max",
|
||||
"listData": "List Data",
|
||||
"linkData": "Link Data"
|
||||
},
|
||||
"resultType": {
|
||||
"list": "List",
|
||||
"tree": "Tree",
|
||||
"object": "Object",
|
||||
"value": "Value",
|
||||
"chartAxis": "Axis Chart",
|
||||
"chartPie": "Pie Chart",
|
||||
"chartGauge": "Gauge Chart",
|
||||
"chartRadar": "Radar Chart",
|
||||
"chartScatter": "Scatter Chart",
|
||||
"chartHeatmap": "Heatmap"
|
||||
},
|
||||
"borderStyle": {
|
||||
"solid": "Solid",
|
||||
"dashed": "Dashed",
|
||||
"dotted": "Dotted",
|
||||
"none": "None"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
"announcement": {
|
||||
"markAllRead": "Mark All as Read",
|
||||
"top": "Top",
|
||||
"noData": "No Announcements",
|
||||
"loading": "Loading...",
|
||||
"noContent": "No detailed content",
|
||||
"publisher": "Publisher: ",
|
||||
"priority": {
|
||||
"normal": "Normal",
|
||||
"important": "Important",
|
||||
"urgent": "Urgent"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "Just now",
|
||||
"minutesAgo": " minutes ago",
|
||||
"hoursAgo": " hours ago",
|
||||
"daysAgo": " days ago"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"city": "City",
|
||||
"humidity": "Humidity",
|
||||
"wind": "Wind",
|
||||
"loading": "Loading weather data...",
|
||||
"error": "Failed to fetch weather data",
|
||||
"codes": {
|
||||
"clear": "Clear",
|
||||
"mainlyClear": "Mainly Clear",
|
||||
"partlyCloudy": "Partly Cloudy",
|
||||
"overcast": "Overcast",
|
||||
"fog": "Fog",
|
||||
"drizzle": "Drizzle",
|
||||
"rain": "Rain",
|
||||
"snow": "Snow",
|
||||
"showers": "Showers",
|
||||
"snowShowers": "Snow Showers",
|
||||
"thunderstorm": "Thunderstorm"
|
||||
},
|
||||
"windDir": {
|
||||
"n": "N",
|
||||
"ne": "NE",
|
||||
"e": "E",
|
||||
"se": "SE",
|
||||
"s": "S",
|
||||
"sw": "SW",
|
||||
"w": "W",
|
||||
"nw": "NW"
|
||||
}
|
||||
},
|
||||
"chart": {
|
||||
"visits": "Visits"
|
||||
},
|
||||
"iframe": {
|
||||
"refresh": "Refresh",
|
||||
"openNew": "Open in New Tab",
|
||||
"placeholder": "iframe Embedding",
|
||||
"noUrl": "URL not set",
|
||||
"loading": "Loading...",
|
||||
"loadFailed": "Load failed",
|
||||
"retry": "Click to retry"
|
||||
},
|
||||
"todo": {
|
||||
"title": "To-do List",
|
||||
"noData": "No to-dos",
|
||||
"priority": {
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low"
|
||||
}
|
||||
},
|
||||
"dataTable": {
|
||||
"status": {
|
||||
"normal": "Normal",
|
||||
"warning": "Warning",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"processing": "Processing"
|
||||
},
|
||||
"summary": {
|
||||
"sum": "Sum",
|
||||
"avg": "Avg",
|
||||
"count": "Count",
|
||||
"max": "Max",
|
||||
"min": "Min"
|
||||
}
|
||||
},
|
||||
"formRender": {
|
||||
"placeholder": "Form Render Widget",
|
||||
"noFormCode": "Please configure form code",
|
||||
"deleteConfirm": "Are you sure to delete this data?",
|
||||
"batchDeleteConfirm": "Are you sure to delete the selected {count} records?",
|
||||
"importResult": "Successfully imported {success}, failed {failed}",
|
||||
"view": "View"
|
||||
},
|
||||
"welcome": {
|
||||
"greeting": {
|
||||
"night": "Good late night",
|
||||
"morning": "Good morning",
|
||||
"morning2": "Good morning",
|
||||
"noon": "Good noon",
|
||||
"afternoon": "Good afternoon",
|
||||
"evening": "Good evening"
|
||||
}
|
||||
},
|
||||
"countdown": {
|
||||
"day": "d",
|
||||
"hour": "h",
|
||||
"minute": "m",
|
||||
"second": "s",
|
||||
"finished": "Finished"
|
||||
},
|
||||
"video": {
|
||||
"placeholder": "Video Player"
|
||||
},
|
||||
"ranking": {
|
||||
"tenThousand": "W"
|
||||
},
|
||||
"clock": {
|
||||
"weekdays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
"year": "-",
|
||||
"month": "-",
|
||||
"day": ""
|
||||
},
|
||||
"image": {
|
||||
"title": "Image",
|
||||
"noData": "No images"
|
||||
},
|
||||
"approvalCenter": {
|
||||
"initiated": "My Initiated",
|
||||
"pending": "My Pending",
|
||||
"handling": "My Handling",
|
||||
"signing": "My Signing",
|
||||
"handled": "My Handled",
|
||||
"copy": "CC to Me",
|
||||
"start": "Start Process",
|
||||
"more": "More",
|
||||
"config": "Approval Center Config",
|
||||
"routePrefix": "Route Prefix"
|
||||
},
|
||||
"myApps": {
|
||||
"more": "More",
|
||||
"noData": "No apps"
|
||||
},
|
||||
"serverMonitor": {
|
||||
"config": "Server Monitor Config",
|
||||
"cpuBgColor": "CPU Background",
|
||||
"memoryBgColor": "Memory Background",
|
||||
"diskBgColor": "Disk Background",
|
||||
"networkBgColor": "Network Background",
|
||||
"core": " Cores",
|
||||
"thread": " Threads",
|
||||
"memory": "Memory",
|
||||
"disk": "Disk",
|
||||
"network": "Network",
|
||||
"read": "Read",
|
||||
"write": "Write",
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"totalRW": "Total R/W",
|
||||
"uptime": "Uptime",
|
||||
"days": "d ",
|
||||
"hours": "h ",
|
||||
"minutes": "m"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"config": "Filter Config",
|
||||
"paramKey": "Param Key",
|
||||
"paramKeyPlaceholder": "e.g. status",
|
||||
"startParamKey": "Start Date Param",
|
||||
"startParamKeyPlaceholder": "e.g. start_date",
|
||||
"endParamKey": "End Date Param",
|
||||
"endParamKeyPlaceholder": "e.g. end_date",
|
||||
"label": "Label",
|
||||
"labelPlaceholder": "Enter label",
|
||||
"defaultValue": "Default Value",
|
||||
"noParamKey": "Param key not configured",
|
||||
"optionConfig": "Option Config",
|
||||
"optionSource": "Option Source",
|
||||
"staticOptions": "Static Options",
|
||||
"optionLabel": "Label",
|
||||
"optionValue": "Value",
|
||||
"addOption": "Add Option",
|
||||
"optionDataSource": "Option Data Source",
|
||||
"labelField": "Label Field",
|
||||
"valueField": "Value Field",
|
||||
"multiple": "Multiple",
|
||||
"dateFormat": "Date Format",
|
||||
"styleConfig": "Style Config",
|
||||
"labelPosition": "Label Position",
|
||||
"labelPositionLeft": "Left",
|
||||
"labelPositionTop": "Top",
|
||||
"labelPositionHidden": "Hidden",
|
||||
"labelWidth": "Label Width",
|
||||
"labelAlign": "Label Align",
|
||||
"alignLeft": "Left",
|
||||
"alignCenter": "Center",
|
||||
"alignRight": "Right",
|
||||
"componentSize": "Size",
|
||||
"sizeLarge": "Large",
|
||||
"sizeDefault": "Default",
|
||||
"sizeSmall": "Small",
|
||||
"showBorder": "Show Border",
|
||||
"borderRadius": "Border Radius"
|
||||
},
|
||||
"gradient": {
|
||||
"solid": "Solid",
|
||||
"gradient": "Gradient",
|
||||
"startColor": "Start",
|
||||
"endColor": "End",
|
||||
"direction": "Direction",
|
||||
"noColor": "Not set",
|
||||
"presetTitle": "Preset Gradients",
|
||||
"presets": {
|
||||
"warmSunrise": "Warm Sunrise",
|
||||
"oceanBreeze": "Ocean Breeze",
|
||||
"freshMint": "Fresh Mint",
|
||||
"peachGlow": "Peach Glow",
|
||||
"lavenderDream": "Lavender Dream",
|
||||
"skyBlue": "Sky Blue",
|
||||
"roseWater": "Rose Water",
|
||||
"softGrass": "Soft Grass",
|
||||
"winterNymph": "Winter Nymph",
|
||||
"cottonCandy": "Cotton Candy",
|
||||
"sunnyMorning": "Sunny Morning",
|
||||
"crystalClear": "Crystal Clear"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"title": "仪表盘设计",
|
||||
"preview": "预览",
|
||||
"save": "保存",
|
||||
"saveSuccess": "保存成功",
|
||||
"clear": "清空",
|
||||
"clearConfirm": "确定要清空画布吗?此操作不可撤销。",
|
||||
"clearSuccess": "已清空",
|
||||
"export": "导出",
|
||||
"exportSuccess": "导出成功",
|
||||
"import": "导入",
|
||||
"importTitle": "导入配置",
|
||||
"importPlaceholder": "请粘贴 JSON 配置内容...",
|
||||
"importSuccess": "导入成功",
|
||||
"importError": "配置格式错误",
|
||||
"importEmpty": "请输入配置内容",
|
||||
"viewCode": "查看代码",
|
||||
"jsonPreview": "JSON 预览",
|
||||
"copyCode": "复制代码",
|
||||
"copySuccess": "已复制到剪贴板",
|
||||
"copyError": "复制失败",
|
||||
"undoSuccess": "已撤销",
|
||||
"redoSuccess": "已重做",
|
||||
"copyWidgetSuccess": "已复制",
|
||||
"pasteWidgetSuccess": "已粘贴",
|
||||
"deleteWidgetSuccess": "已删除",
|
||||
"noWidgetsTip": "请先添加组件",
|
||||
"noConfigTip": "暂无仪表盘配置",
|
||||
"loadingData": "数据加载中...",
|
||||
"loadDataError": "数据加载失败",
|
||||
"unknownWidget": "未知组件类型",
|
||||
"widgetCount": "{count} 个组件",
|
||||
"dragTip": "从左侧拖拽组件到此处",
|
||||
"add": "添加",
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"reset": "重置",
|
||||
"clean": "清除",
|
||||
"canvas": {
|
||||
"title": "设计画布",
|
||||
"adaptive": "自适应",
|
||||
"actualSize": "实际尺寸",
|
||||
"settings": "画布设置",
|
||||
"undo": "撤销",
|
||||
"redo": "重做"
|
||||
},
|
||||
"months": {
|
||||
"jan": "1月",
|
||||
"feb": "2月",
|
||||
"mar": "3月",
|
||||
"apr": "4月",
|
||||
"may": "5月",
|
||||
"jun": "6月",
|
||||
"jul": "7月",
|
||||
"aug": "8月",
|
||||
"sep": "9月",
|
||||
"oct": "10月",
|
||||
"nov": "11月",
|
||||
"dec": "12月"
|
||||
},
|
||||
"weekdaysShort": {
|
||||
"mon": "周一",
|
||||
"tue": "周二",
|
||||
"wed": "周三",
|
||||
"thu": "周四",
|
||||
"fri": "周五",
|
||||
"sat": "周六",
|
||||
"sun": "周日"
|
||||
},
|
||||
"location": {
|
||||
"east": "华东",
|
||||
"south": "华南",
|
||||
"north": "华北",
|
||||
"central": "华中",
|
||||
"southwest": "西南",
|
||||
"northwest": "西北",
|
||||
"northeast": "东北"
|
||||
},
|
||||
"material": {
|
||||
"title": "组件库",
|
||||
"search": "搜索组件...",
|
||||
"category": {
|
||||
"common": "通用",
|
||||
"chart": "图表",
|
||||
"filter": "筛选器",
|
||||
"map": "地图",
|
||||
"media": "多媒体",
|
||||
"other": "其他"
|
||||
},
|
||||
"widgets": {
|
||||
"statCard": "统计卡片",
|
||||
"progressCard": "进度卡片",
|
||||
"chartLine": "折线图",
|
||||
"chartBar": "柱状图",
|
||||
"chartPie": "饼图",
|
||||
"chartGauge": "仪表盘",
|
||||
"chartArea": "面积图",
|
||||
"chartRadar": "雷达图",
|
||||
"chartFunnel": "漏斗图",
|
||||
"chartScatter": "散点图",
|
||||
"chartRing": "环形进度",
|
||||
"chartHeatmap": "热力图",
|
||||
"chartKline": "K线图",
|
||||
"chartSankey": "桑基图",
|
||||
"todoList": "待办列表",
|
||||
"noticeList": "消息通知",
|
||||
"announcementList": "公告列表",
|
||||
"rankingList": "排行榜",
|
||||
"quickLinks": "快捷入口",
|
||||
"welcomeCard": "欢迎卡片",
|
||||
"calendar": "日历",
|
||||
"countdown": "倒计时",
|
||||
"clock": "时钟",
|
||||
"weather": "天气",
|
||||
"imageCarousel": "图片轮播",
|
||||
"dataTable": "数据表格",
|
||||
"iframe": "iframe 嵌入",
|
||||
"videoPlayer": "视频播放器",
|
||||
"image": "图片",
|
||||
"formRender": "表单渲染",
|
||||
"approvalCenter": "审批中心",
|
||||
"myApps": "我的应用",
|
||||
"serverMonitor": "服务器信息",
|
||||
"filterInput": "输入筛选",
|
||||
"filterSelect": "下拉筛选",
|
||||
"filterDate": "日期筛选",
|
||||
"filterDateRange": "日期范围"
|
||||
},
|
||||
"defaultProps": {
|
||||
"statTitle": "统计数据",
|
||||
"trendLabel": "较昨日",
|
||||
"progressTitle": "完成进度",
|
||||
"visitTrend": "访问趋势",
|
||||
"visits": "访问量",
|
||||
"downloads": "下载量",
|
||||
"salesStat": "销售统计",
|
||||
"trafficSource": "流量来源",
|
||||
"searchEngine": "搜索引擎",
|
||||
"directAccess": "直接访问",
|
||||
"emailMarketing": "邮件营销",
|
||||
"unionAds": "联盟广告",
|
||||
"videoAds": "视频广告",
|
||||
"sysLoad": "系统负载",
|
||||
"abilityEval": "能力评估",
|
||||
"sales": "销售",
|
||||
"mgmt": "管理",
|
||||
"tech": "技术",
|
||||
"cs": "客服",
|
||||
"rd": "研发",
|
||||
"mkt": "市场",
|
||||
"budget": "预算",
|
||||
"actual": "实际",
|
||||
"convFunnel": "转化漏斗",
|
||||
"visit": "访问",
|
||||
"consult": "咨询",
|
||||
"intent": "意向",
|
||||
"order": "下单",
|
||||
"deal": "成交",
|
||||
"dataDist": "数据分布",
|
||||
"height": "身高 (cm)",
|
||||
"weight": "体重 (kg)",
|
||||
"male": "男性",
|
||||
"female": "女性",
|
||||
"kpiDone": "KPI完成度",
|
||||
"salesVolume": "销售额",
|
||||
"orderVolume": "订单量",
|
||||
"customerCount": "客户数",
|
||||
"weekVisitHeat": "周访问热力",
|
||||
"stockTrend": "股价走势",
|
||||
"todoTitle": "待办事项",
|
||||
"todoItem1": "完成项目报告",
|
||||
"todoItem2": "团队周会",
|
||||
"todoItem3": "代码审查",
|
||||
"latestNotice": "消息通知",
|
||||
"latestAnnouncement": "最新公告",
|
||||
"salesRanking": "销售排行",
|
||||
"welcomeTitle": "欢迎回来",
|
||||
"welcomeSubtitle": "今天是个好日子",
|
||||
"countdownTitle": "活动倒计时",
|
||||
"todayWeather": "今日天气",
|
||||
"dataList": "数据列表",
|
||||
"externalPage": "外部页面",
|
||||
"itemName": "项目",
|
||||
"imageTitle": "图片",
|
||||
"myDashboard": "我的仪表盘",
|
||||
"formRender": "表单数据",
|
||||
"approvalCenter": "审批中心",
|
||||
"myApps": "我的应用",
|
||||
"serverMonitor": "服务器信息",
|
||||
"filterInput": "关键词",
|
||||
"filterInputPlaceholder": "请输入关键词筛选...",
|
||||
"filterSelect": "选择筛选",
|
||||
"filterSelectPlaceholder": "请选择...",
|
||||
"filterDate": "日期",
|
||||
"filterDatePlaceholder": "请选择日期",
|
||||
"filterDateRange": "日期范围",
|
||||
"filterStartDate": "开始日期",
|
||||
"filterEndDate": "结束日期"
|
||||
}
|
||||
},
|
||||
"attribute": {
|
||||
"title": "属性面板",
|
||||
"canvasConfig": "画布配置",
|
||||
"widgetConfig": "组件配置",
|
||||
"styleConfig": "样式配置",
|
||||
"dataConfig": "数据配置",
|
||||
"globalSettings": "仪表盘全局设置",
|
||||
"dashboardName": "仪表盘名称",
|
||||
"gridLayout": "网格布局",
|
||||
"columns": "列数",
|
||||
"rowHeight": "行高 (px)",
|
||||
"widgetMargin": "组件间距 (px)",
|
||||
"horizontal": "水平",
|
||||
"vertical": "垂直",
|
||||
"background": "背景",
|
||||
"backgroundColor": "背景颜色",
|
||||
"reset": "重置",
|
||||
"display": "显示",
|
||||
"outerMargin": "四周边距",
|
||||
"outerMarginTip": "渲染时显示四周边距",
|
||||
"widgetTitle": "标题",
|
||||
"layout": "布局",
|
||||
"widthGrid": "宽度 (格)",
|
||||
"heightGrid": "高度 (格)",
|
||||
"layoutTip": "提示:直接在画布上拖拽调整组件大小",
|
||||
"iconConfig": "图标配置",
|
||||
"icon": "图标",
|
||||
"iconColor": "图标颜色",
|
||||
"status": "状态",
|
||||
"strokeWidth": "线条宽度",
|
||||
"showText": "显示文字",
|
||||
"border": "边框",
|
||||
"borderWidth": "边框宽度",
|
||||
"borderColor": "边框颜色",
|
||||
"borderStyleLabel": "边框样式",
|
||||
"borderRadius": "圆角",
|
||||
"shadow": "阴影",
|
||||
"enableShadow": "启用阴影",
|
||||
"shadowColor": "阴影颜色",
|
||||
"shadowBlur": "模糊半径",
|
||||
"chartLayout": "图表布局",
|
||||
"margin": {
|
||||
"left": "左边距 (%)",
|
||||
"right": "右边距 (%)",
|
||||
"top": "上边距 (%)",
|
||||
"bottom": "下边距 (%)"
|
||||
},
|
||||
"tabs": {
|
||||
"basic": "基础",
|
||||
"data": "数据",
|
||||
"style": "样式"
|
||||
},
|
||||
"placeholder": {
|
||||
"name": "请输入名称",
|
||||
"title": "请输入标题",
|
||||
"remark": "请输入描述"
|
||||
},
|
||||
"iconOptions": {
|
||||
"trending": "趋势",
|
||||
"creditCard": "信用卡",
|
||||
"users": "用户",
|
||||
"activity": "活动",
|
||||
"bell": "铃铛",
|
||||
"award": "奖杯"
|
||||
},
|
||||
"progressStatus": {
|
||||
"default": "默认",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"exception": "异常"
|
||||
},
|
||||
"colorTheme": {
|
||||
"label": "颜色主题",
|
||||
"default": "默认",
|
||||
"fresh": "清新",
|
||||
"business": "商务",
|
||||
"tech": "科技",
|
||||
"warm": "暖色"
|
||||
},
|
||||
"chart": {
|
||||
"smooth": "平滑曲线",
|
||||
"showArea": "显示面积",
|
||||
"showSymbol": "显示数据点",
|
||||
"symbolSize": "数据点大小",
|
||||
"lineWidth": "线条宽度",
|
||||
"axis": "坐标轴",
|
||||
"xAxisName": "X轴名称",
|
||||
"yAxisName": "Y轴名称",
|
||||
"nameLocation": "名称位置",
|
||||
"legend": "图例",
|
||||
"showLegend": "显示图例",
|
||||
"legendPosition": "图例位置",
|
||||
"barWidth": "柱子宽度",
|
||||
"barRadius": "圆角大小",
|
||||
"horizontal": "水平方向",
|
||||
"stack": "堆叠显示",
|
||||
"showBackground": "显示背景",
|
||||
"pieType": "图表类型",
|
||||
"showLabel": "显示标签",
|
||||
"labelPosition": "标签位置",
|
||||
"minValue": "最小值",
|
||||
"maxValue": "最大值",
|
||||
"splitNumber": "刻度数量",
|
||||
"showProgress": "显示进度",
|
||||
"showMA5": "显示MA5",
|
||||
"showMA10": "显示MA10",
|
||||
"shape": "形状",
|
||||
"sort": "排序方式",
|
||||
"orient": "方向",
|
||||
"pointSize": "点大小",
|
||||
"numerical": "数值配置",
|
||||
"currentValue": "当前值",
|
||||
"min": "最小值",
|
||||
"max": "最大值",
|
||||
"unit": "单位",
|
||||
"maConfig": "均线配置",
|
||||
"prefix": "前缀",
|
||||
"suffix": "后缀",
|
||||
"trendConfig": "趋势配置",
|
||||
"trendValue": "趋势值 (%)",
|
||||
"trendTip": "正数为上升,负数为下降",
|
||||
"trendLabel": "趋势说明",
|
||||
"seriesNamePrefix": "系列",
|
||||
"itemNamePrefix": "项目",
|
||||
"placeholder": {
|
||||
"xAxis": "如:月份",
|
||||
"yAxis": "如:销量",
|
||||
"unit": "如:%",
|
||||
"title": "请输入标题",
|
||||
"xAxis2": "用逗号分隔,如:1月, 2月, 3月",
|
||||
"seriesData": "用逗号分隔,如:100, 200, 300",
|
||||
"seriesName": "系列名称",
|
||||
"prefix": "如:¥",
|
||||
"suffix": "如:元、%",
|
||||
"trendLabel": "如:较昨日",
|
||||
"subtitle": "请输入副标题"
|
||||
},
|
||||
"location": {
|
||||
"start": "起点",
|
||||
"middle": "中间",
|
||||
"end": "末端",
|
||||
"top": "顶部",
|
||||
"bottom": "底部",
|
||||
"left": "左侧",
|
||||
"right": "右侧"
|
||||
},
|
||||
"pieTypes": {
|
||||
"pie": "饼图",
|
||||
"ring": "环形图",
|
||||
"rose": "玫瑰图"
|
||||
},
|
||||
"labelPositions": {
|
||||
"outside": "外部",
|
||||
"inside": "内部"
|
||||
},
|
||||
"barWidthOptions": {
|
||||
"auto": "自动",
|
||||
"thin": "细 (30%)",
|
||||
"medium": "中 (50%)",
|
||||
"thick": "粗 (70%)"
|
||||
},
|
||||
"shapes": {
|
||||
"polygon": "多边形",
|
||||
"circle": "圆形"
|
||||
},
|
||||
"sortOptions": {
|
||||
"descending": "降序",
|
||||
"ascending": "升序",
|
||||
"none": "不排序"
|
||||
},
|
||||
"orientOptions": {
|
||||
"vertical": "垂直",
|
||||
"horizontal": "水平"
|
||||
},
|
||||
"dataEditMode": "编辑模式",
|
||||
"dataEditForm": "表单",
|
||||
"dataEditJson": "JSON",
|
||||
"jsonPlaceholder": "JSON 格式数据",
|
||||
"jsonTip": "直接编辑 JSON 数据,失去焦点后自动应用",
|
||||
"xAxisData": "X轴数据",
|
||||
"seriesDataLabel": "系列数据",
|
||||
"addSeries": "添加系列",
|
||||
"dataItem": "数据项",
|
||||
"addItem": "添加数据项",
|
||||
"radarTip": "雷达图数据结构较复杂,建议使用 JSON 模式编辑",
|
||||
"scatterTip": "散点图数据为坐标点数组,建议使用 JSON 模式编辑",
|
||||
"ringTip": "环形进度数据建议使用 JSON 模式编辑",
|
||||
"heatmapTip": "热力图数据建议使用 JSON 模式编辑",
|
||||
"klineTip": "K线图数据建议使用 JSON 模式编辑",
|
||||
"sankeyTip": "桑基图数据建议使用 JSON 模式编辑",
|
||||
"gaugeTip": "仪表盘数据在基础配置中设置\"当前值\""
|
||||
},
|
||||
"dataSource": {
|
||||
"dataType": "数据类型",
|
||||
"static": "静态数据",
|
||||
"uploadImage": "图片上传",
|
||||
"uploadVideo": "视频上传",
|
||||
"dataSource": "数据源",
|
||||
"api": "API接口",
|
||||
"config": "数据源配置",
|
||||
"select": "选择数据源",
|
||||
"selectPlaceholder": "请选择数据源",
|
||||
"mapping": "字段映射",
|
||||
"sourceField": "源字段",
|
||||
"targetField": "目标",
|
||||
"addMapping": "添加映射",
|
||||
"mappingTip": "提示:数据源已在「数据源管理」中配置好字段映射,此处可添加额外的字段映射覆盖默认配置。",
|
||||
"refresh": "刷新配置",
|
||||
"autoRefresh": "自动刷新",
|
||||
"interval": "刷新间隔",
|
||||
"intervalUnit": "单位:秒,最小 5 秒",
|
||||
"apiConfig": "接口配置",
|
||||
"apiUrl": "请求地址",
|
||||
"apiUrlPlaceholder": "如:/api/dashboard/stats",
|
||||
"apiMethod": "请求方式",
|
||||
"dataPath": "数据路径",
|
||||
"dataPathPlaceholder": "如:data.list",
|
||||
"dataPathTip": "从响应中提取数据的路径",
|
||||
"enableRefresh": "启用刷新",
|
||||
"intervalSeconds": "刷新间隔 (秒)",
|
||||
"paramBinding": "参数绑定",
|
||||
"paramName": "数据源参数",
|
||||
"globalParam": "筛选器参数",
|
||||
"addBinding": "添加绑定",
|
||||
"paramBindingTip": "将筛选器组件的参数绑定到数据源参数,实现筛选联动。左侧选择数据源中定义的参数,右侧选择或输入筛选器的参数键名。",
|
||||
"required": "必填"
|
||||
},
|
||||
"widget": {
|
||||
"todo": {
|
||||
"items": "待办项",
|
||||
"add": "添加待办",
|
||||
"done": "完成",
|
||||
"newItem": "新待办",
|
||||
"priority": {
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
}
|
||||
},
|
||||
"notice": {
|
||||
"tip": "通知列表自动从系统消息中获取数据,无需手动配置",
|
||||
"limit": "显示数量"
|
||||
},
|
||||
"announcement": {
|
||||
"tip": "公告列表自动从系统公告中获取数据,无需手动配置",
|
||||
"limit": "显示数量"
|
||||
},
|
||||
"ranking": {
|
||||
"items": "排行项",
|
||||
"add": "添加排行",
|
||||
"newItem": "新成员"
|
||||
},
|
||||
"quickLinks": {
|
||||
"title": "快捷入口",
|
||||
"tip": "点击格子选择菜单,图标自动使用菜单配置",
|
||||
"iconColorDefault": "默认主题色",
|
||||
"selectMenu": "选择菜单"
|
||||
},
|
||||
"welcome": {
|
||||
"subtitle": "副标题",
|
||||
"subtitlePlaceholder": "请输入副标题",
|
||||
"showTime": "显示时间"
|
||||
},
|
||||
"countdown": {
|
||||
"config": "倒计时设置",
|
||||
"targetTime": "目标时间",
|
||||
"targetTimePlaceholder": "选择目标时间",
|
||||
"showDays": "显示天数",
|
||||
"showHours": "显示小时",
|
||||
"finishedTextPlaceholder": "已结束"
|
||||
},
|
||||
"clock": {
|
||||
"timezone": "时区"
|
||||
},
|
||||
"weather": {
|
||||
"config": "天气配置",
|
||||
"cityName": "城市名称",
|
||||
"cityNamePlaceholder": "如:北京",
|
||||
"presetCity": "预设城市",
|
||||
"presetCityPlaceholder": "快速选择城市",
|
||||
"latitude": "纬度",
|
||||
"longitude": "经度",
|
||||
"autoLocate": "自动定位",
|
||||
"refreshInterval": "刷新间隔(分钟)",
|
||||
"refreshIntervalTip": "单位:分钟,最小 5 分钟"
|
||||
},
|
||||
"iframe": {
|
||||
"config": "显示设置",
|
||||
"showBorder": "显示边框",
|
||||
"allowFullscreen": "允许全屏",
|
||||
"pageAddress": "页面地址"
|
||||
},
|
||||
"video": {
|
||||
"config": "播放设置",
|
||||
"autoplay": "自动播放",
|
||||
"loop": "循环播放",
|
||||
"muted": "静音",
|
||||
"controls": "原生控制栏",
|
||||
"videoAddress": "视频地址",
|
||||
"poster": "封面图",
|
||||
"posterPlaceholder": "封面图片 URL(可选)",
|
||||
"selectFromFileLib": "从文件库选择"
|
||||
},
|
||||
"image": {
|
||||
"title": "图片",
|
||||
"config": "图片设置",
|
||||
"alt": "替代文本",
|
||||
"altPlaceholder": "图片无法显示时的替代文本",
|
||||
"fit": "填充方式",
|
||||
"fitOptions": {
|
||||
"cover": "覆盖 (cover)",
|
||||
"contain": "包含 (contain)",
|
||||
"fill": "填充 (fill)",
|
||||
"none": "无 (none)",
|
||||
"scaleDown": "缩小 (scale-down)"
|
||||
},
|
||||
"lazy": "懒加载",
|
||||
"previewConfig": "预览设置",
|
||||
"zIndex": "预览层级",
|
||||
"closeOnClickModal": "点击遮罩关闭",
|
||||
"url": "图片 URL",
|
||||
"urlPlaceholder": "请输入图片URL",
|
||||
"link": "跳转链接(可选)",
|
||||
"previewList": "预览图片列表",
|
||||
"previewListTip": "点击图片时可预览的图片列表,为空时预览当前图片",
|
||||
"addImage": "添加图片",
|
||||
"mainImage": "主图"
|
||||
},
|
||||
"common": {
|
||||
"staticDataTip": "当前组件使用静态数据,可在基础配置中修改"
|
||||
},
|
||||
"formRender": {
|
||||
"formConfig": "表单配置",
|
||||
"formCode": "选择表单",
|
||||
"formCodePlaceholder": "请选择表单",
|
||||
"containerType": "容器类型",
|
||||
"drawer": "抽屉",
|
||||
"dialog": "弹窗",
|
||||
"displayConfig": "显示配置",
|
||||
"showToolbar": "显示工具栏",
|
||||
"showPagination": "显示分页",
|
||||
"pageSize": "每页条数",
|
||||
"buttonConfig": "按钮配置",
|
||||
"showAdd": "新增按钮",
|
||||
"showView": "查看按钮",
|
||||
"showEdit": "编辑按钮",
|
||||
"showDelete": "删除按钮"
|
||||
},
|
||||
"table": {
|
||||
"config": "表格配置",
|
||||
"stripe": "斑马纹",
|
||||
"border": "边框",
|
||||
"showIndex": "显示序号",
|
||||
"showHeader": "显示表头",
|
||||
"highlightCurrentRow": "高亮当前行",
|
||||
"alignment": "对齐方式",
|
||||
"headerAlign": "表头对齐",
|
||||
"cellAlign": "单元格对齐",
|
||||
"heightConfig": "高度配置",
|
||||
"height": "固定高度",
|
||||
"heightPlaceholder": "如:300px 或 100%",
|
||||
"heightTip": "支持像素值或百分比",
|
||||
"maxHeight": "最大高度",
|
||||
"maxHeightPlaceholder": "如:500",
|
||||
"maxHeightTip": "超出后显示滚动条",
|
||||
"customization": "自定义",
|
||||
"emptyText": "空数据提示",
|
||||
"emptyTextPlaceholder": "暂无数据",
|
||||
"headerBgColor": "表头背景色",
|
||||
"summaryConfig": "统计配置",
|
||||
"showSummary": "显示表尾统计",
|
||||
"summaryType": "统计类型",
|
||||
"summaryTypes": {
|
||||
"sum": "合计",
|
||||
"avg": "平均值",
|
||||
"count": "计数",
|
||||
"max": "最大值",
|
||||
"min": "最小值"
|
||||
},
|
||||
"summaryPrecision": "小数位数",
|
||||
"summaryColumns": "统计列",
|
||||
"noColumnsConfigured": "请先配置表格列",
|
||||
"mergeConfig": "合并配置",
|
||||
"enableMerge": "启用合并",
|
||||
"mergeRules": "合并规则",
|
||||
"mergeTypes": {
|
||||
"row": "行合并",
|
||||
"column": "列合并"
|
||||
},
|
||||
"selectMergeField": "选择合并字段",
|
||||
"rowIndex": "行号",
|
||||
"startCol": "起始列",
|
||||
"colspan": "合并列数",
|
||||
"addMergeRule": "添加合并规则",
|
||||
"columnConfig": "列配置",
|
||||
"columnConfigJson": "JSON 格式列配置",
|
||||
"tableData": "表格数据",
|
||||
"tableDataJson": "JSON 格式表格数据",
|
||||
"complexDataTip": "表格数据结构较复杂,建议使用 JSON 模式编辑",
|
||||
"sizeOptions": {
|
||||
"default": "默认",
|
||||
"large": "大",
|
||||
"small": "小"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fieldLabels": {
|
||||
"title": "标题",
|
||||
"value": "数值",
|
||||
"time": "时间",
|
||||
"trend": "趋势百分比",
|
||||
"trendLabel": "趋势说明",
|
||||
"prefix": "前缀",
|
||||
"suffix": "后缀",
|
||||
"percentage": "百分比",
|
||||
"xAxisData": "X轴数据",
|
||||
"seriesData": "系列数据",
|
||||
"currentValue": "当前值",
|
||||
"min": "最小值",
|
||||
"max": "最大值",
|
||||
"listData": "列表数据",
|
||||
"linkData": "链接数据"
|
||||
},
|
||||
"resultType": {
|
||||
"list": "列表",
|
||||
"tree": "树形",
|
||||
"object": "对象",
|
||||
"value": "单值",
|
||||
"chartAxis": "轴向图表",
|
||||
"chartPie": "饼图",
|
||||
"chartGauge": "仪表盘",
|
||||
"chartRadar": "雷达图",
|
||||
"chartScatter": "散点图",
|
||||
"chartHeatmap": "热力图"
|
||||
},
|
||||
"borderStyle": {
|
||||
"solid": "实线",
|
||||
"dashed": "虚线",
|
||||
"dotted": "点线",
|
||||
"none": "无"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
"announcement": {
|
||||
"markAllRead": "全部已读",
|
||||
"top": "置顶",
|
||||
"noData": "暂无公告",
|
||||
"loading": "加载中...",
|
||||
"noContent": "暂无详细内容",
|
||||
"publisher": "发布者:",
|
||||
"priority": {
|
||||
"normal": "普通",
|
||||
"important": "重要",
|
||||
"urgent": "紧急"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "分钟前",
|
||||
"hoursAgo": "小时前",
|
||||
"daysAgo": "天前"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"city": "城市",
|
||||
"humidity": "湿度",
|
||||
"wind": "风力",
|
||||
"loading": "加载天气数据中...",
|
||||
"error": "获取天气数据失败",
|
||||
"codes": {
|
||||
"clear": "晴",
|
||||
"mainlyClear": "晴间多云",
|
||||
"partlyCloudy": "多云",
|
||||
"overcast": "阴",
|
||||
"fog": "雾",
|
||||
"drizzle": "毛毛雨",
|
||||
"rain": "雨",
|
||||
"snow": "雪",
|
||||
"showers": "阵雨",
|
||||
"snowShowers": "阵雪",
|
||||
"thunderstorm": "雷暴"
|
||||
},
|
||||
"windDir": {
|
||||
"n": "北风",
|
||||
"ne": "东北风",
|
||||
"e": "东风",
|
||||
"se": "东南风",
|
||||
"s": "南风",
|
||||
"sw": "西南风",
|
||||
"w": "西风",
|
||||
"nw": "西北风"
|
||||
}
|
||||
},
|
||||
"chart": {
|
||||
"visits": "访问量"
|
||||
},
|
||||
"iframe": {
|
||||
"refresh": "刷新",
|
||||
"openNew": "新窗口打开",
|
||||
"placeholder": "iframe 嵌入",
|
||||
"noUrl": "未设置 URL",
|
||||
"loading": "加载中...",
|
||||
"loadFailed": "加载失败",
|
||||
"retry": "点击重试"
|
||||
},
|
||||
"todo": {
|
||||
"title": "待办事项",
|
||||
"noData": "暂无待办",
|
||||
"priority": {
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
}
|
||||
},
|
||||
"dataTable": {
|
||||
"status": {
|
||||
"normal": "正常",
|
||||
"warning": "警告",
|
||||
"error": "异常",
|
||||
"success": "成功",
|
||||
"failed": "失败",
|
||||
"processing": "进行中"
|
||||
},
|
||||
"summary": {
|
||||
"sum": "合计",
|
||||
"avg": "平均",
|
||||
"count": "计数",
|
||||
"max": "最大",
|
||||
"min": "最小"
|
||||
}
|
||||
},
|
||||
"formRender": {
|
||||
"placeholder": "表单渲染组件",
|
||||
"noFormCode": "请配置表单编码",
|
||||
"deleteConfirm": "确定要删除这条数据吗?",
|
||||
"batchDeleteConfirm": "确定要删除选中的 {count} 条数据吗?",
|
||||
"importResult": "成功导入 {success} 条,失败 {failed} 条",
|
||||
"view": "查看"
|
||||
},
|
||||
"welcome": {
|
||||
"greeting": {
|
||||
"night": "夜深了",
|
||||
"morning": "早上好",
|
||||
"morning2": "上午好",
|
||||
"noon": "中午好",
|
||||
"afternoon": "下午好",
|
||||
"evening": "晚上好"
|
||||
}
|
||||
},
|
||||
"countdown": {
|
||||
"day": "天",
|
||||
"hour": "时",
|
||||
"minute": "分",
|
||||
"second": "秒",
|
||||
"finished": "已结束"
|
||||
},
|
||||
"video": {
|
||||
"placeholder": "视频播放器"
|
||||
},
|
||||
"ranking": {
|
||||
"tenThousand": "万"
|
||||
},
|
||||
"clock": {
|
||||
"weekdays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
"year": "年",
|
||||
"month": "月",
|
||||
"day": "日"
|
||||
},
|
||||
"image": {
|
||||
"title": "图片",
|
||||
"noData": "暂无图片"
|
||||
},
|
||||
"approvalCenter": {
|
||||
"initiated": "我发起的",
|
||||
"pending": "我的待办",
|
||||
"handling": "我的在办",
|
||||
"signing": "我的待签",
|
||||
"handled": "我的已办",
|
||||
"copy": "抄送我的",
|
||||
"start": "发起流程",
|
||||
"more": "更多",
|
||||
"config": "审批中心配置",
|
||||
"routePrefix": "路由前缀"
|
||||
},
|
||||
"myApps": {
|
||||
"more": "更多",
|
||||
"noData": "暂无应用"
|
||||
},
|
||||
"serverMonitor": {
|
||||
"config": "服务器信息配置",
|
||||
"cpuBgColor": "CPU背景色",
|
||||
"memoryBgColor": "内存背景色",
|
||||
"diskBgColor": "磁盘背景色",
|
||||
"networkBgColor": "网络背景色",
|
||||
"core": "核",
|
||||
"thread": "线程",
|
||||
"memory": "内存",
|
||||
"disk": "磁盘",
|
||||
"network": "网络",
|
||||
"read": "读取",
|
||||
"write": "写入",
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"totalRW": "累计读/写",
|
||||
"uptime": "运行时间",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"config": "筛选器配置",
|
||||
"paramKey": "参数键名",
|
||||
"paramKeyPlaceholder": "如:status",
|
||||
"startParamKey": "开始日期参数",
|
||||
"startParamKeyPlaceholder": "如:start_date",
|
||||
"endParamKey": "结束日期参数",
|
||||
"endParamKeyPlaceholder": "如:end_date",
|
||||
"label": "标签",
|
||||
"labelPlaceholder": "请输入标签",
|
||||
"defaultValue": "默认值",
|
||||
"noParamKey": "未配置参数键名",
|
||||
"optionConfig": "选项配置",
|
||||
"optionSource": "选项来源",
|
||||
"staticOptions": "静态选项",
|
||||
"optionLabel": "标签",
|
||||
"optionValue": "值",
|
||||
"addOption": "添加选项",
|
||||
"optionDataSource": "选项数据源",
|
||||
"labelField": "标签字段",
|
||||
"valueField": "值字段",
|
||||
"multiple": "多选",
|
||||
"dateFormat": "日期格式",
|
||||
"styleConfig": "样式配置",
|
||||
"labelPosition": "标签位置",
|
||||
"labelPositionLeft": "左侧",
|
||||
"labelPositionTop": "上方",
|
||||
"labelPositionHidden": "隐藏",
|
||||
"labelWidth": "标签宽度",
|
||||
"labelAlign": "标签对齐",
|
||||
"alignLeft": "左对齐",
|
||||
"alignCenter": "居中",
|
||||
"alignRight": "右对齐",
|
||||
"componentSize": "组件尺寸",
|
||||
"sizeLarge": "大",
|
||||
"sizeDefault": "中",
|
||||
"sizeSmall": "小",
|
||||
"showBorder": "显示边框",
|
||||
"borderRadius": "圆角"
|
||||
},
|
||||
"gradient": {
|
||||
"solid": "纯色",
|
||||
"gradient": "渐变",
|
||||
"startColor": "起始色",
|
||||
"endColor": "结束色",
|
||||
"direction": "方向",
|
||||
"noColor": "未设置",
|
||||
"presetTitle": "预设渐变",
|
||||
"presets": {
|
||||
"warmSunrise": "暖阳晨曦",
|
||||
"oceanBreeze": "海洋微风",
|
||||
"freshMint": "清新薄荷",
|
||||
"peachGlow": "蜜桃光晕",
|
||||
"lavenderDream": "薰衣草梦",
|
||||
"skyBlue": "天空蔚蓝",
|
||||
"roseWater": "玫瑰花水",
|
||||
"softGrass": "柔和青草",
|
||||
"winterNymph": "冬日仙子",
|
||||
"cottonCandy": "棉花糖",
|
||||
"sunnyMorning": "阳光早晨",
|
||||
"crystalClear": "水晶透明"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,894 @@
|
||||
{
|
||||
"title": "儀表板設計",
|
||||
"preview": "預覽",
|
||||
"save": "保存",
|
||||
"saveSuccess": "保存成功",
|
||||
"clear": "清空",
|
||||
"clearConfirm": "確定要清空畫布嗎?此操作不可撤銷。",
|
||||
"clearSuccess": "已清空",
|
||||
"export": "導出",
|
||||
"exportSuccess": "導出成功",
|
||||
"import": "導入",
|
||||
"importTitle": "導入配置",
|
||||
"importPlaceholder": "請粘貼 JSON 配置內容...",
|
||||
"importSuccess": "導入成功",
|
||||
"importError": "配置格式錯誤",
|
||||
"importEmpty": "請輸入配置內容",
|
||||
"viewCode": "查看代碼",
|
||||
"jsonPreview": "JSON 預覽",
|
||||
"copyCode": "複製代碼",
|
||||
"copySuccess": "已複製到剪貼板",
|
||||
"copyError": "複製失敗",
|
||||
"undoSuccess": "已撤銷",
|
||||
"redoSuccess": "已重做",
|
||||
"copyWidgetSuccess": "已複製",
|
||||
"pasteWidgetSuccess": "已粘貼",
|
||||
"deleteWidgetSuccess": "已刪除",
|
||||
"noWidgetsTip": "請先添加組件",
|
||||
"noConfigTip": "暫無儀表板配置",
|
||||
"loadingData": "數據加載中...",
|
||||
"loadDataError": "數據加載失敗",
|
||||
"unknownWidget": "未知組件類型",
|
||||
"widgetCount": "{count} 個組件",
|
||||
"dragTip": "從左側拖拽組件到此處",
|
||||
"add": "添加",
|
||||
"copy": "複製",
|
||||
"delete": "刪除",
|
||||
"reset": "重置",
|
||||
"clean": "清除",
|
||||
"canvas": {
|
||||
"title": "設計畫布",
|
||||
"adaptive": "自適應",
|
||||
"actualSize": "實際尺寸",
|
||||
"settings": "畫布設置",
|
||||
"undo": "撤銷",
|
||||
"redo": "重做"
|
||||
},
|
||||
"months": {
|
||||
"jan": "1月",
|
||||
"feb": "2月",
|
||||
"mar": "3月",
|
||||
"apr": "4月",
|
||||
"may": "5月",
|
||||
"jun": "6月",
|
||||
"jul": "7月",
|
||||
"aug": "8月",
|
||||
"sep": "9月",
|
||||
"oct": "10月",
|
||||
"nov": "11月",
|
||||
"dec": "12月"
|
||||
},
|
||||
"weekdaysShort": {
|
||||
"mon": "週一",
|
||||
"tue": "週二",
|
||||
"wed": "週三",
|
||||
"thu": "週四",
|
||||
"fri": "週五",
|
||||
"sat": "週六",
|
||||
"sun": "週日"
|
||||
},
|
||||
"material": {
|
||||
"title": "組件庫",
|
||||
"search": "搜索組件...",
|
||||
"category": {
|
||||
"common": "通用",
|
||||
"chart": "圖表",
|
||||
"filter": "篩選器",
|
||||
"map": "地圖",
|
||||
"media": "多媒體",
|
||||
"other": "其他"
|
||||
},
|
||||
"widgets": {
|
||||
"statCard": "統計卡片",
|
||||
"progressCard": "進度卡片",
|
||||
"chartLine": "折線圖",
|
||||
"chartBar": "柱狀圖",
|
||||
"chartPie": "餅圖",
|
||||
"chartGauge": "儀表板",
|
||||
"chartArea": "面積圖",
|
||||
"chartRadar": "雷達圖",
|
||||
"chartFunnel": "漏斗圖",
|
||||
"chartScatter": "散點圖",
|
||||
"chartRing": "環形進度",
|
||||
"chartHeatmap": "熱力圖",
|
||||
"chartKline": "K線圖",
|
||||
"chartSankey": "桑基圖",
|
||||
"todoList": "待辦列表",
|
||||
"noticeList": "消息通知",
|
||||
"announcementList": "公告列表",
|
||||
"rankingList": "排行榜",
|
||||
"quickLinks": "快捷入口",
|
||||
"welcomeCard": "歡迎卡片",
|
||||
"calendar": "日曆",
|
||||
"countdown": "倒計時",
|
||||
"clock": "時鐘",
|
||||
"weather": "天氣",
|
||||
"imageCarousel": "圖片輪播",
|
||||
"dataTable": "數據表格",
|
||||
"iframe": "iframe 嵌入",
|
||||
"videoPlayer": "視頻播放器",
|
||||
"image": "圖片",
|
||||
"formRender": "表單渲染",
|
||||
"approvalCenter": "審批中心",
|
||||
"myApps": "我的應用",
|
||||
"serverMonitor": "服務器信息",
|
||||
"filterInput": "輸入篩選",
|
||||
"filterSelect": "下拉篩選",
|
||||
"filterDate": "日期篩選",
|
||||
"filterDateRange": "日期範圍"
|
||||
},
|
||||
"defaultProps": {
|
||||
"statTitle": "統計數據",
|
||||
"trendLabel": "較昨日",
|
||||
"progressTitle": "完成進度",
|
||||
"visitTrend": "訪問趨勢",
|
||||
"visits": "訪問量",
|
||||
"downloads": "下載量",
|
||||
"salesStat": "銷售統計",
|
||||
"trafficSource": "流量來源",
|
||||
"searchEngine": "搜索引擎",
|
||||
"directAccess": "直接訪問",
|
||||
"emailMarketing": "郵件營銷",
|
||||
"unionAds": "聯盟廣告",
|
||||
"videoAds": "視頻廣告",
|
||||
"sysLoad": "系統負載",
|
||||
"abilityEval": "能力評估",
|
||||
"sales": "銷售",
|
||||
"mgmt": "管理",
|
||||
"tech": "技術",
|
||||
"cs": "客服",
|
||||
"rd": "研發",
|
||||
"mkt": "市場",
|
||||
"budget": "預算",
|
||||
"actual": "實際",
|
||||
"convFunnel": "轉化漏斗",
|
||||
"visit": "訪問",
|
||||
"consult": "諮詢",
|
||||
"intent": "意向",
|
||||
"order": "下單",
|
||||
"deal": "成交",
|
||||
"dataDist": "數據分佈",
|
||||
"height": "身高 (cm)",
|
||||
"weight": "體重 (kg)",
|
||||
"male": "男性",
|
||||
"female": "女性",
|
||||
"kpiDone": "KPI完成度",
|
||||
"salesVolume": "銷售額",
|
||||
"orderVolume": "訂單量",
|
||||
"customerCount": "客戶數",
|
||||
"weekVisitHeat": "周訪問熱力",
|
||||
"stockTrend": "股價走勢",
|
||||
"todoTitle": "待辦事項",
|
||||
"todoItem1": "完成項目報告",
|
||||
"todoItem2": "團隊週會",
|
||||
"todoItem3": "代碼審查",
|
||||
"latestNotice": "消息通知",
|
||||
"latestAnnouncement": "最新公告",
|
||||
"salesRanking": "銷售排行",
|
||||
"welcomeTitle": "歡迎回來",
|
||||
"welcomeSubtitle": "今天個好日子",
|
||||
"countdownTitle": "活動倒計時",
|
||||
"todayWeather": "今日天氣",
|
||||
"dataList": "數據列表",
|
||||
"externalPage": "外部頁面",
|
||||
"itemName": "項目",
|
||||
"imageTitle": "圖片",
|
||||
"myDashboard": "我的儀表盤",
|
||||
"formRender": "表單數據",
|
||||
"approvalCenter": "審批中心",
|
||||
"myApps": "我的應用",
|
||||
"serverMonitor": "服務器信息",
|
||||
"filterInput": "關鍵詞",
|
||||
"filterInputPlaceholder": "請輸入關鍵詞篩選...",
|
||||
"filterSelect": "選擇篩選",
|
||||
"filterSelectPlaceholder": "請選擇...",
|
||||
"filterDate": "日期",
|
||||
"filterDatePlaceholder": "請選擇日期",
|
||||
"filterDateRange": "日期範圍",
|
||||
"filterStartDate": "開始日期",
|
||||
"filterEndDate": "結束日期"
|
||||
}
|
||||
},
|
||||
"attribute": {
|
||||
"title": "屬性面板",
|
||||
"canvasConfig": "畫布配置",
|
||||
"widgetConfig": "組件配置",
|
||||
"styleConfig": "樣式配置",
|
||||
"dataConfig": "數據配置",
|
||||
"globalSettings": "儀表板全局設置",
|
||||
"dashboardName": "儀表板名稱",
|
||||
"gridLayout": "網格佈局",
|
||||
"columns": "列數",
|
||||
"rowHeight": "行高 (px)",
|
||||
"widgetMargin": "組件間距 (px)",
|
||||
"horizontal": "水平",
|
||||
"vertical": "垂直",
|
||||
"background": "背景",
|
||||
"backgroundColor": "背景顏色",
|
||||
"reset": "重置",
|
||||
"display": "顯示",
|
||||
"outerMargin": "四週邊距",
|
||||
"outerMarginTip": "渲染時顯示四週邊距",
|
||||
"widgetTitle": "標題",
|
||||
"layout": "佈局",
|
||||
"widthGrid": "寬度 (格)",
|
||||
"heightGrid": "高度 (格)",
|
||||
"layoutTip": "提示:直接在畫布上拖拽調整組件大小",
|
||||
"iconConfig": "圖標配置",
|
||||
"icon": "圖標",
|
||||
"iconColor": "圖標顏色",
|
||||
"status": "狀態",
|
||||
"strokeWidth": "線條寬度",
|
||||
"showText": "顯示文字",
|
||||
"border": "邊框",
|
||||
"borderWidth": "邊框寬度",
|
||||
"borderColor": "邊框顏色",
|
||||
"borderStyleLabel": "邊框樣式",
|
||||
"borderRadius": "圓角",
|
||||
"shadow": "陰影",
|
||||
"enableShadow": "啟用陰影",
|
||||
"shadowColor": "陰影顏色",
|
||||
"shadowBlur": "模糊半徑",
|
||||
"chartLayout": "圖表佈局",
|
||||
"margin": {
|
||||
"left": "左邊距 (%)",
|
||||
"right": "右邊距 (%)",
|
||||
"top": "上邊距 (%)",
|
||||
"bottom": "下邊距 (%)"
|
||||
},
|
||||
"tabs": {
|
||||
"basic": "基礎",
|
||||
"data": "數據",
|
||||
"style": "樣式"
|
||||
},
|
||||
"placeholder": {
|
||||
"name": "請輸入名稱",
|
||||
"title": "請輸入標題",
|
||||
"remark": "請輸入描述"
|
||||
},
|
||||
"iconOptions": {
|
||||
"trending": "趨勢",
|
||||
"creditCard": "信用卡",
|
||||
"users": "用戶",
|
||||
"activity": "活動",
|
||||
"bell": "鈴鐺",
|
||||
"award": "獎杯"
|
||||
},
|
||||
"progressStatus": {
|
||||
"default": "默認",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"exception": "異常"
|
||||
},
|
||||
"colorTheme": {
|
||||
"label": "顏色主題",
|
||||
"default": "默認",
|
||||
"fresh": "清新",
|
||||
"business": "商務",
|
||||
"tech": "科技",
|
||||
"warm": "暖色"
|
||||
},
|
||||
"chart": {
|
||||
"smooth": "平滑曲線",
|
||||
"showArea": "顯示面積",
|
||||
"showSymbol": "顯示數據點",
|
||||
"symbolSize": "數據點大小",
|
||||
"lineWidth": "線條寬度",
|
||||
"axis": "座標軸",
|
||||
"xAxisName": "X軸名稱",
|
||||
"yAxisName": "Y軸名稱",
|
||||
"nameLocation": "名稱位置",
|
||||
"legend": "圖例",
|
||||
"showLegend": "顯示圖例",
|
||||
"legendPosition": "圖例位置",
|
||||
"barWidth": "柱子寬度",
|
||||
"barRadius": "圓角大小",
|
||||
"horizontal": "水平方向",
|
||||
"stack": "堆疊顯示",
|
||||
"showBackground": "顯示背景",
|
||||
"pieType": "圖表類型",
|
||||
"showLabel": "顯示標籤",
|
||||
"labelPosition": "標籤位置",
|
||||
"minValue": "最小值",
|
||||
"maxValue": "最大值",
|
||||
"splitNumber": "刻度數量",
|
||||
"showProgress": "顯示進度",
|
||||
"showMA5": "顯示MA5",
|
||||
"showMA10": "顯示MA10",
|
||||
"shape": "形狀",
|
||||
"sort": "排序方式",
|
||||
"orient": "方向",
|
||||
"pointSize": "點大小",
|
||||
"numerical": "數值配置",
|
||||
"currentValue": "當前值",
|
||||
"min": "最小值",
|
||||
"max": "最大值",
|
||||
"unit": "單位",
|
||||
"maConfig": "均線配置",
|
||||
"prefix": "前綴",
|
||||
"suffix": "後綴",
|
||||
"trendConfig": "趨勢配置",
|
||||
"trendValue": "趨勢值 (%)",
|
||||
"trendTip": "正數為上升,負數為下降",
|
||||
"trendLabel": "趨勢說明",
|
||||
"seriesNamePrefix": "系列",
|
||||
"itemNamePrefix": "項目",
|
||||
"placeholder": {
|
||||
"xAxis": "如:月份",
|
||||
"yAxis": "如:銷量",
|
||||
"unit": "如:%",
|
||||
"title": "請輸入標題",
|
||||
"xAxis2": "用逗號分隔,如:1月, 2月, 3月",
|
||||
"seriesData": "用逗號分隔,如:100, 200, 300",
|
||||
"seriesName": "系列名稱",
|
||||
"prefix": "如:¥",
|
||||
"suffix": "如:元、%",
|
||||
"trendLabel": "如:較昨日",
|
||||
"subtitle": "請輸入副標題"
|
||||
},
|
||||
"location": {
|
||||
"start": "起點",
|
||||
"middle": "中間",
|
||||
"end": "末端",
|
||||
"top": "頂部",
|
||||
"bottom": "底部",
|
||||
"left": "左側",
|
||||
"right": "右側"
|
||||
},
|
||||
"pieTypes": {
|
||||
"pie": "餅圖",
|
||||
"ring": "環形圖",
|
||||
"rose": "玫瑰圖"
|
||||
},
|
||||
"labelPositions": {
|
||||
"outside": "外部",
|
||||
"inside": "內部"
|
||||
},
|
||||
"barWidthOptions": {
|
||||
"auto": "自動",
|
||||
"thin": "細 (30%)",
|
||||
"medium": "中 (50%)",
|
||||
"thick": "粗 (70%)"
|
||||
},
|
||||
"shapes": {
|
||||
"polygon": "多邊形",
|
||||
"circle": "圓形"
|
||||
},
|
||||
"sortOptions": {
|
||||
"descending": "降序",
|
||||
"ascending": "升序",
|
||||
"none": "不排序"
|
||||
},
|
||||
"orientOptions": {
|
||||
"vertical": "垂直",
|
||||
"horizontal": "水平"
|
||||
},
|
||||
"dataEditMode": "編輯模式",
|
||||
"dataEditForm": "表單",
|
||||
"dataEditJson": "JSON",
|
||||
"jsonPlaceholder": "JSON 格式數據",
|
||||
"jsonTip": "直接編輯 JSON 數據,失去焦點後自動應用",
|
||||
"xAxisData": "X軸數據",
|
||||
"seriesDataLabel": "系列數據",
|
||||
"addSeries": "添加系列",
|
||||
"dataItem": "數據項",
|
||||
"addItem": "添加數據項",
|
||||
"radarTip": "雷達圖數據結構較複雜,建議使用 JSON 模式編輯",
|
||||
"scatterTip": "散點圖數據為座標點數組,建議使用 JSON 模式編輯",
|
||||
"ringTip": "環形進度數據建議使用 JSON 模式編輯",
|
||||
"heatmapTip": "熱力圖數據建議使用 JSON 模式編輯",
|
||||
"klineTip": "K線圖數據建議使用 JSON 模式編輯",
|
||||
"sankeyTip": "桑基圖數據建議使用 JSON 模式編輯",
|
||||
"gaugeTip": "儀表盤數據在基礎配置中設置\"當前值\""
|
||||
},
|
||||
"dataSource": {
|
||||
"dataType": "數據類型",
|
||||
"static": "靜態數據",
|
||||
"uploadImage": "圖片上傳",
|
||||
"uploadVideo": "視頻上傳",
|
||||
"dataSource": "數據源",
|
||||
"api": "API接口",
|
||||
"config": "數據源配置",
|
||||
"select": "選擇數據源",
|
||||
"selectPlaceholder": "請選擇數據源",
|
||||
"mapping": "字段映射",
|
||||
"sourceField": "源字段",
|
||||
"targetField": "目標",
|
||||
"addMapping": "添加映射",
|
||||
"mappingTip": "提示:數據源已在「數據源管理」中配置好字段映射,此處可添加額外的字段映射覆蓋默認配置。",
|
||||
"refresh": "刷新配置",
|
||||
"autoRefresh": "自動刷新",
|
||||
"interval": "刷新間隔",
|
||||
"intervalUnit": "單位:秒,最小 5 秒",
|
||||
"apiConfig": "接口配置",
|
||||
"apiUrl": "請求地址",
|
||||
"apiUrlPlaceholder": "如:/api/dashboard/stats",
|
||||
"apiMethod": "請求方式",
|
||||
"dataPath": "數據路徑",
|
||||
"dataPathPlaceholder": "如:data.list",
|
||||
"dataPathTip": "從響應中提取數據的路徑",
|
||||
"enableRefresh": "啟用刷新",
|
||||
"intervalSeconds": "刷新間隔 (秒)",
|
||||
"paramBinding": "參數綁定",
|
||||
"paramName": "數據源參數",
|
||||
"globalParam": "篩選器參數",
|
||||
"addBinding": "添加綁定",
|
||||
"paramBindingTip": "將篩選器組件的參數綁定到數據源參數,實現篩選聯動。左側選擇數據源中定義的參數,右側選擇或輸入篩選器的參數鍵名。",
|
||||
"required": "必填"
|
||||
},
|
||||
"widget": {
|
||||
"todo": {
|
||||
"items": "待辦項",
|
||||
"add": "添加待辦",
|
||||
"done": "完成",
|
||||
"newItem": "新待辦",
|
||||
"priority": {
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
}
|
||||
},
|
||||
"notice": {
|
||||
"tip": "通知列表自動從系統消息中獲取數據,無需手動配置",
|
||||
"limit": "顯示數量"
|
||||
},
|
||||
"announcement": {
|
||||
"tip": "公告列表自動從系統公告中獲取數據,無需手動配置",
|
||||
"limit": "顯示數量"
|
||||
},
|
||||
"ranking": {
|
||||
"items": "排行項",
|
||||
"add": "添加排行",
|
||||
"newItem": "新成員"
|
||||
},
|
||||
"quickLinks": {
|
||||
"title": "快捷入口",
|
||||
"tip": "點擊格子選擇菜單,圖標自動使用菜單配置",
|
||||
"iconColorDefault": "默認主題色",
|
||||
"selectMenu": "選擇菜單"
|
||||
},
|
||||
"welcome": {
|
||||
"subtitle": "副標題",
|
||||
"subtitlePlaceholder": "請輸入副標題",
|
||||
"showTime": "顯示時間"
|
||||
},
|
||||
"countdown": {
|
||||
"config": "倒計時設置",
|
||||
"targetTime": "目標時間",
|
||||
"targetTimePlaceholder": "選擇目標時間",
|
||||
"showDays": "顯示天數",
|
||||
"showHours": "顯示小時",
|
||||
"showMinutes": "顯示分鐘",
|
||||
"showSeconds": "顯示秒數",
|
||||
"finishedText": "結束文字",
|
||||
"finishedTextPlaceholder": "如:已結束"
|
||||
},
|
||||
"clock": {
|
||||
"config": "時鐘設置",
|
||||
"showDate": "顯示日期",
|
||||
"showSeconds": "顯示秒數",
|
||||
"format24": "24小時制",
|
||||
"timezone": "時區",
|
||||
"timezoneOptions": {
|
||||
"local": "本地時間",
|
||||
"utc": "UTC",
|
||||
"beijing": "北京時間",
|
||||
"tokyo": "東京時間",
|
||||
"newyork": "紐約時間",
|
||||
"london": "倫敦時間"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"config": "天氣配置",
|
||||
"cityName": "城市名稱",
|
||||
"cityNamePlaceholder": "如:北京",
|
||||
"presetCity": "預設城市",
|
||||
"presetCityPlaceholder": "快速選擇城市",
|
||||
"latitude": "緯度",
|
||||
"longitude": "經度",
|
||||
"autoLocate": "自動定位",
|
||||
"refreshInterval": "刷新間隔(分鐘)",
|
||||
"refreshIntervalTip": "單位:分鐘,最小 5 分鐘"
|
||||
},
|
||||
"carousel": {
|
||||
"config": "輪播設置",
|
||||
"autoplay": "自動播放",
|
||||
"interval": "切換間隔",
|
||||
"intervalUnit": "毫秒",
|
||||
"showIndicator": "顯示指示器",
|
||||
"showArrow": "顯示箭頭",
|
||||
"imageList": "圖片列表",
|
||||
"addImage": "添加圖片",
|
||||
"selectFromGallery": "從圖庫選擇",
|
||||
"selectedImages": "已選圖片",
|
||||
"selectTip": "請從圖庫選擇圖片",
|
||||
"newItem": "新圖片"
|
||||
},
|
||||
"table": {
|
||||
"config": "表格設置",
|
||||
"stripe": "斑馬紋",
|
||||
"border": "邊框",
|
||||
"showIndex": "顯示序號",
|
||||
"showHeader": "顯示表頭",
|
||||
"highlightCurrentRow": "高亮當前行",
|
||||
"alignment": "對齊方式",
|
||||
"headerAlign": "表頭對齊",
|
||||
"cellAlign": "單元格對齊",
|
||||
"heightConfig": "高度配置",
|
||||
"height": "固定高度",
|
||||
"heightPlaceholder": "如:300px 或 100%",
|
||||
"heightTip": "支持像素值或百分比",
|
||||
"maxHeight": "最大高度",
|
||||
"maxHeightPlaceholder": "如:500",
|
||||
"maxHeightTip": "超出後顯示滾動條",
|
||||
"customization": "自定義",
|
||||
"emptyText": "空數據提示",
|
||||
"emptyTextPlaceholder": "暫無數據",
|
||||
"headerBgColor": "表頭背景色",
|
||||
"summaryConfig": "統計配置",
|
||||
"showSummary": "顯示表尾統計",
|
||||
"summaryType": "統計類型",
|
||||
"summaryTypes": {
|
||||
"sum": "合計",
|
||||
"avg": "平均值",
|
||||
"count": "計數",
|
||||
"max": "最大值",
|
||||
"min": "最小值"
|
||||
},
|
||||
"summaryPrecision": "小數位數",
|
||||
"summaryColumns": "統計列",
|
||||
"noColumnsConfigured": "請先配置表格列",
|
||||
"mergeConfig": "合併配置",
|
||||
"enableMerge": "啟用合併",
|
||||
"mergeRules": "合併規則",
|
||||
"mergeTypes": {
|
||||
"row": "行合併",
|
||||
"column": "列合併"
|
||||
},
|
||||
"selectMergeField": "選擇合併字段",
|
||||
"rowIndex": "行號",
|
||||
"startCol": "起始列",
|
||||
"colspan": "合併列數",
|
||||
"addMergeRule": "添加合併規則",
|
||||
"size": "尺寸",
|
||||
"sizeOptions": {
|
||||
"default": "默認",
|
||||
"large": "大",
|
||||
"small": "小"
|
||||
},
|
||||
"columnConfig": "列配置",
|
||||
"columnConfigJson": "列配置 JSON",
|
||||
"tableData": "表格數據",
|
||||
"tableDataJson": "表格數據 JSON",
|
||||
"complexDataTip": "表格數據結構較複雜,建議使用 JSON 模式編輯"
|
||||
},
|
||||
"iframe": {
|
||||
"config": "顯示設置",
|
||||
"showBorder": "顯示邊框",
|
||||
"allowFullscreen": "允許全屏",
|
||||
"pageAddress": "頁面地址"
|
||||
},
|
||||
"video": {
|
||||
"config": "播放設置",
|
||||
"autoplay": "自動播放",
|
||||
"loop": "循環播放",
|
||||
"muted": "靜音",
|
||||
"controls": "原生控制欄",
|
||||
"videoAddress": "視頻地址",
|
||||
"poster": "封面圖",
|
||||
"posterPlaceholder": "封面圖片 URL(選填)",
|
||||
"selectFromFileLib": "從文件庫選擇"
|
||||
},
|
||||
"image": {
|
||||
"title": "圖片",
|
||||
"config": "圖片設置",
|
||||
"alt": "替代文本",
|
||||
"altPlaceholder": "圖片無法顯示時的替代文本",
|
||||
"fit": "填充方式",
|
||||
"fitOptions": {
|
||||
"cover": "覆蓋 (cover)",
|
||||
"contain": "包含 (contain)",
|
||||
"fill": "填充 (fill)",
|
||||
"none": "無 (none)",
|
||||
"scaleDown": "縮小 (scale-down)"
|
||||
},
|
||||
"lazy": "懶加載",
|
||||
"previewConfig": "預覽設置",
|
||||
"zIndex": "預覽層級",
|
||||
"closeOnClickModal": "點擊遮罩關閉",
|
||||
"url": "圖片 URL",
|
||||
"urlPlaceholder": "請輸入圖片URL",
|
||||
"link": "跳轉鏈接(選填)",
|
||||
"previewList": "預覽圖片列表",
|
||||
"previewListTip": "點擊圖片時可預覽的圖片列表,為空時預覽當前圖片",
|
||||
"addImage": "添加圖片",
|
||||
"mainImage": "主圖"
|
||||
},
|
||||
"common": {
|
||||
"staticDataTip": "當前組件使用靜態數據,可在基礎配置中修改"
|
||||
},
|
||||
"formRender": {
|
||||
"formConfig": "表單配置",
|
||||
"formCode": "選擇表單",
|
||||
"formCodePlaceholder": "請選擇表單",
|
||||
"containerType": "容器類型",
|
||||
"drawer": "抽屜",
|
||||
"dialog": "彈窗",
|
||||
"displayConfig": "顯示配置",
|
||||
"showToolbar": "顯示工具欄",
|
||||
"showPagination": "顯示分頁",
|
||||
"pageSize": "每頁條數",
|
||||
"buttonConfig": "按鈕配置",
|
||||
"showAdd": "新增按鈕",
|
||||
"showView": "查看按鈕",
|
||||
"showEdit": "編輯按鈕",
|
||||
"showDelete": "刪除按鈕"
|
||||
}
|
||||
},
|
||||
"fieldLabels": {
|
||||
"title": "標題",
|
||||
"value": "數值",
|
||||
"time": "時間",
|
||||
"trend": "趨勢百分比",
|
||||
"trendLabel": "趨勢說明",
|
||||
"prefix": "前綴",
|
||||
"suffix": "後綴",
|
||||
"percentage": "百分比",
|
||||
"xAxisData": "X軸數據",
|
||||
"seriesData": "系列數據",
|
||||
"currentValue": "當前值",
|
||||
"min": "最小值",
|
||||
"max": "最大值",
|
||||
"listData": "列表數據",
|
||||
"linkData": "鏈接數據"
|
||||
},
|
||||
"resultType": {
|
||||
"list": "列表",
|
||||
"tree": "樹形",
|
||||
"object": "對象",
|
||||
"value": "單值",
|
||||
"chartAxis": "軸向圖表",
|
||||
"chartPie": "餅圖",
|
||||
"chartGauge": "儀表盤",
|
||||
"chartRadar": "雷達圖",
|
||||
"chartScatter": "散點圖",
|
||||
"chartHeatmap": "熱力圖"
|
||||
},
|
||||
"borderStyle": {
|
||||
"solid": "實線",
|
||||
"dashed": "虛線",
|
||||
"dotted": "點線",
|
||||
"none": "無"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
"announcement": {
|
||||
"markAllRead": "全部已讀",
|
||||
"top": "置頂",
|
||||
"noData": "暫無公告",
|
||||
"loading": "加載中...",
|
||||
"noContent": "暫無詳細內容",
|
||||
"publisher": "發佈者:",
|
||||
"priority": {
|
||||
"normal": "普通",
|
||||
"important": "重要",
|
||||
"urgent": "緊急"
|
||||
},
|
||||
"time": {
|
||||
"justNow": "剛剛",
|
||||
"minutesAgo": "分鐘前",
|
||||
"hoursAgo": "小時前",
|
||||
"daysAgo": "天前"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"city": "城市",
|
||||
"humidity": "濕度",
|
||||
"wind": "風力",
|
||||
"loading": "載入天氣資料中...",
|
||||
"error": "取得天氣資料失敗",
|
||||
"codes": {
|
||||
"clear": "晴",
|
||||
"mainlyClear": "晴間多雲",
|
||||
"partlyCloudy": "多雲",
|
||||
"overcast": "陰",
|
||||
"fog": "霧",
|
||||
"drizzle": "毛毛雨",
|
||||
"rain": "雨",
|
||||
"snow": "雪",
|
||||
"showers": "陣雨",
|
||||
"snowShowers": "陣雪",
|
||||
"thunderstorm": "雷暴"
|
||||
},
|
||||
"windDir": {
|
||||
"n": "北風",
|
||||
"ne": "東北風",
|
||||
"e": "東風",
|
||||
"se": "東南風",
|
||||
"s": "南風",
|
||||
"sw": "西南風",
|
||||
"w": "西風",
|
||||
"nw": "西北風"
|
||||
}
|
||||
},
|
||||
"chart": {
|
||||
"visits": "訪問量"
|
||||
},
|
||||
"iframe": {
|
||||
"refresh": "刷新",
|
||||
"openNew": "新窗口打開",
|
||||
"placeholder": "iframe 嵌入",
|
||||
"noUrl": "未設置 URL",
|
||||
"loading": "加載中...",
|
||||
"loadFailed": "加載失敗",
|
||||
"retry": "點擊重試"
|
||||
},
|
||||
"todo": {
|
||||
"title": "待辦事項",
|
||||
"noData": "暫無待辦",
|
||||
"priority": {
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
}
|
||||
},
|
||||
"dataTable": {
|
||||
"status": {
|
||||
"normal": "正常",
|
||||
"warning": "警告",
|
||||
"error": "異常",
|
||||
"success": "成功",
|
||||
"failed": "失敗",
|
||||
"processing": "進行中"
|
||||
},
|
||||
"summary": {
|
||||
"sum": "合計",
|
||||
"avg": "平均",
|
||||
"count": "計數",
|
||||
"max": "最大",
|
||||
"min": "最小"
|
||||
}
|
||||
},
|
||||
"formRender": {
|
||||
"placeholder": "表單渲染組件",
|
||||
"noFormCode": "請配置表單編碼",
|
||||
"deleteConfirm": "確定要刪除這條數據嗎?",
|
||||
"batchDeleteConfirm": "確定要刪除選中的 {count} 條數據嗎?",
|
||||
"importResult": "成功導入 {success} 條,失敗 {failed} 條",
|
||||
"view": "查看"
|
||||
},
|
||||
"welcome": {
|
||||
"greeting": {
|
||||
"night": "夜深了",
|
||||
"morning": "早上好",
|
||||
"morning2": "上午好",
|
||||
"noon": "中午好",
|
||||
"afternoon": "下午好",
|
||||
"evening": "晚上好"
|
||||
}
|
||||
},
|
||||
"countdown": {
|
||||
"day": "天",
|
||||
"hour": "時",
|
||||
"minute": "分",
|
||||
"second": "秒",
|
||||
"finished": "已結束"
|
||||
},
|
||||
"video": {
|
||||
"placeholder": "視頻播放器"
|
||||
},
|
||||
"ranking": {
|
||||
"tenThousand": "萬"
|
||||
},
|
||||
"clock": {
|
||||
"weekdays": ["週日", "週一", "週二", "週三", "週四", "週五", "週六"],
|
||||
"year": "年",
|
||||
"month": "月",
|
||||
"day": "日"
|
||||
},
|
||||
"image": {
|
||||
"title": "圖片",
|
||||
"noData": "暫無圖片"
|
||||
},
|
||||
"approvalCenter": {
|
||||
"initiated": "我發起的",
|
||||
"pending": "我的待辦",
|
||||
"handling": "我的在辦",
|
||||
"signing": "我的待簽",
|
||||
"handled": "我的已辦",
|
||||
"copy": "抄送我的",
|
||||
"start": "發起流程",
|
||||
"more": "更多",
|
||||
"config": "審批中心配置",
|
||||
"routePrefix": "路由前綴"
|
||||
},
|
||||
"myApps": {
|
||||
"more": "更多",
|
||||
"noData": "暫無應用"
|
||||
},
|
||||
"serverMonitor": {
|
||||
"config": "服務器信息配置",
|
||||
"cpuBgColor": "CPU背景色",
|
||||
"memoryBgColor": "記憶體背景色",
|
||||
"diskBgColor": "磁碟背景色",
|
||||
"networkBgColor": "網絡背景色",
|
||||
"core": "核",
|
||||
"thread": "線程",
|
||||
"memory": "記憶體",
|
||||
"disk": "磁碟",
|
||||
"network": "網絡",
|
||||
"read": "讀取",
|
||||
"write": "寫入",
|
||||
"upload": "上傳",
|
||||
"download": "下載",
|
||||
"totalRW": "累計讀/寫",
|
||||
"uptime": "運行時間",
|
||||
"days": "天",
|
||||
"hours": "小時",
|
||||
"minutes": "分鐘"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"config": "篩選器配置",
|
||||
"paramKey": "參數鍵名",
|
||||
"paramKeyPlaceholder": "如:status",
|
||||
"startParamKey": "開始日期參數",
|
||||
"startParamKeyPlaceholder": "如:start_date",
|
||||
"endParamKey": "結束日期參數",
|
||||
"endParamKeyPlaceholder": "如:end_date",
|
||||
"label": "標籤",
|
||||
"labelPlaceholder": "請輸入標籤",
|
||||
"defaultValue": "默認值",
|
||||
"noParamKey": "未配置參數鍵名",
|
||||
"optionConfig": "選項配置",
|
||||
"optionSource": "選項來源",
|
||||
"staticOptions": "靜態選項",
|
||||
"optionLabel": "標籤",
|
||||
"optionValue": "值",
|
||||
"addOption": "添加選項",
|
||||
"optionDataSource": "選項數據源",
|
||||
"labelField": "標籤字段",
|
||||
"valueField": "值字段",
|
||||
"multiple": "多選",
|
||||
"dateFormat": "日期格式",
|
||||
"styleConfig": "樣式配置",
|
||||
"labelPosition": "標籤位置",
|
||||
"labelPositionLeft": "左側",
|
||||
"labelPositionTop": "上方",
|
||||
"labelPositionHidden": "隱藏",
|
||||
"labelWidth": "標籤寬度",
|
||||
"labelAlign": "標籤對齊",
|
||||
"alignLeft": "左對齊",
|
||||
"alignCenter": "居中",
|
||||
"alignRight": "右對齊",
|
||||
"componentSize": "組件尺寸",
|
||||
"sizeLarge": "大",
|
||||
"sizeDefault": "中",
|
||||
"sizeSmall": "小",
|
||||
"showBorder": "顯示邊框",
|
||||
"borderRadius": "圓角"
|
||||
},
|
||||
"gradient": {
|
||||
"solid": "純色",
|
||||
"gradient": "漸變",
|
||||
"startColor": "起始色",
|
||||
"endColor": "結束色",
|
||||
"direction": "方向",
|
||||
"noColor": "未設置",
|
||||
"presetTitle": "預設漸變",
|
||||
"presets": {
|
||||
"warmSunrise": "暖陽晨曦",
|
||||
"oceanBreeze": "海洋微風",
|
||||
"freshMint": "清新薄荷",
|
||||
"peachGlow": "蜜桃光暈",
|
||||
"lavenderDream": "薰衣草夢",
|
||||
"skyBlue": "天空蔚藍",
|
||||
"roseWater": "玫瑰花水",
|
||||
"softGrass": "柔和青草",
|
||||
"winterNymph": "冬日仙子",
|
||||
"cottonCandy": "棉花糖",
|
||||
"sunnyMorning": "陽光早晨",
|
||||
"crystalClear": "水晶透明"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus';
|
||||
|
||||
import { getPageByCodeApi } from '#/api/online-dev/page-manager';
|
||||
import DashboardRenderer from '#/components/dashboard-design/DashboardRenderer.vue';
|
||||
|
||||
defineOptions({ name: 'PageRender' });
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const pageConfig = ref<string>('');
|
||||
const pageName = ref('');
|
||||
const pageCode = ref('');
|
||||
|
||||
// 获取页面编码
|
||||
function getPageCode(): string {
|
||||
// 优先从 query 获取
|
||||
if (route.query.pageCode) {
|
||||
return route.query.pageCode as string;
|
||||
}
|
||||
|
||||
// 其次从 params 获取
|
||||
if (route.params.code) {
|
||||
return route.params.code as string;
|
||||
}
|
||||
|
||||
// 最后从路径中提取(路径格式:/page-render/xxx)
|
||||
const pathParts = route.path.split('/').filter(Boolean);
|
||||
const length = pathParts.length;
|
||||
if (length >= 2 && pathParts[length - 2] === 'page-render') {
|
||||
return pathParts[length - 1] || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// 加载页面数据
|
||||
async function loadPageData() {
|
||||
const code = getPageCode();
|
||||
if (!code) {
|
||||
ElMessage.error('页面编码不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
pageCode.value = code;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const page = await getPageByCodeApi(code);
|
||||
pageName.value = page.name;
|
||||
pageConfig.value =
|
||||
page.page_config && Object.keys(page.page_config).length > 0
|
||||
? JSON.stringify(page.page_config)
|
||||
: '';
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '加载页面失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPageData();
|
||||
});
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
loadPageData();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElScrollbar class="rounded-[8px] py-3">
|
||||
<div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3">
|
||||
<DashboardRenderer v-if="pageConfig" :config="pageConfig" />
|
||||
<ElEmpty v-else-if="!loading" description="暂无页面配置" />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
Reference in New Issue
Block a user