Track AI form editor confirmation panels
This commit is contained in:
@@ -269,6 +269,3 @@ web/apps/web-ele/src/views/ai-platform/workflow/editor/panels/SystemSummaryPanel
|
|||||||
!web/apps/web-ele/src/views/ai-platform/workflow/editor/nodes/SystemSummaryNode.vue
|
!web/apps/web-ele/src/views/ai-platform/workflow/editor/nodes/SystemSummaryNode.vue
|
||||||
!web/apps/web-ele/src/views/ai-platform/workflow/editor/panels/SystemSummaryPanel.vue
|
!web/apps/web-ele/src/views/ai-platform/workflow/editor/panels/SystemSummaryPanel.vue
|
||||||
web/apps/web-ele/src/views/wiki/
|
web/apps/web-ele/src/views/wiki/
|
||||||
# keep only lightweight form-editor barrel
|
|
||||||
web/apps/web-ele/src/components/form-editor/*
|
|
||||||
!web/apps/web-ele/src/components/form-editor/index.ts
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton, ElInput } from 'element-plus';
|
||||||
|
|
||||||
|
export interface AppDesignData {
|
||||||
|
type: 'app_design';
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
content: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
nodeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
design?: AppDesignData;
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: Record<string, any>];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
const editContent = ref('');
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const panelTitle = computed(
|
||||||
|
() => props.design?.data?.title || props.design?.title || '应用设计方案',
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.design,
|
||||||
|
(design) => {
|
||||||
|
editContent.value = design?.data?.content || '';
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm', {
|
||||||
|
content: editContent.value,
|
||||||
|
title: panelTitle.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex shrink-0 items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">{{ panelTitle }}</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
{{ $t('common.confirmAndContinue') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="isFullscreen = !isFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden p-4">
|
||||||
|
<div class="rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground">
|
||||||
|
AI 已生成应用设计方案。确认前可以直接调整正文,确认后将继续后续编排节点。
|
||||||
|
</div>
|
||||||
|
<ElInput
|
||||||
|
v-model="editContent"
|
||||||
|
class="min-h-0 flex-1"
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入应用设计方案"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-textarea),
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 应用设置面板
|
||||||
|
* 用于配置应用设置,包括应用配置、Logo配置、内置主题和布局
|
||||||
|
*/
|
||||||
|
import type { BuiltinThemeType, LayoutType, ThemeModeType } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
import {
|
||||||
|
Block,
|
||||||
|
BuiltinTheme,
|
||||||
|
Layout,
|
||||||
|
Theme,
|
||||||
|
} from '@vben/layouts/preferences-blocks';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElDivider,
|
||||||
|
ElInput,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElScrollbar,
|
||||||
|
ElSelect,
|
||||||
|
ElSwitch,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { uploadFile } from '#/api/core/file';
|
||||||
|
import { getFileUrlPublic } from '#/composables/useFileUrl';
|
||||||
|
|
||||||
|
// 应用设置数据接口
|
||||||
|
export interface AppSettingsData {
|
||||||
|
type: 'app_settings';
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
app?: {
|
||||||
|
defaultHomePath?: string;
|
||||||
|
dynamicTitle?: boolean;
|
||||||
|
enableCheckUpdates?: boolean;
|
||||||
|
enablePreferences?: boolean;
|
||||||
|
layout?: string;
|
||||||
|
locale?: string;
|
||||||
|
name?: string;
|
||||||
|
watermark?: boolean;
|
||||||
|
watermarkContent?: string;
|
||||||
|
};
|
||||||
|
breadcrumb?: Record<string, any>;
|
||||||
|
copyright?: Record<string, any>;
|
||||||
|
footer?: Record<string, any>;
|
||||||
|
header?: Record<string, any>;
|
||||||
|
logo?: {
|
||||||
|
enable?: boolean;
|
||||||
|
fit?: string;
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
navigation?: Record<string, any>;
|
||||||
|
shortcutKeys?: Record<string, any>;
|
||||||
|
sidebar?: Record<string, any>;
|
||||||
|
tabbar?: Record<string, any>;
|
||||||
|
theme?: {
|
||||||
|
builtinType?: string;
|
||||||
|
colorPrimary?: string;
|
||||||
|
mode?: string;
|
||||||
|
radius?: string;
|
||||||
|
semiDarkHeader?: boolean;
|
||||||
|
semiDarkSidebar?: boolean;
|
||||||
|
};
|
||||||
|
transition?: Record<string, any>;
|
||||||
|
widget?: Record<string, any>;
|
||||||
|
};
|
||||||
|
nodeId?: string;
|
||||||
|
layoutOptions?: Array<{ label: string; value: string }>;
|
||||||
|
themeOptions?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
settings?: AppSettingsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: Record<string, any>];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
const appName = ref('');
|
||||||
|
const appDefaultHomePath = ref('/analytics');
|
||||||
|
const appEnablePreferences = ref(true);
|
||||||
|
const appLayout = ref<LayoutType>('sidebar-nav');
|
||||||
|
|
||||||
|
// Logo配置
|
||||||
|
const logoEnable = ref(true);
|
||||||
|
const logoSource = ref('');
|
||||||
|
const logoFit = ref('contain');
|
||||||
|
const logoDisplayUrl = ref('');
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
const themeMode = ref<ThemeModeType>('light');
|
||||||
|
const themeBuiltinType = ref<BuiltinThemeType>('default');
|
||||||
|
const themeColorPrimary = ref('hsl(212 100% 45%)');
|
||||||
|
const themeSemiDarkSidebar = ref(false);
|
||||||
|
const themeSemiDarkHeader = ref(false);
|
||||||
|
|
||||||
|
// 适应方式选项
|
||||||
|
const fitOptions = computed(() => [
|
||||||
|
{ label: $t('ai-platform.appSettings.fitOptions.contain'), value: 'contain' },
|
||||||
|
{ label: $t('ai-platform.appSettings.fitOptions.cover'), value: 'cover' },
|
||||||
|
{ label: $t('ai-platform.appSettings.fitOptions.fill'), value: 'fill' },
|
||||||
|
{ label: $t('ai-platform.appSettings.fitOptions.none'), value: 'none' },
|
||||||
|
{
|
||||||
|
label: $t('ai-platform.appSettings.fitOptions.scale-down'),
|
||||||
|
value: 'scale-down',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 面板标题
|
||||||
|
const panelTitle = computed(() => {
|
||||||
|
return props.settings?.title || $t('ai-platform.appSettings.title');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听设置数据变化
|
||||||
|
watch(
|
||||||
|
() => props.settings,
|
||||||
|
(newSettings) => {
|
||||||
|
if (newSettings?.data) {
|
||||||
|
const data = newSettings.data;
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
appName.value = data.app?.name || '';
|
||||||
|
appDefaultHomePath.value = data.app?.defaultHomePath || '/analytics';
|
||||||
|
appEnablePreferences.value = data.app?.enablePreferences ?? true;
|
||||||
|
appLayout.value = (data.app?.layout || 'sidebar-nav') as LayoutType;
|
||||||
|
|
||||||
|
// Logo配置
|
||||||
|
logoEnable.value = data.logo?.enable ?? true;
|
||||||
|
logoSource.value = data.logo?.source || '';
|
||||||
|
logoFit.value = data.logo?.fit || 'contain';
|
||||||
|
loadLogoDisplayUrl(logoSource.value);
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
themeMode.value = (data.theme?.mode || 'light') as ThemeModeType;
|
||||||
|
themeBuiltinType.value = (data.theme?.builtinType ||
|
||||||
|
'default') as BuiltinThemeType;
|
||||||
|
themeColorPrimary.value = data.theme?.colorPrimary || 'hsl(212 100% 45%)';
|
||||||
|
themeSemiDarkSidebar.value = data.theme?.semiDarkSidebar ?? false;
|
||||||
|
themeSemiDarkHeader.value = data.theme?.semiDarkHeader ?? false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听logoSource变化,加载显示URL
|
||||||
|
watch(logoSource, (newSource) => {
|
||||||
|
loadLogoDisplayUrl(newSource);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否为文件ID
|
||||||
|
function isFileId(value: string): boolean {
|
||||||
|
return (
|
||||||
|
/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(value) ||
|
||||||
|
/^\d+$/.test(value) ||
|
||||||
|
/^[\w-]{10,30}$/.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载Logo显示URL
|
||||||
|
function loadLogoDisplayUrl(source: string) {
|
||||||
|
if (!source) {
|
||||||
|
logoDisplayUrl.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
source.startsWith('http://') ||
|
||||||
|
source.startsWith('https://') ||
|
||||||
|
source.startsWith('/') ||
|
||||||
|
source.startsWith('data:')
|
||||||
|
) {
|
||||||
|
logoDisplayUrl.value = source;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isFileId(source)) {
|
||||||
|
logoDisplayUrl.value = getFileUrlPublic(source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logoDisplayUrl.value = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
const uploadInputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
function openFileSelector() {
|
||||||
|
uploadInputRef.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileInputChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await uploadFile(file, {
|
||||||
|
isPublic: true,
|
||||||
|
source: 'avatar',
|
||||||
|
});
|
||||||
|
if (response?.id) {
|
||||||
|
logoSource.value = getFileUrlPublic(String(response.id));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error($t('ui-config.loadError'));
|
||||||
|
}
|
||||||
|
target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogo() {
|
||||||
|
logoSource.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建设置数据
|
||||||
|
function buildSettingsData(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
name: appName.value,
|
||||||
|
defaultHomePath: appDefaultHomePath.value,
|
||||||
|
enablePreferences: appEnablePreferences.value,
|
||||||
|
layout: appLayout.value,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
enable: logoEnable.value,
|
||||||
|
source: logoSource.value,
|
||||||
|
fit: logoFit.value,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: themeMode.value,
|
||||||
|
builtinType: themeBuiltinType.value,
|
||||||
|
colorPrimary: themeColorPrimary.value,
|
||||||
|
semiDarkSidebar: themeSemiDarkSidebar.value,
|
||||||
|
semiDarkHeader: themeSemiDarkHeader.value,
|
||||||
|
},
|
||||||
|
// 保留原有的其他配置
|
||||||
|
sidebar: props.settings?.data?.sidebar,
|
||||||
|
header: props.settings?.data?.header,
|
||||||
|
footer: props.settings?.data?.footer,
|
||||||
|
copyright: props.settings?.data?.copyright,
|
||||||
|
navigation: props.settings?.data?.navigation,
|
||||||
|
tabbar: props.settings?.data?.tabbar,
|
||||||
|
breadcrumb: props.settings?.data?.breadcrumb,
|
||||||
|
transition: props.settings?.data?.transition,
|
||||||
|
widget: props.settings?.data?.widget,
|
||||||
|
shortcutKeys: props.settings?.data?.shortcutKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认并继续
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm', buildSettingsData());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
function handleClose() {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="app-settings-panel border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">
|
||||||
|
{{ panelTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
{{ $t('common.confirmAndContinue') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<ElScrollbar class="flex-1">
|
||||||
|
<div class="grid grid-cols-2 gap-6 p-6">
|
||||||
|
<!-- 左列:应用配置 + Logo配置 -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 应用配置 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h4 class="mb-4 text-sm font-semibold">
|
||||||
|
{{ $t('ai-platform.appSettings.appConfig') }}
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Block :title="$t('ai-platform.appSettings.appName')">
|
||||||
|
<ElInput
|
||||||
|
v-model="appName"
|
||||||
|
:placeholder="
|
||||||
|
$t('ai-platform.appSettings.appNamePlaceholder')
|
||||||
|
"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :title="$t('ai-platform.appSettings.defaultHomePath')">
|
||||||
|
<ElInput
|
||||||
|
v-model="appDefaultHomePath"
|
||||||
|
:placeholder="
|
||||||
|
$t('ai-platform.appSettings.defaultHomePathPlaceholder')
|
||||||
|
"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :title="$t('ai-platform.appSettings.enablePreferences')">
|
||||||
|
<ElSwitch v-model="appEnablePreferences" />
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDivider />
|
||||||
|
|
||||||
|
<!-- Logo配置 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h4 class="mb-4 text-sm font-semibold">
|
||||||
|
{{ $t('ai-platform.appSettings.logoConfig') }}
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Block :title="$t('ai-platform.appSettings.logoEnable')">
|
||||||
|
<ElSwitch v-model="logoEnable" />
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<template v-if="logoEnable">
|
||||||
|
<Block :title="$t('ai-platform.appSettings.logoSource')">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<!-- Logo 预览和上传 -->
|
||||||
|
<div class="logo-preview-container flex">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="logoDisplayUrl"
|
||||||
|
class="logo-preview"
|
||||||
|
@click="openFileSelector"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="logoDisplayUrl"
|
||||||
|
alt="Logo"
|
||||||
|
class="logo-image"
|
||||||
|
/>
|
||||||
|
<div class="logo-overlay">
|
||||||
|
<span class="text-sm text-white">{{
|
||||||
|
$t('common.replace')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="logo-placeholder"
|
||||||
|
@click="openFileSelector"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground text-sm">{{
|
||||||
|
$t(
|
||||||
|
'ai-platform.appSettings.logoSourcePlaceholder',
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElButton
|
||||||
|
v-if="logoDisplayUrl"
|
||||||
|
size="small"
|
||||||
|
@click="clearLogo"
|
||||||
|
>
|
||||||
|
{{ $t('common.clear') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="uploadInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleFileInputChange"
|
||||||
|
/>
|
||||||
|
<ElInput
|
||||||
|
v-model="logoSource"
|
||||||
|
:placeholder="
|
||||||
|
$t('ai-platform.appSettings.logoSourcePlaceholder')
|
||||||
|
"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :title="$t('ai-platform.appSettings.logoFit')">
|
||||||
|
<ElSelect v-model="logoFit" class="w-full">
|
||||||
|
<ElOption
|
||||||
|
v-for="item in fitOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</Block>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右列:布局配置 + 主题配置 -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 布局配置 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h4 class="mb-4 text-sm font-semibold">
|
||||||
|
{{ $t('ai-platform.appSettings.layoutConfig') }}
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Block :title="$t('ai-platform.appSettings.layout')">
|
||||||
|
<Layout v-model="appLayout" />
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDivider />
|
||||||
|
|
||||||
|
<!-- 主题配置 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<h4 class="mb-4 text-sm font-semibold">
|
||||||
|
{{ $t('ai-platform.appSettings.themeConfig') }}
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Block :title="$t('ai-platform.appSettings.themeMode')">
|
||||||
|
<Theme
|
||||||
|
v-model="themeMode"
|
||||||
|
v-model:theme-semi-dark-sidebar="themeSemiDarkSidebar"
|
||||||
|
v-model:theme-semi-dark-header="themeSemiDarkHeader"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :title="$t('ai-platform.appSettings.builtinTheme')">
|
||||||
|
<BuiltinTheme
|
||||||
|
v-model="themeBuiltinType"
|
||||||
|
v-model:theme-color-primary="themeColorPrimary"
|
||||||
|
:is-dark="themeMode === 'dark'"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-settings-panel {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgb(0 0 0 / 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview:hover .logo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 2px dashed hsl(var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-placeholder:hover {
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 表单基础信息编辑组件
|
||||||
|
* 可用于:表单管理器、工作流设计预览编辑
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
export interface BasicFormData {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
form_type: 'normal' | 'workflow';
|
||||||
|
sort: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: BasicFormData;
|
||||||
|
disabled?: boolean;
|
||||||
|
showTitle?: boolean;
|
||||||
|
labelWidth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
disabled: false,
|
||||||
|
showTitle: true,
|
||||||
|
labelWidth: '100px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: BasicFormData];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formData = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新单个字段
|
||||||
|
function updateField<K extends keyof BasicFormData>(key: K, value: BasicFormData[K]) {
|
||||||
|
emit('update:modelValue', { ...props.modelValue, [key]: value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="basic-info-editor">
|
||||||
|
<h3 v-if="showTitle" class="mb-6 text-center text-lg font-medium">
|
||||||
|
{{ $t('form-manager.editor.steps.basic') }}
|
||||||
|
</h3>
|
||||||
|
<ElForm
|
||||||
|
:model="formData"
|
||||||
|
:label-width="labelWidth"
|
||||||
|
label-position="right"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<ElFormItem :label="$t('form-manager.name')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="formData.name"
|
||||||
|
:placeholder="$t('form-manager.placeholder.name')"
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateField('name', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem :label="$t('form-manager.code')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="formData.code"
|
||||||
|
:placeholder="$t('form-manager.placeholder.code')"
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateField('code', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem :label="$t('form-manager.type')" required>
|
||||||
|
<ElSelect
|
||||||
|
:model-value="formData.form_type"
|
||||||
|
:placeholder="$t('form-manager.placeholder.type')"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateField('form_type', $event)"
|
||||||
|
>
|
||||||
|
<ElOption :label="$t('form-manager.typeMap.normal')" value="normal" />
|
||||||
|
<ElOption :label="$t('form-manager.typeMap.workflow')" value="workflow" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem :label="$t('common.sort')">
|
||||||
|
<ElInput
|
||||||
|
:model-value="formData.sort"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
@update:model-value="updateField('sort', Number($event))"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem :label="$t('form-manager.description')">
|
||||||
|
<ElInput
|
||||||
|
:model-value="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
:placeholder="$t('form-manager.editor.placeholder.remark')"
|
||||||
|
@update:model-value="updateField('description', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.basic-info-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 仪表盘基础信息确认面板
|
||||||
|
* 复用 PageBasicInfoEditor 公共组件
|
||||||
|
*/
|
||||||
|
import type { PageBasicInfo } from './PageBasicInfoEditor.vue';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import PageBasicInfoEditor from './PageBasicInfoEditor.vue';
|
||||||
|
|
||||||
|
export interface DashboardBasicInfoData {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
category: string;
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
};
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
basicInfo?: DashboardBasicInfoData;
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: Record<string, any>];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = ref<PageBasicInfo>({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
category: 'dashboard',
|
||||||
|
description: '',
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.basicInfo,
|
||||||
|
(info) => {
|
||||||
|
if (info?.data) {
|
||||||
|
form.value = {
|
||||||
|
name: info.data.name || '',
|
||||||
|
code: info.data.code || '',
|
||||||
|
category: info.data.category || 'dashboard',
|
||||||
|
description: info.data.description || '',
|
||||||
|
sort: info.data.sort || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', { ...form.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">
|
||||||
|
{{ basicInfo?.title || $t('ai-platform.dashboard.basicInfo.title') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
{{ $t('common.confirmAndContinue') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
|
<PageBasicInfoEditor v-model="form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton, ElInput, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
export interface DashboardDesignData {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
dashboard_code?: string;
|
||||||
|
design_suggestion?: string;
|
||||||
|
design_title?: string;
|
||||||
|
page_config?: Record<string, any>;
|
||||||
|
};
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
design?: DashboardDesignData;
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: Record<string, any>];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
const configText = ref('{}');
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.design,
|
||||||
|
(design) => {
|
||||||
|
configText.value = JSON.stringify(design?.data?.page_config || {}, null, 2);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
try {
|
||||||
|
emit('confirm', {
|
||||||
|
design_title:
|
||||||
|
props.design?.data?.design_title ||
|
||||||
|
$t('ai-platform.dashboard.design.title'),
|
||||||
|
page_config: JSON.parse(configText.value || '{}'),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('页面配置不是有效 JSON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex shrink-0 items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground flex items-center gap-3 font-medium">
|
||||||
|
<div class="bg-primary flex h-8 w-8 items-center justify-center rounded">
|
||||||
|
<span class="text-sm font-bold text-white">D</span>
|
||||||
|
</div>
|
||||||
|
<span>{{ design?.title || $t('ai-platform.dashboard.design.title') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
{{ $t('common.confirmAndContinue') }}
|
||||||
|
</ElButton>
|
||||||
|
<div class="bg-border mx-1 h-5 w-px"></div>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="isFullscreen = !isFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid min-h-0 flex-1 grid-cols-[320px_minmax(0,1fr)] gap-4 overflow-hidden p-4">
|
||||||
|
<div class="space-y-4 overflow-auto rounded-md border bg-muted/20 p-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium">仪表盘编码</div>
|
||||||
|
<div class="mt-1 text-sm text-muted-foreground">
|
||||||
|
{{ design?.data?.dashboard_code || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium">设计标题</div>
|
||||||
|
<div class="mt-1 text-sm text-muted-foreground">
|
||||||
|
{{ design?.data?.design_title || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium">设计建议</div>
|
||||||
|
<div class="mt-1 whitespace-pre-wrap text-sm text-muted-foreground">
|
||||||
|
{{ design?.data?.design_suggestion || '暂无设计建议' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElInput
|
||||||
|
v-model="configText"
|
||||||
|
class="min-h-0"
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="页面配置 JSON"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-textarea),
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 仪表盘发布确认面板
|
||||||
|
* 复用 PagePublishInfoEditor 公共组件
|
||||||
|
*/
|
||||||
|
import type { PagePublishInfo } from './PagePublishInfoEditor.vue';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import PagePublishInfoEditor from './PagePublishInfoEditor.vue';
|
||||||
|
|
||||||
|
export interface DashboardPublishData {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
application_id?: string;
|
||||||
|
dashboard_code: string;
|
||||||
|
dashboard_id: string;
|
||||||
|
menu_icon: string;
|
||||||
|
menu_name: string;
|
||||||
|
menu_order: number;
|
||||||
|
menu_parent_id?: string;
|
||||||
|
};
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
publishData?: DashboardPublishData;
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: Record<string, any>];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = ref<PagePublishInfo>({
|
||||||
|
menu_name: '',
|
||||||
|
menu_parent_id: undefined,
|
||||||
|
menu_icon: 'lucide:layout-dashboard',
|
||||||
|
menu_order: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.publishData,
|
||||||
|
(data) => {
|
||||||
|
if (data?.data) {
|
||||||
|
form.value = {
|
||||||
|
menu_name: data.data.menu_name || '',
|
||||||
|
menu_parent_id: data.data.menu_parent_id || undefined,
|
||||||
|
menu_icon: data.data.menu_icon || 'lucide:layout-dashboard',
|
||||||
|
menu_order: data.data.menu_order || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const confirmData = {
|
||||||
|
dashboard_id: props.publishData?.data.dashboard_id,
|
||||||
|
dashboard_code: props.publishData?.data.dashboard_code,
|
||||||
|
menu_name: form.value.menu_name,
|
||||||
|
menu_parent_id: form.value.menu_parent_id || '',
|
||||||
|
menu_icon: form.value.menu_icon,
|
||||||
|
menu_order: form.value.menu_order,
|
||||||
|
application_id: props.publishData?.data.application_id || '',
|
||||||
|
};
|
||||||
|
console.log('[DashboardPublishConfirmPanel] 确认数据:', confirmData);
|
||||||
|
emit('confirm', confirmData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">
|
||||||
|
{{ publishData?.title || $t('ai-platform.dashboard.publish.title') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
{{ $t('common.confirmAndContinue') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
|
<PagePublishInfoEditor
|
||||||
|
v-model="form"
|
||||||
|
:page-code="publishData?.data.dashboard_code || ''"
|
||||||
|
:show-route-info="true"
|
||||||
|
route-prefix="/page-render/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 设计编辑面板
|
||||||
|
* 用于工作流中每步设计完成后的编辑
|
||||||
|
* 使用 FormEditorContent 公共组件,根据当前步骤显示对应内容
|
||||||
|
*/
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { Maximize, Minimize } from '@vben/icons';
|
||||||
|
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
import FormEditorContent from './FormEditorContent.vue';
|
||||||
|
|
||||||
|
// 设计类型
|
||||||
|
export type DesignType = 'form_basic_info' | 'database_design' | 'form_ui_design' | 'list_config' | 'form_publish' | 'dashboard_basic_info' | 'dashboard_design' | 'dashboard_publish';
|
||||||
|
|
||||||
|
// 设计数据
|
||||||
|
export interface DesignData {
|
||||||
|
type: DesignType;
|
||||||
|
title: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
nodeId: string;
|
||||||
|
form_fields?: any[]; // 表单字段列表(用于列表设计)
|
||||||
|
table_configs?: any[]; // 数据表配置(用于表单设计)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
design?: DesignData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
'save': [data: Record<string, any>];
|
||||||
|
'confirm': [data: Record<string, any>];
|
||||||
|
'close': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 编辑器组件引用
|
||||||
|
const editorRef = ref<InstanceType<typeof FormEditorContent>>();
|
||||||
|
|
||||||
|
// 根据设计类型映射到步骤索引
|
||||||
|
const stepMap: Record<DesignType, number> = {
|
||||||
|
'form_basic_info': 0,
|
||||||
|
'database_design': 1,
|
||||||
|
'form_ui_design': 2,
|
||||||
|
'list_config': 3,
|
||||||
|
'form_publish': 4,
|
||||||
|
'dashboard_basic_info': 0,
|
||||||
|
'dashboard_design': 0,
|
||||||
|
'dashboard_publish': 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前步骤
|
||||||
|
const currentStep = ref(0);
|
||||||
|
|
||||||
|
// 面板标题
|
||||||
|
const panelTitle = computed(() => {
|
||||||
|
if (!props.design) return '';
|
||||||
|
return props.design.title;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听设计数据变化,设置当前步骤和初始化数据
|
||||||
|
watch(
|
||||||
|
() => props.design,
|
||||||
|
(newDesign) => {
|
||||||
|
if (!newDesign) return;
|
||||||
|
|
||||||
|
// 根据设计类型设置当前步骤
|
||||||
|
currentStep.value = stepMap[newDesign.type] ?? 0;
|
||||||
|
|
||||||
|
// 初始化编辑器数据
|
||||||
|
if (editorRef.value) {
|
||||||
|
initEditorData(newDesign);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听编辑器引用变化,初始化数据
|
||||||
|
watch(
|
||||||
|
() => editorRef.value,
|
||||||
|
(editor) => {
|
||||||
|
if (editor && props.design) {
|
||||||
|
initEditorData(props.design);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化编辑器数据
|
||||||
|
function initEditorData(design: DesignData) {
|
||||||
|
if (!editorRef.value) return;
|
||||||
|
|
||||||
|
switch (design.type) {
|
||||||
|
case 'form_basic_info': {
|
||||||
|
const data = design.data || {};
|
||||||
|
editorRef.value.setData({
|
||||||
|
basicForm: {
|
||||||
|
name: data.name || '',
|
||||||
|
code: data.code || '',
|
||||||
|
form_type: data.form_type || 'normal',
|
||||||
|
sort: data.sort || 0,
|
||||||
|
description: data.description || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'database_design': {
|
||||||
|
const tableData = design.data?.table || design.data || {};
|
||||||
|
// 优先从 meta 中读取 schema,兼容旧数据从顶层读取
|
||||||
|
const metaData = tableData.meta || {};
|
||||||
|
editorRef.value.setData({
|
||||||
|
tableConfigs: [{
|
||||||
|
id: 'main-table',
|
||||||
|
type: 'main',
|
||||||
|
tableName: tableData.tableName || '',
|
||||||
|
alias: tableData.alias || tableData.tableName || '',
|
||||||
|
fields: tableData.fields || [],
|
||||||
|
meta: {
|
||||||
|
schema: metaData.schema || tableData.schema || '',
|
||||||
|
schemaRaw: metaData.schemaRaw || tableData.schemaRaw || '',
|
||||||
|
database: metaData.database || tableData.database || '',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'form_ui_design': {
|
||||||
|
const formConfig = design.data || {};
|
||||||
|
// 从 design 中获取 table_configs(后端在 waiting_config 中传递)
|
||||||
|
const tableConfigs = design.table_configs || design.data?.table_configs || [];
|
||||||
|
editorRef.value.setData({
|
||||||
|
tableConfigs: tableConfigs,
|
||||||
|
formConfig: {
|
||||||
|
items: formConfig.items || [],
|
||||||
|
labelWidth: formConfig.labelWidth || 120,
|
||||||
|
labelPosition: formConfig.labelPosition || 'right',
|
||||||
|
size: formConfig.size || 'default',
|
||||||
|
formPadding: formConfig.formPadding || 20,
|
||||||
|
formMargin: formConfig.formMargin || 0,
|
||||||
|
itemSpacing: formConfig.itemSpacing || 18,
|
||||||
|
formWidth: formConfig.formWidth || '100%',
|
||||||
|
formMaxWidth: formConfig.formMaxWidth || 0,
|
||||||
|
formBackground: formConfig.formBackground || false,
|
||||||
|
formBorder: formConfig.formBorder || false,
|
||||||
|
formBorderRadius: formConfig.formBorderRadius || 4,
|
||||||
|
formShadow: formConfig.formShadow || false,
|
||||||
|
disabled: formConfig.disabled || false,
|
||||||
|
tableConfigs: tableConfigs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'list_config': {
|
||||||
|
// 从 design 中获取 form_fields(后端在 waiting_config 中传递)
|
||||||
|
const formFields = design.form_fields || design.data?.form_fields || [];
|
||||||
|
editorRef.value.setData({
|
||||||
|
listConfig: design.data || {},
|
||||||
|
formFields: formFields,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'form_publish': {
|
||||||
|
const publishData = design.data || {};
|
||||||
|
editorRef.value.setData({
|
||||||
|
publishData: {
|
||||||
|
menu_name: publishData.menu_name || '',
|
||||||
|
menu_parent_id: publishData.menu_parent_id,
|
||||||
|
menu_icon: publishData.menu_icon || 'lucide:file-text',
|
||||||
|
menu_order: publishData.menu_order ?? 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前编辑的数据
|
||||||
|
function getCurrentData(): Record<string, any> {
|
||||||
|
if (!editorRef.value) return {};
|
||||||
|
|
||||||
|
const data = editorRef.value.getData();
|
||||||
|
const designType = props.design?.type;
|
||||||
|
|
||||||
|
switch (designType) {
|
||||||
|
case 'form_basic_info':
|
||||||
|
return { ...data.basicForm };
|
||||||
|
case 'database_design':
|
||||||
|
return {
|
||||||
|
type: data.tableConfigs[0]?.type || 'main', // 添加 type 字段,默认为 main
|
||||||
|
table: data.tableConfigs[0] ? {
|
||||||
|
tableName: data.tableConfigs[0].tableName,
|
||||||
|
alias: data.tableConfigs[0].alias,
|
||||||
|
fields: data.tableConfigs[0].fields,
|
||||||
|
meta: {
|
||||||
|
schema: data.tableConfigs[0].meta?.schema || '',
|
||||||
|
schemaRaw: data.tableConfigs[0].meta?.schemaRaw || '',
|
||||||
|
database: data.tableConfigs[0].meta?.database || '',
|
||||||
|
},
|
||||||
|
} : {},
|
||||||
|
dbConfig: props.design?.data?.dbConfig || 'default', // 保留原有的 dbConfig
|
||||||
|
};
|
||||||
|
case 'form_ui_design':
|
||||||
|
return {
|
||||||
|
items: data.formConfig.items,
|
||||||
|
labelWidth: data.formConfig.labelWidth,
|
||||||
|
labelPosition: data.formConfig.labelPosition,
|
||||||
|
};
|
||||||
|
case 'list_config':
|
||||||
|
return { ...data.listConfig };
|
||||||
|
case 'form_publish':
|
||||||
|
return { ...data.publishData };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认并继续
|
||||||
|
function handleConfirm() {
|
||||||
|
const data = getCurrentData();
|
||||||
|
emit('confirm', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
function handleClose() {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
:class="[
|
||||||
|
'border-border bg-card flex flex-col rounded-lg',
|
||||||
|
isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">
|
||||||
|
{{ panelTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">{{ $t('common.cancel') }}</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">{{ $t('common.confirmAndContinue') }}</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域:使用 FormEditorContent 公共组件 -->
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<FormEditorContent
|
||||||
|
ref="editorRef"
|
||||||
|
:step="currentStep"
|
||||||
|
:show-steps="true"
|
||||||
|
:show-actions="false"
|
||||||
|
:workflow-mode="true"
|
||||||
|
@update:step="currentStep = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.design-editor-panel {
|
||||||
|
/* box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElInputNumber,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
ElSteps,
|
||||||
|
ElStep,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import PublishInfoEditor, { type FormPublishInput } from './PublishInfoEditor.vue';
|
||||||
|
|
||||||
|
export interface BasicFormData {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
form_type: 'normal' | 'workflow';
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableConfig {
|
||||||
|
alias?: string;
|
||||||
|
fields?: any[];
|
||||||
|
id?: string;
|
||||||
|
meta?: Record<string, any>;
|
||||||
|
tableName?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormEditorData {
|
||||||
|
basicForm: BasicFormData;
|
||||||
|
formConfig: {
|
||||||
|
disabled: boolean;
|
||||||
|
formBackground: boolean;
|
||||||
|
formBorder: boolean;
|
||||||
|
formBorderRadius: number;
|
||||||
|
formMargin: number;
|
||||||
|
formMaxWidth: number;
|
||||||
|
formPadding: number;
|
||||||
|
formShadow: boolean;
|
||||||
|
formWidth: string;
|
||||||
|
items: any[];
|
||||||
|
itemSpacing: number;
|
||||||
|
labelPosition: 'left' | 'right' | 'top';
|
||||||
|
labelWidth: number;
|
||||||
|
lifecycleHooks?: any[];
|
||||||
|
size: 'default' | 'large' | 'small';
|
||||||
|
tableConfigs: TableConfig[];
|
||||||
|
};
|
||||||
|
formFields: any[];
|
||||||
|
listConfig: Record<string, any>;
|
||||||
|
publishData?: FormPublishInput;
|
||||||
|
tableConfigs: TableConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
initialBasicForm?: BasicFormData;
|
||||||
|
initialListDesign?: Record<string, any>;
|
||||||
|
initialTableConfigs?: TableConfig[];
|
||||||
|
readonly?: boolean;
|
||||||
|
showActions?: boolean;
|
||||||
|
showSteps?: boolean;
|
||||||
|
step?: number;
|
||||||
|
workflowMode?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
initialBasicForm: undefined,
|
||||||
|
initialListDesign: undefined,
|
||||||
|
initialTableConfigs: undefined,
|
||||||
|
readonly: false,
|
||||||
|
showActions: true,
|
||||||
|
showSteps: true,
|
||||||
|
step: undefined,
|
||||||
|
workflowMode: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
cancel: [];
|
||||||
|
save: [data: FormEditorData];
|
||||||
|
'step-change': [step: number];
|
||||||
|
'update:step': [step: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const internalStep = ref(0);
|
||||||
|
const currentStep = computed({
|
||||||
|
get: () => (props.step === undefined ? internalStep.value : props.step),
|
||||||
|
set: (val) => {
|
||||||
|
if (props.step === undefined) internalStep.value = val;
|
||||||
|
else emit('update:step', val);
|
||||||
|
emit('step-change', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicForm = ref<BasicFormData>({
|
||||||
|
code: '',
|
||||||
|
description: '',
|
||||||
|
form_type: 'normal',
|
||||||
|
name: '',
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
const tableConfigs = ref<TableConfig[]>([]);
|
||||||
|
const formConfigText = ref('{}');
|
||||||
|
const listConfigText = ref('{}');
|
||||||
|
const formFields = ref<any[]>([]);
|
||||||
|
const publishData = ref<FormPublishInput>({
|
||||||
|
menu_icon: 'lucide:file-text',
|
||||||
|
menu_name: '',
|
||||||
|
menu_order: 0,
|
||||||
|
menu_parent_id: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = computed(() => {
|
||||||
|
const base = [
|
||||||
|
{ title: $t('form-manager.editor.steps.basic') || '基础信息' },
|
||||||
|
{ title: $t('form-manager.editor.steps.database') || '数据表' },
|
||||||
|
{ title: $t('form-manager.editor.steps.form') || '表单设计' },
|
||||||
|
{ title: $t('form-manager.editor.steps.list') || '列表设计' },
|
||||||
|
];
|
||||||
|
if (props.workflowMode) {
|
||||||
|
base.push({ title: $t('form-manager.editor.steps.publish') || '发布配置' });
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canGoPrev = computed(() => currentStep.value > 0);
|
||||||
|
const isLastStep = computed(() => currentStep.value === steps.value.length - 1);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialBasicForm,
|
||||||
|
(val) => {
|
||||||
|
if (val) basicForm.value = { ...val };
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialTableConfigs,
|
||||||
|
(val) => {
|
||||||
|
if (val) tableConfigs.value = [...val];
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialListDesign,
|
||||||
|
(val) => {
|
||||||
|
if (val) listConfigText.value = JSON.stringify(val, null, 2);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => basicForm.value.name,
|
||||||
|
(name) => {
|
||||||
|
if (props.workflowMode && name && !publishData.value.menu_name) {
|
||||||
|
publishData.value.menu_name = name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function parseJson(text: string, fallback: any) {
|
||||||
|
if (!text.trim()) return fallback;
|
||||||
|
return JSON.parse(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultFormConfig(items: any[] = []) {
|
||||||
|
return {
|
||||||
|
disabled: false,
|
||||||
|
formBackground: false,
|
||||||
|
formBorder: false,
|
||||||
|
formBorderRadius: 4,
|
||||||
|
formMargin: 0,
|
||||||
|
formMaxWidth: 0,
|
||||||
|
formPadding: 20,
|
||||||
|
formShadow: false,
|
||||||
|
formWidth: '100%',
|
||||||
|
items,
|
||||||
|
itemSpacing: 18,
|
||||||
|
labelPosition: 'right' as const,
|
||||||
|
labelWidth: 120,
|
||||||
|
size: 'default' as const,
|
||||||
|
tableConfigs: tableConfigs.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getData(): FormEditorData {
|
||||||
|
const formConfig = {
|
||||||
|
...buildDefaultFormConfig(),
|
||||||
|
...parseJson(formConfigText.value, {}),
|
||||||
|
};
|
||||||
|
const listConfig = parseJson(listConfigText.value, {});
|
||||||
|
return {
|
||||||
|
basicForm: { ...basicForm.value },
|
||||||
|
formConfig,
|
||||||
|
formFields: [...formFields.value],
|
||||||
|
listConfig,
|
||||||
|
publishData: props.workflowMode ? { ...publishData.value } : undefined,
|
||||||
|
tableConfigs: [...tableConfigs.value],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setData(data: Partial<FormEditorData>) {
|
||||||
|
if (data.basicForm) basicForm.value = { ...data.basicForm };
|
||||||
|
if (data.tableConfigs) tableConfigs.value = [...data.tableConfigs];
|
||||||
|
if (data.formConfig) {
|
||||||
|
formConfigText.value = JSON.stringify(data.formConfig, null, 2);
|
||||||
|
formFields.value = data.formFields || data.formConfig.items || [];
|
||||||
|
}
|
||||||
|
if (data.listConfig) listConfigText.value = JSON.stringify(data.listConfig, null, 2);
|
||||||
|
if (data.formFields) formFields.value = [...data.formFields];
|
||||||
|
if (data.publishData) publishData.value = { ...publishData.value, ...data.publishData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
if (canGoPrev.value) currentStep.value -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
if (!isLastStep.value) currentStep.value += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
try {
|
||||||
|
emit('save', getData());
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('JSON 配置格式不正确');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ getData, setData });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<div v-if="showSteps" class="border-border border-b px-6 py-4">
|
||||||
|
<ElSteps :active="currentStep" align-center finish-status="success">
|
||||||
|
<ElStep v-for="item in steps" :key="item.title" :title="item.title" />
|
||||||
|
</ElSteps>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-auto p-6">
|
||||||
|
<ElForm v-if="currentStep === 0" :model="basicForm" label-width="100px">
|
||||||
|
<ElFormItem label="名称" required>
|
||||||
|
<ElInput v-model="basicForm.name" :disabled="readonly" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="编码" required>
|
||||||
|
<ElInput v-model="basicForm.code" :disabled="readonly" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="类型">
|
||||||
|
<ElSelect v-model="basicForm.form_type" :disabled="readonly" class="w-full">
|
||||||
|
<ElOption label="普通表单" value="normal" />
|
||||||
|
<ElOption label="流程表单" value="workflow" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="排序">
|
||||||
|
<ElInputNumber v-model="basicForm.sort" :disabled="readonly" class="w-full" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="描述">
|
||||||
|
<ElInput v-model="basicForm.description" :disabled="readonly" type="textarea" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
|
<div v-else-if="currentStep === 1" class="space-y-3">
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
轻量版保留 AI 生成的数据表结构确认,不加载在线开发表设计器。
|
||||||
|
</div>
|
||||||
|
<ElInput
|
||||||
|
:model-value="JSON.stringify(tableConfigs, null, 2)"
|
||||||
|
readonly
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="currentStep === 2" class="h-full min-h-[420px]">
|
||||||
|
<ElInput
|
||||||
|
v-model="formConfigText"
|
||||||
|
:readonly="readonly"
|
||||||
|
class="h-full"
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="表单配置 JSON"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="currentStep === 3" class="h-full min-h-[420px]">
|
||||||
|
<ElInput
|
||||||
|
v-model="listConfigText"
|
||||||
|
:readonly="readonly"
|
||||||
|
class="h-full"
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="列表配置 JSON"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PublishInfoEditor
|
||||||
|
v-else
|
||||||
|
v-model="publishData"
|
||||||
|
:form-code="basicForm.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showActions" class="border-border flex justify-end gap-2 border-t px-6 py-4">
|
||||||
|
<ElButton @click="emit('cancel')">{{ $t('common.cancel') }}</ElButton>
|
||||||
|
<ElButton :disabled="!canGoPrev" @click="handlePrev">上一步</ElButton>
|
||||||
|
<ElButton v-if="!isLastStep" type="primary" @click="handleNext">下一步</ElButton>
|
||||||
|
<ElButton v-else type="primary" @click="handleSave">{{ $t('common.save') }}</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-textarea),
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 页面基础信息编辑器
|
||||||
|
* 供 page-manager 和工作流节点共用
|
||||||
|
*/
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElInputNumber,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
export interface PageBasicInfo {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
sort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: PageBasicInfo;
|
||||||
|
isEditMode?: boolean;
|
||||||
|
showTitle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isEditMode: false,
|
||||||
|
showTitle: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: PageBasicInfo];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 更新表单数据
|
||||||
|
function updateFormData(key: keyof PageBasicInfo, value: any) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类选项
|
||||||
|
const categoryOptions = [
|
||||||
|
{ label: $t('page-manager.categoryMap.dashboard'), value: 'dashboard' },
|
||||||
|
{ label: $t('page-manager.categoryMap.portal'), value: 'portal' },
|
||||||
|
{ label: $t('page-manager.categoryMap.databoard'), value: 'databoard' },
|
||||||
|
{ label: $t('page-manager.categoryMap.other'), value: 'other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 v-if="showTitle" class="mb-6 text-center text-lg font-medium">
|
||||||
|
{{ $t('page-manager.editor.steps.basic') }}
|
||||||
|
</h3>
|
||||||
|
<ElForm :model="modelValue" label-width="100px" label-position="right">
|
||||||
|
<ElFormItem :label="$t('page-manager.name')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="modelValue.name"
|
||||||
|
:placeholder="$t('page-manager.placeholder.name')"
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateFormData('name', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.code')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="modelValue.code"
|
||||||
|
:placeholder="$t('page-manager.placeholder.code')"
|
||||||
|
clearable
|
||||||
|
:disabled="isEditMode"
|
||||||
|
@update:model-value="updateFormData('code', $event)"
|
||||||
|
/>
|
||||||
|
<template #error>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
modelValue.code &&
|
||||||
|
!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(modelValue.code)
|
||||||
|
"
|
||||||
|
class="el-form-item__error"
|
||||||
|
>
|
||||||
|
{{ $t('page-manager.codeFormatError') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.category')">
|
||||||
|
<ElSelect
|
||||||
|
:model-value="modelValue.category"
|
||||||
|
:placeholder="$t('page-manager.placeholder.category')"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateFormData('category', $event)"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="opt in categoryOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('common.sort')">
|
||||||
|
<ElInputNumber
|
||||||
|
:model-value="modelValue.sort"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateFormData('sort', $event ?? 0)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.description')">
|
||||||
|
<ElInput
|
||||||
|
:model-value="modelValue.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
:placeholder="$t('page-manager.placeholder.description')"
|
||||||
|
@update:model-value="updateFormData('description', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PageBasicInfo } from './PageBasicInfoEditor.vue';
|
||||||
|
import type { PagePublishInfo } from './PagePublishInfoEditor.vue';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton, ElInput, ElMessage, ElStep, ElSteps } from 'element-plus';
|
||||||
|
|
||||||
|
import PageBasicInfoEditor from './PageBasicInfoEditor.vue';
|
||||||
|
import PagePublishInfoEditor from './PagePublishInfoEditor.vue';
|
||||||
|
|
||||||
|
export interface PageEditorData {
|
||||||
|
basicForm: PageBasicInfo;
|
||||||
|
pageConfig: Record<string, any>;
|
||||||
|
publishData?: PagePublishInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
initialBasicForm?: PageBasicInfo;
|
||||||
|
initialPageConfig?: Record<string, any>;
|
||||||
|
initialPublishData?: PagePublishInfo;
|
||||||
|
isEditMode?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
showActions?: boolean;
|
||||||
|
showSteps?: boolean;
|
||||||
|
step?: number;
|
||||||
|
workflowMode?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
initialBasicForm: undefined,
|
||||||
|
initialPageConfig: undefined,
|
||||||
|
initialPublishData: undefined,
|
||||||
|
isEditMode: false,
|
||||||
|
readonly: false,
|
||||||
|
showActions: true,
|
||||||
|
showSteps: true,
|
||||||
|
step: undefined,
|
||||||
|
workflowMode: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
cancel: [];
|
||||||
|
save: [data: PageEditorData];
|
||||||
|
'step-change': [step: number];
|
||||||
|
'update:step': [step: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const internalStep = ref(0);
|
||||||
|
const currentStep = computed({
|
||||||
|
get: () => (props.step === undefined ? internalStep.value : props.step),
|
||||||
|
set: (val) => {
|
||||||
|
if (props.step === undefined) internalStep.value = val;
|
||||||
|
else emit('update:step', val);
|
||||||
|
emit('step-change', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicForm = ref<PageBasicInfo>({
|
||||||
|
category: 'dashboard',
|
||||||
|
code: '',
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageConfigText = ref('{}');
|
||||||
|
const publishData = ref<PagePublishInfo>({
|
||||||
|
menu_icon: 'lucide:layout-dashboard',
|
||||||
|
menu_name: '',
|
||||||
|
menu_order: 0,
|
||||||
|
menu_parent_id: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = computed(() => {
|
||||||
|
const base = [
|
||||||
|
{ title: $t('page-manager.editor.steps.basic') || '基础信息' },
|
||||||
|
{ title: $t('page-manager.editor.steps.design') || '页面设计' },
|
||||||
|
];
|
||||||
|
if (props.workflowMode) {
|
||||||
|
base.push({ title: $t('page-manager.editor.steps.publish') || '发布配置' });
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canGoPrev = computed(() => currentStep.value > 0);
|
||||||
|
const isLastStep = computed(() => currentStep.value === steps.value.length - 1);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialBasicForm,
|
||||||
|
(val) => {
|
||||||
|
if (val) basicForm.value = { ...val };
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialPageConfig,
|
||||||
|
(val) => {
|
||||||
|
if (val) pageConfigText.value = JSON.stringify(val, null, 2);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialPublishData,
|
||||||
|
(val) => {
|
||||||
|
if (val) publishData.value = { ...val };
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => basicForm.value.name,
|
||||||
|
(name) => {
|
||||||
|
if (props.workflowMode && name && !publishData.value.menu_name) {
|
||||||
|
publishData.value.menu_name = name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function getData(): PageEditorData {
|
||||||
|
const pageConfig = pageConfigText.value.trim()
|
||||||
|
? JSON.parse(pageConfigText.value)
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
basicForm: { ...basicForm.value },
|
||||||
|
pageConfig,
|
||||||
|
publishData: props.workflowMode ? { ...publishData.value } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setData(data: Partial<PageEditorData>) {
|
||||||
|
if (data.basicForm) basicForm.value = { ...data.basicForm };
|
||||||
|
if (data.pageConfig) pageConfigText.value = JSON.stringify(data.pageConfig, null, 2);
|
||||||
|
if (data.publishData) publishData.value = { ...publishData.value, ...data.publishData };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
if (canGoPrev.value) currentStep.value -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
if (!isLastStep.value) currentStep.value += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
try {
|
||||||
|
emit('save', getData());
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('页面配置不是有效 JSON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ getData, setData });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<div v-if="showSteps" class="border-border border-b px-6 py-4">
|
||||||
|
<ElSteps :active="currentStep" align-center finish-status="success">
|
||||||
|
<ElStep v-for="item in steps" :key="item.title" :title="item.title" />
|
||||||
|
</ElSteps>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-auto p-6">
|
||||||
|
<PageBasicInfoEditor
|
||||||
|
v-if="currentStep === 0"
|
||||||
|
v-model="basicForm"
|
||||||
|
:is-edit-mode="isEditMode"
|
||||||
|
/>
|
||||||
|
<div v-else-if="currentStep === 1" class="h-full min-h-[460px]">
|
||||||
|
<div class="mb-3 text-sm text-muted-foreground">
|
||||||
|
轻量版保留 AI 生成页面配置确认,不加载大屏可视化设计器。
|
||||||
|
</div>
|
||||||
|
<ElInput
|
||||||
|
v-model="pageConfigText"
|
||||||
|
:readonly="readonly"
|
||||||
|
class="h-full"
|
||||||
|
resize="none"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="页面配置 JSON"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PagePublishInfoEditor
|
||||||
|
v-else
|
||||||
|
v-model="publishData"
|
||||||
|
:page-code="basicForm.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showActions" class="border-border flex justify-end gap-2 border-t px-6 py-4">
|
||||||
|
<ElButton @click="emit('cancel')">{{ $t('common.cancel') }}</ElButton>
|
||||||
|
<ElButton :disabled="!canGoPrev" @click="handlePrev">上一步</ElButton>
|
||||||
|
<ElButton v-if="!isLastStep" type="primary" @click="handleNext">下一步</ElButton>
|
||||||
|
<ElButton v-else type="primary" @click="handleSave">{{ $t('common.save') }}</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-textarea),
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 页面发布信息编辑器
|
||||||
|
* 供 page-manager 和工作流节点共用
|
||||||
|
*/
|
||||||
|
import type { MenuItem } from '#/components/zq-form/zq-menu-selector/types';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||||
|
|
||||||
|
import { ZqIconPicker } from '#/components/zq-form/zq-icon-picker';
|
||||||
|
import { ZqMenuSelector } from '#/components/zq-form/zq-menu-selector';
|
||||||
|
import { useAppContextStore } from '#/store/app-context';
|
||||||
|
|
||||||
|
export interface PagePublishInfo {
|
||||||
|
menu_name: string;
|
||||||
|
menu_parent_id?: string;
|
||||||
|
menu_icon: string;
|
||||||
|
menu_order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: PagePublishInfo;
|
||||||
|
pageCode?: string;
|
||||||
|
showRouteInfo?: boolean;
|
||||||
|
routePrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
pageCode: '',
|
||||||
|
showRouteInfo: true,
|
||||||
|
routePrefix: '/page/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: PagePublishInfo];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const appContextStore = useAppContextStore();
|
||||||
|
|
||||||
|
// 更新表单数据
|
||||||
|
function updateFormData(key: keyof PagePublishInfo, value: any) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单选择回调
|
||||||
|
function handleMenuChange(menu: MenuItem | MenuItem[] | null) {
|
||||||
|
if (menu && !Array.isArray(menu)) {
|
||||||
|
updateFormData('menu_parent_id', menu.id);
|
||||||
|
} else {
|
||||||
|
updateFormData('menu_parent_id', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElForm :model="modelValue" label-width="100px">
|
||||||
|
<!-- 菜单配置 -->
|
||||||
|
<div class="mb-4 font-medium">
|
||||||
|
{{ $t('page-manager.publishDialog.menuConfig') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.publishDialog.menuName')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="modelValue.menu_name"
|
||||||
|
:placeholder="$t('page-manager.placeholder.name')"
|
||||||
|
@update:model-value="updateFormData('menu_name', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.publishDialog.parentMenu')">
|
||||||
|
<ZqMenuSelector
|
||||||
|
:model-value="modelValue.menu_parent_id || null"
|
||||||
|
mode="dialog"
|
||||||
|
:placeholder="$t('page-manager.publishDialog.parentMenuPlaceholder')"
|
||||||
|
:application-id="appContextStore.currentApp?.id"
|
||||||
|
@change="handleMenuChange"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.publishDialog.menuIcon')">
|
||||||
|
<ZqIconPicker
|
||||||
|
:model-value="modelValue.menu_icon"
|
||||||
|
prefix="lucide"
|
||||||
|
:auto-fetch-api="false"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateFormData('menu_icon', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('common.sort')">
|
||||||
|
<ElInputNumber
|
||||||
|
:model-value="modelValue.menu_order"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateFormData('menu_order', $event ?? 0)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- 路由信息 -->
|
||||||
|
<template v-if="showRouteInfo && pageCode">
|
||||||
|
<div class="mb-4 mt-6 font-medium">
|
||||||
|
{{ $t('page-manager.publishDialog.routeInfo') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('page-manager.publishDialog.accessPath')">
|
||||||
|
<ElInput
|
||||||
|
:model-value="`${routePrefix}${pageCode}`"
|
||||||
|
disabled
|
||||||
|
class="text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
</ElForm>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MenuItem } from '#/components/zq-form/zq-menu-selector/types';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElForm, ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||||
|
|
||||||
|
import { ZqIconPicker } from '#/components/zq-form/zq-icon-picker';
|
||||||
|
import { ZqMenuSelector } from '#/components/zq-form/zq-menu-selector';
|
||||||
|
import { useAppContextStore } from '#/store/app-context';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: FormPublishInput;
|
||||||
|
formCode?: string;
|
||||||
|
showRouteInfo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
formCode: '',
|
||||||
|
showRouteInfo: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: FormPublishInput];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const appContextStore = useAppContextStore();
|
||||||
|
|
||||||
|
// 更新表单数据
|
||||||
|
function updateFormData(key: keyof FormPublishInput, value: any) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单选择回调
|
||||||
|
function handleMenuChange(menu: MenuItem | MenuItem[] | null) {
|
||||||
|
if (menu && !Array.isArray(menu)) {
|
||||||
|
updateFormData('menu_parent_id', menu.id);
|
||||||
|
} else {
|
||||||
|
updateFormData('menu_parent_id', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 modelValue 变化,确保响应式
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElForm :model="modelValue" label-width="100px">
|
||||||
|
<!-- 菜单配置 -->
|
||||||
|
<div class="mb-4 font-medium">
|
||||||
|
{{ $t('form-manager.publishDialog.menuConfig') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('form-manager.publishDialog.menuName')" required>
|
||||||
|
<ElInput
|
||||||
|
:model-value="modelValue.menu_name"
|
||||||
|
:placeholder="$t('form-manager.placeholder.name')"
|
||||||
|
@update:model-value="updateFormData('menu_name', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('form-manager.publishDialog.parentMenu')">
|
||||||
|
<ZqMenuSelector
|
||||||
|
:model-value="modelValue.menu_parent_id || null"
|
||||||
|
mode="dialog"
|
||||||
|
:placeholder="$t('form-manager.publishDialog.parentMenuPlaceholder')"
|
||||||
|
:application-id="appContextStore.currentApp?.id"
|
||||||
|
@change="handleMenuChange"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('form-manager.publishDialog.menuIcon')">
|
||||||
|
<ZqIconPicker
|
||||||
|
:model-value="modelValue.menu_icon"
|
||||||
|
prefix="lucide"
|
||||||
|
:auto-fetch-api="false"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateFormData('menu_icon', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('common.sort')">
|
||||||
|
<ElInputNumber
|
||||||
|
:model-value="modelValue.menu_order"
|
||||||
|
:min="0"
|
||||||
|
:max="999"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="updateFormData('menu_order', $event)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<!-- 路由信息 -->
|
||||||
|
<template v-if="showRouteInfo && formCode">
|
||||||
|
<div class="mb-4 mt-6 font-medium">
|
||||||
|
{{ $t('form-manager.publishDialog.routeInfo') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem :label="$t('form-manager.publishDialog.accessPath')">
|
||||||
|
<ElInput
|
||||||
|
:model-value="`/form-render/${formCode}`"
|
||||||
|
disabled
|
||||||
|
class="text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
</ElForm>
|
||||||
|
</template>
|
||||||
|
export interface FormPublishInput {
|
||||||
|
menu_icon: string;
|
||||||
|
menu_name: string;
|
||||||
|
menu_order: number;
|
||||||
|
menu_parent_id?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AppWindow,
|
||||||
|
CircleCheck,
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
LayoutDashboard,
|
||||||
|
Maximize,
|
||||||
|
Minimize,
|
||||||
|
} from '@vben/icons';
|
||||||
|
import { useI18n } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ElButton, ElCard, ElEmpty } from 'element-plus';
|
||||||
|
|
||||||
|
export interface SystemSummaryData {
|
||||||
|
type: 'system_summary';
|
||||||
|
title: string;
|
||||||
|
data: {
|
||||||
|
app?: {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
link: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
base_url?: string;
|
||||||
|
created_at: string;
|
||||||
|
dashboard?: {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
link: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
forms: Array<{
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
link: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
statistics?: {
|
||||||
|
forms_count: number;
|
||||||
|
has_app: boolean;
|
||||||
|
has_dashboard: boolean;
|
||||||
|
total_modules: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data?: SystemSummaryData;
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
confirm: [data: any];
|
||||||
|
'update:visible': [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// 全屏状态
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryData = computed(() => props.data?.data || ({} as any));
|
||||||
|
const statistics = computed(() => summaryData.value.statistics);
|
||||||
|
const app = computed(() => summaryData.value.app);
|
||||||
|
const forms = computed(() => summaryData.value.forms || []);
|
||||||
|
const dashboard = computed(() => summaryData.value.dashboard);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', summaryData.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLink = (link: string) => {
|
||||||
|
if (link) {
|
||||||
|
const baseUrl = summaryData.value.base_url || '';
|
||||||
|
const fullUrl = baseUrl ? `${baseUrl}${link}` : link;
|
||||||
|
window.open(fullUrl, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="border-border bg-card flex flex-col rounded-lg"
|
||||||
|
:class="[isFullscreen ? 'fixed inset-0 z-50 ml-0' : 'ml-3 h-full w-full']"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div
|
||||||
|
class="border-border bg-muted/50 flex items-center justify-between border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="text-foreground font-medium">
|
||||||
|
{{ data?.title || t('ai.systemSummary.title') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ElButton size="small" @click="handleClose">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton size="small" type="primary" @click="handleConfirm">
|
||||||
|
<CircleCheck class="mr-1 size-4" />
|
||||||
|
{{ t('ai.systemSummary.complete') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:icon="isFullscreen ? Minimize : Maximize"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="flex-1 overflow-auto p-6">
|
||||||
|
<div
|
||||||
|
class="mb-6 flex items-center gap-3 border-b border-green-200 pb-4 dark:border-green-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex size-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900"
|
||||||
|
>
|
||||||
|
<CircleCheck class="size-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-green-700 dark:text-green-300">
|
||||||
|
{{ summaryData.title || t('ai.systemSummary.title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
{{ t('ai.systemSummary.subtitle') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="statistics" class="mb-6 grid grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border bg-blue-50 p-4 text-center dark:bg-blue-950"
|
||||||
|
>
|
||||||
|
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{{ statistics.has_app ? 1 : 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-sm">
|
||||||
|
{{ t('ai.systemSummary.app') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg border bg-purple-50 p-4 text-center dark:bg-purple-950"
|
||||||
|
>
|
||||||
|
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400">
|
||||||
|
{{ statistics.forms_count }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-sm">
|
||||||
|
{{ t('ai.systemSummary.formModules') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg border bg-orange-50 p-4 text-center dark:bg-orange-950"
|
||||||
|
>
|
||||||
|
<div class="text-3xl font-bold text-orange-600 dark:text-orange-400">
|
||||||
|
{{ statistics.has_dashboard ? 1 : 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground mt-1 text-sm">
|
||||||
|
{{ t('ai.systemSummary.dashboard') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="app" class="mb-6">
|
||||||
|
<h3 class="mb-3 flex items-center gap-2 font-medium">
|
||||||
|
<AppWindow class="size-5 text-blue-500" />
|
||||||
|
{{ t('ai.systemSummary.appInfo') }}
|
||||||
|
</h3>
|
||||||
|
<ElCard
|
||||||
|
shadow="hover"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openLink(app.link)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex size-10 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900"
|
||||||
|
>
|
||||||
|
<AppWindow class="size-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ app.name }}</div>
|
||||||
|
<div class="text-muted-foreground text-sm">
|
||||||
|
{{ app.description || app.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
v-if="app.link"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click.stop="openLink(app.link)"
|
||||||
|
>
|
||||||
|
<ExternalLink class="mr-1 size-4" />
|
||||||
|
{{ t('ai.systemSummary.visit') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="forms.length > 0" class="mb-6">
|
||||||
|
<h3 class="mb-3 flex items-center gap-2 font-medium">
|
||||||
|
<FileText class="size-5 text-purple-500" />
|
||||||
|
{{ t('ai.systemSummary.formModulesTitle') }}
|
||||||
|
<span class="text-muted-foreground text-sm">({{ forms.length }})</span>
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<ElCard
|
||||||
|
v-for="form in forms"
|
||||||
|
:key="form.id || form.code"
|
||||||
|
shadow="hover"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openLink(form.link)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex size-10 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900"
|
||||||
|
>
|
||||||
|
<FileText
|
||||||
|
class="size-5 text-purple-600 dark:text-purple-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ form.name }}</div>
|
||||||
|
<div class="text-muted-foreground text-sm">
|
||||||
|
{{ form.description || form.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
v-if="form.link"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click.stop="openLink(form.link)"
|
||||||
|
>
|
||||||
|
<ExternalLink class="mr-1 size-4" />
|
||||||
|
{{ t('ai.systemSummary.visit') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dashboard" class="mb-6">
|
||||||
|
<h3 class="mb-3 flex items-center gap-2 font-medium">
|
||||||
|
<LayoutDashboard class="size-5 text-orange-500" />
|
||||||
|
{{ t('ai.systemSummary.dashboardTitle') }}
|
||||||
|
</h3>
|
||||||
|
<ElCard
|
||||||
|
shadow="hover"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openLink(dashboard.link)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex size-10 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900"
|
||||||
|
>
|
||||||
|
<LayoutDashboard
|
||||||
|
class="size-5 text-orange-600 dark:text-orange-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{{ dashboard.name }}</div>
|
||||||
|
<div class="text-muted-foreground text-sm">
|
||||||
|
{{ dashboard.description || dashboard.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
v-if="dashboard.link"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click.stop="openLink(dashboard.link)"
|
||||||
|
>
|
||||||
|
<ExternalLink class="mr-1 size-4" />
|
||||||
|
{{ t('ai.systemSummary.visit') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElEmpty
|
||||||
|
v-if="!app && forms.length === 0 && !dashboard"
|
||||||
|
:description="t('ai.systemSummary.noData')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user