Track AI form editor confirmation panels
This commit is contained in:
@@ -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