Improve lightweight AI design preview dialog

This commit is contained in:
2026-06-09 22:29:53 +08:00
parent e6aa82f058
commit e7dc89d6aa
@@ -10,11 +10,11 @@ import {
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import { import {
ElAlert,
ElButton, ElButton,
ElDescriptions, ElDescriptions,
ElDescriptionsItem, ElDescriptionsItem,
ElDialog, ElDialog,
ElDivider,
ElInput, ElInput,
ElMessage, ElMessage,
ElScrollbar, ElScrollbar,
@@ -39,6 +39,7 @@ const emit = defineEmits<{
}>(); }>();
const draftText = ref('{}'); const draftText = ref('{}');
const contentText = ref('');
const sourceData = computed(() => { const sourceData = computed(() => {
return ( return (
@@ -52,7 +53,16 @@ const sourceData = computed(() => {
); );
}); });
const payload = computed(() => { const sourceType = computed(() => {
const source = sourceData.value as Record<string, any>;
if (source.type) return String(source.type);
if (props.settings) return 'app_settings';
if (props.publishData) return 'dashboard_publish';
if (props.basicInfo) return 'dashboard_basic_info';
return 'design';
});
const payload = computed<Record<string, any>>(() => {
const source = sourceData.value as Record<string, any>; const source = sourceData.value as Record<string, any>;
if ('data' in source && source.data && typeof source.data === 'object') { if ('data' in source && source.data && typeof source.data === 'object') {
return source.data; return source.data;
@@ -60,30 +70,80 @@ const payload = computed(() => {
return source; return source;
}); });
const typeLabelMap: Record<string, string> = {
app_design: '应用设计方案',
app_settings: '应用设置方案',
dashboard_basic_info: '仪表盘基础信息',
dashboard_design: '仪表盘设计方案',
dashboard_publish: '仪表盘发布配置',
database_design: '数据库设计方案',
form_basic_info: '表单基础信息',
form_ui_design: '表单界面设计',
list_config: '列表配置方案',
system_summary: '系统生成总结',
};
const dialogTitle = computed(() => { const dialogTitle = computed(() => {
const source = sourceData.value as Record<string, any>; const source = sourceData.value as Record<string, any>;
return source.title || props.title || 'AI 方案确认'; return (
source.title ||
props.title ||
typeLabelMap[sourceType.value] ||
'AI 方案确认'
);
});
const primaryText = computed(() => {
const source = payload.value;
return (
source.content ||
source.design_suggestion ||
source.summary ||
source.description ||
''
);
}); });
const summaryItems = computed(() => { const summaryItems = computed(() => {
const source = payload.value as Record<string, any>; const source = payload.value;
const keys = ['name', 'title', 'description', 'code', 'type', 'category']; const candidates: Array<[string, any]> = [
return keys ['名称', source.name || source.title || source.design_title],
.filter((key) => source[key] !== undefined && source[key] !== null) ['编码', source.code || source.dashboard_code || source.form_code],
.map((key) => ({ key, value: String(source[key]) })); ['类型', source.type || source.category],
['路由', source.route_path || source.path],
['菜单', source.menu_name],
['图标', source.menu_icon],
['排序', source.menu_order],
['说明', source.description],
];
return candidates
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([label, value]) => ({ label, value: String(value) }));
}); });
const fieldItems = computed(() => { const fieldItems = computed(() => {
const source = payload.value as Record<string, any>; const source = payload.value;
const fields = const fields =
source.schema_fields || source.form_fields || source.fields || source.schema?.fields; source.schema_fields ||
return Array.isArray(fields) ? fields.slice(0, 12) : []; source.form_fields ||
source.fields ||
source.schema?.fields;
return Array.isArray(fields) ? fields.slice(0, 16) : [];
}); });
const tableItems = computed(() => { const tableItems = computed(() => {
const source = payload.value as Record<string, any>; const source = payload.value;
const tables = source.data_sources || source.table_configs || source.tables; const tables = source.data_sources || source.table_configs || source.tables;
return Array.isArray(tables) ? tables.slice(0, 8) : []; return Array.isArray(tables) ? tables.slice(0, 10) : [];
});
const routePreview = computed(() => {
const source = payload.value;
if (source.route_path) return source.route_path;
if (source.dashboard_code) return `/dashboard/${source.dashboard_code}`;
if (source.form_code) return `/form-render/${source.form_code}`;
return '';
}); });
watch( watch(
@@ -91,11 +151,24 @@ watch(
() => { () => {
if (props.visible) { if (props.visible) {
draftText.value = JSON.stringify(payload.value || {}, null, 2); draftText.value = JSON.stringify(payload.value || {}, null, 2);
contentText.value = primaryText.value;
} }
}, },
{ immediate: true }, { immediate: true },
); );
function mergedDraft() {
const data = JSON.parse(draftText.value || '{}');
if (
contentText.value &&
(sourceType.value === 'app_design' || sourceType.value === 'dashboard_design')
) {
if ('content' in data) data.content = contentText.value;
else data.design_suggestion = contentText.value;
}
return data && typeof data === 'object' ? data : { value: data };
}
function handleClose() { function handleClose() {
emit('update:visible', false); emit('update:visible', false);
emit('close'); emit('close');
@@ -103,11 +176,10 @@ function handleClose() {
function handleConfirm() { function handleConfirm() {
try { try {
const data = JSON.parse(draftText.value || '{}'); emit('confirm', mergedDraft());
emit('confirm', data && typeof data === 'object' ? data : { value: data });
emit('update:visible', false); emit('update:visible', false);
} catch { } catch {
ElMessage.error('JSON 格式不正确'); ElMessage.error('JSON 格式不正确,请检查后再确认');
} }
} }
@@ -125,7 +197,7 @@ function handleVisibleChange(visible: boolean) {
<ElDialog <ElDialog
:model-value="visible" :model-value="visible"
:title="dialogTitle" :title="dialogTitle"
width="860px" width="980px"
append-to-body append-to-body
destroy-on-close destroy-on-close
class="ai-design-preview-dialog" class="ai-design-preview-dialog"
@@ -133,22 +205,44 @@ function handleVisibleChange(visible: boolean) {
> >
<div class="ai-design-preview"> <div class="ai-design-preview">
<section class="preview-panel"> <section class="preview-panel">
<div class="panel-title">方案概要</div> <div class="panel-heading">
<div>
<div class="panel-title">{{ typeLabelMap[sourceType] || '方案预览' }}</div>
<div class="panel-subtext">确认后将把当前结构化配置回传给工作流继续执行</div>
</div>
<ElTag effect="dark" type="primary">{{ sourceType }}</ElTag>
</div>
<ElAlert
v-if="routePreview"
class="mb-3"
type="info"
:closable="false"
show-icon
>
<template #title>预计访问路径{{ routePreview }}</template>
</ElAlert>
<ElDescriptions v-if="summaryItems.length" :column="2" border size="small"> <ElDescriptions v-if="summaryItems.length" :column="2" border size="small">
<ElDescriptionsItem <ElDescriptionsItem
v-for="item in summaryItems" v-for="item in summaryItems"
:key="item.key" :key="item.label"
:label="item.key" :label="item.label"
> >
{{ item.value }} {{ item.value }}
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<div v-else class="empty-hint">当前方案未提供概要字段</div> <div v-else class="empty-hint">当前方案未提供概要字段</div>
<ElDivider v-if="fieldItems.length || tableItems.length" /> <template v-if="primaryText">
<div class="section-title">方案说明</div>
<ElScrollbar height="150px" class="text-preview">
<pre>{{ primaryText }}</pre>
</ElScrollbar>
</template>
<template v-if="fieldItems.length"> <template v-if="fieldItems.length">
<div class="panel-subtitle">字段设计</div> <div class="section-title">字段设计</div>
<div class="tag-grid"> <div class="tag-grid">
<ElTag <ElTag
v-for="(field, index) in fieldItems" v-for="(field, index) in fieldItems"
@@ -161,14 +255,14 @@ function handleVisibleChange(visible: boolean) {
</template> </template>
<template v-if="tableItems.length"> <template v-if="tableItems.length">
<div class="panel-subtitle">数据配置</div> <div class="section-title">数据/页面配置</div>
<div class="table-list"> <div class="table-list">
<div <div
v-for="(table, index) in tableItems" v-for="(table, index) in tableItems"
:key="table.id || table.name || index" :key="table.id || table.name || index"
class="table-item" class="table-item"
> >
<span>{{ table.title || table.name || `数据表 ${index + 1}` }}</span> <span>{{ table.title || table.name || `配置 ${index + 1}` }}</span>
<small>{{ table.code || table.table_name || table.type || '' }}</small> <small>{{ table.code || table.table_name || table.type || '' }}</small>
</div> </div>
</div> </div>
@@ -176,15 +270,25 @@ function handleVisibleChange(visible: boolean) {
</section> </section>
<section class="editor-panel"> <section class="editor-panel">
<div v-if="primaryText" class="content-editor">
<div class="panel-title">可编辑方案正文</div>
<ElInput
v-model="contentText"
type="textarea"
:autosize="{ minRows: 7, maxRows: 10 }"
resize="none"
/>
</div>
<div class="panel-title-row"> <div class="panel-title-row">
<span class="panel-title">结构化数据</span> <span class="panel-title">结构化数据</span>
<ElButton size="small" :icon="Copy" @click="copyJson">复制</ElButton> <ElButton size="small" :icon="Copy" @click="copyJson">复制</ElButton>
</div> </div>
<ElScrollbar height="390px"> <ElScrollbar height="330px">
<ElInput <ElInput
v-model="draftText" v-model="draftText"
type="textarea" type="textarea"
:autosize="{ minRows: 18, maxRows: 24 }" :autosize="{ minRows: 16, maxRows: 22 }"
resize="none" resize="none"
class="json-editor" class="json-editor"
/> />
@@ -204,7 +308,7 @@ function handleVisibleChange(visible: boolean) {
<style scoped> <style scoped>
.ai-design-preview { .ai-design-preview {
display: grid; display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
gap: 16px; gap: 16px;
} }
@@ -217,23 +321,30 @@ function handleVisibleChange(visible: boolean) {
background: var(--el-bg-color); background: var(--el-bg-color);
} }
.panel-title, .panel-heading,
.panel-title-row {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.panel-title-row { .panel-title-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
} }
.panel-subtitle { .panel-title {
font-size: 14px;
font-weight: 600;
}
.panel-subtext {
margin-top: 4px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.section-title {
margin: 14px 0 8px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
margin: 12px 0 8px;
} }
.empty-hint { .empty-hint {
@@ -243,6 +354,31 @@ function handleVisibleChange(visible: boolean) {
text-align: center; text-align: center;
} }
.text-preview {
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
background: var(--el-fill-color-lighter);
padding: 10px;
}
.text-preview pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--el-text-color-regular);
font-family: inherit;
font-size: 13px;
line-height: 1.6;
}
.content-editor {
margin-bottom: 14px;
}
.content-editor .panel-title {
margin-bottom: 8px;
}
.tag-grid { .tag-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;