优化创作模式表单界面

- 将创作表单面板的类名更改为 `creation-main-panel`,提升代码可读性。
- 新增表单折叠功能,允许用户展开或收起表单,改善用户体验。
- 调整表单项的显示逻辑,确保在没有配置时显示占位信息。
- 更新历史对话区域的样式和内容展示,增强界面友好性。
This commit is contained in:
2026-05-14 14:00:07 +08:00
parent caa5cc71c7
commit 6ac6cc4659

View File

@@ -188,51 +188,40 @@
<div class="main">
<!-- 创作模式动态表单 -->
<div v-show="isCreationMode" class="creation-mode-container">
<div class="panel creation-form-panel">
<div class="panel creation-main-panel">
<div class="creation-header">
<div>
<div class="title">{{ currentWorkflowForCreation?.flowName || '内容创作' }}</div>
<div class="sub">{{ currentWorkflowForCreation?.description || '填写表单参数进行内容创作' }}</div>
</div>
<div class="creation-header-actions">
<!-- <el-button type="warning" size="large" @click="showChatModelSelector = true">
<el-icon><Setting /></el-icon>
设置对话模型
</el-button> -->
<el-button @click="creationFormCollapsed = !creationFormCollapsed">
{{ creationFormCollapsed ? '展开表单' : '收起表单' }}
</el-button>
<el-button @click="backToCanvas">返回画布</el-button>
</div>
</div>
<div class="creation-form-scroll">
<el-form label-position="top" class="creation-form">
<template v-if="currentWorkflowForCreation?.nodeInputParams">
<div v-for="node in currentWorkflowForCreation.nodeInputParams" :key="node.id" class="node-form-wrapper">
<!-- 跳过开始节点显示其他所有节点 -->
<div v-if="node.nodeCode !== '__start__'" class="node-form-section">
<div class="node-form-title">
<el-icon class="node-icon"><Document /></el-icon>
<span>{{ node.name }}</span>
</div>
<div class="form-grid">
<!-- 节点基本信息始终显示 -->
<el-form-item label="节点类型" class="form-item-medium">
<el-input :model-value="node.nodeCode" disabled />
</el-form-item>
<el-form-item label="选择模型" class="form-item-medium" v-if="node.modelConfig?.modelName">
<el-input :model-value="node.modelConfig.modelName" disabled />
</el-form-item>
<el-form-item label="技能" class="form-item-medium" v-if="node.skillName">
<el-input :model-value="node.skillName" disabled />
</el-form-item>
<!-- 自定义表单字段 -->
<template v-if="node.formConfig && node.formConfig.length > 0">
<div
class="creation-middle"
:class="{ 'form-collapsed': creationFormCollapsed }"
:style="
creationFormCollapsed
? undefined
: { gridTemplateRows: `${formPanelHeightPercent}% 8px minmax(0, calc(100% - ${formPanelHeightPercent}% - 8px))` }
"
>
<div v-show="!creationFormCollapsed" class="creation-form-panel">
<div class="simple-form-scroll">
<el-form label-position="top" class="simple-creation-form">
<template v-if="currentWorkflowForCreation?.nodeInputParams">
<template v-for="node in currentWorkflowForCreation.nodeInputParams" :key="node.id">
<template v-if="node.nodeCode !== '__start__' && node.formConfig && node.formConfig.length > 0">
<el-form-item
v-for="field in node.formConfig"
:key="`${node.id}_${field.label}`"
:label="field.label"
:required="field.required"
:class="getFieldClass(field.type)"
>
<el-input
v-if="field.type === 'input'"
@@ -252,7 +241,7 @@
v-else-if="field.type === 'textarea'"
v-model="creationFormValues[`${node.id}_${field.label}`]"
type="textarea"
:rows="4"
:rows="3"
:placeholder="field.required ? '必填' : '选填'"
:disabled="isFromWorkspace"
show-word-limit
@@ -267,70 +256,40 @@
/>
</el-form-item>
</template>
</template>
</template>
<el-empty v-else description="暂无表单配置" :image-size="80" />
</el-form>
</div>
</div>
<!-- 其他配置字段排除已在 formConfig 中的字段 -->
<template v-if="node.config">
<el-form-item
v-for="(value, key) in node.config"
:key="`${node.id}_${key}`"
v-show="
!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'].includes(
String(key)
) && !(node.formConfig || []).some((f: any) => f.label === key || f.label === node.config.fieldMetadata?.[key]?.label)
"
:label="node.config.fieldMetadata?.[key]?.label || String(key)"
:required="node.config.fieldMetadata?.[key]?.required || false"
:class="getFieldClass(node.config.fieldMetadata?.[key]?.type || (typeof value === 'string' ? 'textarea' : 'number'))"
>
<el-input
v-if="typeof value === 'string'"
v-model="creationFormValues[`${node.id}_${key}`]"
:type="node.config.fieldMetadata?.[key]?.type === 'textarea' || String(value).length > 50 ? 'textarea' : 'text'"
:rows="4"
:placeholder="node.config.fieldMetadata?.[key]?.required ? '必填' : '选填'"
:disabled="isFromWorkspace"
clearable
:show-word-limit="node.config.fieldMetadata?.[key]?.type === 'textarea'"
:maxlength="500"
/>
<el-input-number
v-else-if="typeof value === 'number'"
v-model="creationFormValues[`${node.id}_${key}`]"
class="w100"
:controls="true"
:precision="2"
:step="0.1"
:disabled="isFromWorkspace"
/>
<el-switch
v-else-if="typeof value === 'boolean'"
v-model="creationFormValues[`${node.id}_${key}`]"
active-text="开启"
inactive-text="关闭"
:disabled="isFromWorkspace"
/>
</el-form-item>
</template>
</div>
</div>
<div v-show="!creationFormCollapsed" class="middle-splitter" @mousedown="handleMiddleSplitterMouseDown">
<div class="middle-splitter-line"></div>
</div>
<div class="panel creation-history-panel">
<div class="history-header">历史对话</div>
<div class="history-list-placeholder">
<div class="history-item assistant">
<div class="role">助手</div>
<div class="bubble">这里展示历史对话内容样式占位功能待定</div>
</div>
</template>
<el-empty v-else description="暂无表单配置" :image-size="100" />
</el-form>
<div class="history-item user">
<div class="role"></div>
<div class="bubble">收起上方表单后此区域可完整展示历史对话</div>
</div>
</div>
</div>
</div>
<!-- AI 创作输入区域 -->
<div class="creation-input-area">
<!-- 已选文件列表 -->
<div v-if="selectedFiles.length > 0" class="selected-files-top">
<el-tag v-for="(file, index) in selectedFiles" :key="index" closable @close="removeFile(index)" type="info" size="small">
{{ file.name }}
</el-tag>
</div>
<!-- 输入框容器 -->
<div class="chat-input-container">
<!-- 左侧工具按钮 -->
<div class="input-tools-left">
<el-upload :auto-upload="false" :show-file-list="false" :on-change="handleFileSelect" multiple>
<el-button text :icon="Paperclip" class="tool-btn" />
@@ -338,15 +297,12 @@
<el-button text :icon="MagicStick" @click="showCreationSkillSelector = true" class="tool-btn" />
</div>
<!-- 输入框 -->
<el-input v-model="userInput" placeholder="说点什么..." class="chat-input" @keydown.enter="sendMessage" />
<!-- 右侧发送/暂停按钮 -->
<el-button v-if="!isCreating" type="primary" :icon="Promotion" @click="sendMessage" class="send-btn" circle />
<el-button v-else type="danger" :icon="VideoPause" @click="stopExecution" class="send-btn" circle />
</div>
<!-- 已选技能标签 -->
<div v-if="selectedCreationSkill" class="selected-skill-bottom">
<el-tag type="success" closable @close="selectedCreationSkill = null" size="small"> 技能: {{ selectedCreationSkill.name }} </el-tag>
</div>
@@ -683,6 +639,9 @@ const showCreationSkillSelector = ref(false);
const currentSessionId = ref<string | null>(null); // 当前会话的 sessionId从工作空间进入时使用
const isFromWorkspace = ref(false); // 是否从工作空间进入创作模式
const isCreating = ref(false);
const creationFormCollapsed = ref(false);
const formPanelHeightPercent = ref(50);
const isDraggingMiddleSplitter = ref(false);
// 预览相关状态
const previewDialogVisible = ref(false);
const previewUrl = ref('');
@@ -1447,11 +1406,25 @@ const stopExecution = async () => {
ElMessage.error('终止执行失败');
}
};
// 判断节点是否有可见字段
const _hasVisibleFields = (node: any) => {
if (!node.config) return false;
const excludeKeys = ['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'];
return Object.keys(node.config).some((key) => !excludeKeys.includes(key));
const handleMiddleSplitterMouseDown = () => {
if (creationFormCollapsed.value) return;
isDraggingMiddleSplitter.value = true;
};
const handleGlobalMouseMove = (e: MouseEvent) => {
if (!isDraggingMiddleSplitter.value) return;
const middleEl = document.querySelector('.creation-middle') as HTMLElement | null;
if (!middleEl) return;
const rect = middleEl.getBoundingClientRect();
const y = e.clientY - rect.top;
const ratio = (y / rect.height) * 100;
formPanelHeightPercent.value = Math.min(75, Math.max(25, ratio));
};
const handleGlobalMouseUp = () => {
isDraggingMiddleSplitter.value = false;
};
// 根据字段类型返回CSS类名
const getFieldClass = (type: string) => {
@@ -2520,6 +2493,8 @@ onMounted(async () => {
initLogicFlow();
await getNodeLibrary();
await fetchWorkflowList();
window.addEventListener('mousemove', handleGlobalMouseMove);
window.addEventListener('mouseup', handleGlobalMouseUp);
// 获取当前用户角色
try {
@@ -2530,6 +2505,8 @@ onMounted(async () => {
}
});
onBeforeUnmount(() => {
window.removeEventListener('mousemove', handleGlobalMouseMove);
window.removeEventListener('mouseup', handleGlobalMouseUp);
logicFlowInstance.value?.destroy();
logicFlowInstance.value = null;
});
@@ -2672,20 +2649,74 @@ onBeforeUnmount(() => {
display: block;
}
.creation-mode-container {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 14px;
flex: 1;
min-height: 0;
height: 100%;
}
.creation-form-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.creation-form-panel.collapsed {
display: none;
}
.creation-history-panel {
display: flex;
flex-direction: column;
min-height: 0;
}
.history-header {
font-size: 16px;
font-weight: 700;
color: #1e293b;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 12px;
}
.history-list-placeholder {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
gap: 12px;
padding-right: 4px;
}
.history-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.history-item .role {
font-size: 12px;
color: #64748b;
}
.history-item .bubble {
max-width: 86%;
padding: 10px 12px;
border-radius: 12px;
line-height: 1.6;
font-size: 13px;
}
.history-item.assistant .bubble {
background: #f1f5f9;
color: #0f172a;
align-self: flex-start;
}
.history-item.user {
align-items: flex-end;
}
.history-item.user .bubble {
background: #3b82f6;
color: #fff;
}
.creation-form-panel.collapsed + .creation-history-panel {
grid-column: 1 / -1;
}
/* 画布模式:画布和侧边栏并排 */
.panel.canvas-panel {
flex: 1;
@@ -3040,6 +3071,9 @@ onBeforeUnmount(() => {
width: 100%;
flex-wrap: wrap;
}
.creation-middle {
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
}
.workflow-list-panel {
padding: 16px;
@@ -3170,15 +3204,111 @@ onBeforeUnmount(() => {
display: flex;
flex: 1;
min-height: 0;
height: 100%;
}
.creation-main-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
}
.creation-middle {
display: grid;
grid-template-rows: minmax(0, 1fr) 8px minmax(0, 1fr);
gap: 0;
flex: 1;
min-height: 0;
overflow: hidden;
}
.creation-middle.form-collapsed {
grid-template-rows: 1fr;
}
.middle-splitter {
display: flex;
align-items: center;
justify-content: center;
cursor: row-resize;
user-select: none;
}
.middle-splitter-line {
width: 100%;
height: 2px;
background: #dbe4ef;
border-radius: 999px;
}
.middle-splitter:hover .middle-splitter-line {
background: #8db4f7;
}
.creation-form-panel {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
max-width: 1200px;
margin: 0 auto;
width: 100%;
margin: 0;
max-width: none;
padding: 0;
box-shadow: none;
border: 1px solid #e5e7eb;
}
.simple-form-scroll {
flex: 1;
overflow: auto;
padding: 12px;
}
.simple-creation-form :deep(.el-form-item) {
margin-bottom: 12px;
}
.creation-history-panel {
display: flex;
flex-direction: column;
min-height: 0;
padding: 12px;
height: 100%;
}
.history-header {
font-size: 16px;
font-weight: 700;
color: #1e293b;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 10px;
}
.history-list-placeholder {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
gap: 12px;
padding-right: 4px;
}
.history-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.history-item .role {
font-size: 12px;
color: #64748b;
}
.history-item .bubble {
max-width: 86%;
padding: 10px 12px;
border-radius: 12px;
line-height: 1.6;
font-size: 13px;
}
.history-item.assistant .bubble {
background: #f1f5f9;
color: #0f172a;
align-self: flex-start;
}
.history-item.user {
align-items: flex-end;
}
.history-item.user .bubble {
background: #3b82f6;
color: #fff;
}
.creation-header {
display: flex;
@@ -3387,6 +3517,9 @@ onBeforeUnmount(() => {
width: 100%;
flex-wrap: wrap;
}
.creation-middle {
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
}
/* 左侧Tab面板样式 */