修复:贴片模板统一走CDN参考(默认模板与下载、移除本地模板文件)、首页新建会话首次执行后贴片模板回显空白

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-27 11:54:50 +08:00
co-authored by Claude
parent ef4976ee03
commit 0ec961e210
3 changed files with 34 additions and 222 deletions
-213
View File
@@ -1,213 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* ========== 全局重置 ========== */
/* 去掉所有元素的自带边距和内边距,让布局从零开始 */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ========== 画布(视频输出尺寸) ========== */
/* 这是最终视频的分辨率:宽1080px × 高1920px(竖屏 9:16,手机全屏) */
/* 如果你想改输出尺寸,改这里的 width 和 height 就行 */
body {
width: 1080px; /* 画布宽度 ← 改成 720 就是 720×1280 */
height: 1920px; /* 画布高度 ← 改成 1280 就是 720p */
overflow: hidden; /* 超出画布的部分裁掉,不显示滚动条 */
background: #000; /* 画布背景色(黑色),视频没铺满时露出的颜色 */
}
/* ========== 所有元素的定位基础 ========== */
/* 给所有带 clip 类的元素开启绝对定位,可以用 left/top 精确控制位置 */
.clip {
position: absolute; /* 绝对定位,相对于父容器(body)定位 */
}
/* ========== 文字元素的通用样式 ========== */
.text-element {
font-family: Arial, Helvetica, sans-serif; /* 字体:首选Arial,不行就Helvetica,再不行用系统无衬线体 */
text-align: center; /* 文字水平居中 */
display: flex; /* 启用弹性盒子布局 */
align-items: center; /* 垂直居中(配合flex使用) */
justify-content: center; /* 水平居中(配合flex使用) */
white-space: pre-wrap; /* 保留换行,文字超出宽度自动换行 */
word-break: break-word; /* 长单词或URL撑破容器时强制换行 */
}
/* ========== 入场动画定义 ========== */
/* 想改动画时长,改后面的秒数(如 0.5s → 1s 就是慢一倍) */
/* 淡入:从完全透明 → 完全可见 */
@keyframes fadeIn {
from { opacity: 0; } /* 开始:透明(完全看不见) */
to { opacity: 1; } /* 结束:不透明(完全可见) */
}
/* 上滑:从下方40px处边向上移动边显现 */
@keyframes slideUp {
from { opacity: 0; transform: translateY(40px); } /* 开始:透明 + 在下方40px */
to { opacity: 1; transform: translateY(0); } /* 结束:可见 + 回到原位 */
}
/* 左滑:从右侧40px处边向左移动边显现(视觉上像从右边滑入) */
@keyframes slideLeft {
from { opacity: 0; transform: translateX(40px); } /* 开始:透明 + 在右边40px */
to { opacity: 1; transform: translateX(0); } /* 结束:可见 + 回到原位 */
}
/* 放大:从缩小到80%边放大边显现 */
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.8); } /* 开始:透明 + 缩到80%大小 */
to { opacity: 1; transform: scale(1); } /* 结束:可见 + 100%正常大小 */
}
/* 脉冲:一直循环缩放,像呼吸一样忽大忽小(一般用于强调/吸引注意) */
@keyframes pulse {
0% { transform: scale(1); } /* 开始:正常大小 */
50% { transform: scale(1.05); } /* 中间:放大到1.05倍(105%*/
100% { transform: scale(1); } /* 结束:回到正常大小 */
}
/* ========== 动画类名 ========== */
/* 给元素加上 class="anim-xxx" 就能用上面的动画,用法看下面的HTML元素 */
/* 淡入 0.5秒,先快后慢 */
.anim-fadeIn { animation: fadeIn 0.5s ease-out; }
/* 上滑 0.6秒,先快后慢 */
.anim-slideUp { animation: slideUp 0.6s ease-out; }
/* 左滑 0.6秒,先快后慢 */
.anim-slideLeft { animation: slideLeft 0.6s ease-out; }
/* 放大入场 0.5秒,先快后慢 */
.anim-scaleIn { animation: scaleIn 0.5s ease-out; }
/* 脉冲 1.5秒一个循环,一直重复(infinite) */
.anim-pulse { animation: pulse 1.5s ease-in-out infinite; }
/* ── 动画参数怎么改? ── */
/* • 0.5s → 改成 1s 动画就慢一倍,改 0.2s 就快一倍 */
/* • ease-out → 开始快结尾慢,换成 linear 就是匀速,换成 ease-in 就是开始慢结尾快 */
/* • infinite → 一直重复,去掉就不重复只播一次 */
</style>
<!-- GSAP:专业动画引擎,用于精确控制时间轴(当前脚本只用了初始化,没做复杂动画) -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
</head>
<body>
<!-- ====================================================================== -->
<!-- 舞台容器:所有元素都放在这里面 -->
<!-- data-composition-id 是内部标识,不要改 -->
<!-- data-width / data-height 需要和上面的 body 宽高一致 -->
<!-- ====================================================================== -->
<div id="stage" data-composition-id="main" data-start="0" data-width="1080" data-height="1920">
<!-- ================================================================== -->
<!-- data-key 属性:这个元素的内容会被程序自动替换 -->
<!-- 比如 data-key="title",程序就会用"标题"对应的文字替换掉这个元素的内容 -->
<!-- 所以里面的"标题文字"只是占位,实际显示什么由数据决定 -->
<!-- ================================================================== -->
<!-- ======================================================== -->
<!-- 标题:红色大标题,靠上居中 -->
<!-- ======================================================== -->
<!--
data-key="title" → 程序会用标题数据替换此元素内容
data-start="0" → 第0秒开始显示(单位:秒)
data-duration="58.000" → 持续显示58秒
class="clip" → 绝对定位,可以用 left/top 控制位置
class="text-element" → 文字居中、自动换行
class="anim-slideUp" → 入场动画:从下方滑入,0.6秒
-->
<div data-key="title" data-start="0" data-duration="58.000" data-track-index="1"
class="clip text-element anim-slideUp"
style="
left: 50%; /* 水平居中定位(配合 translateX 使用) */
top: 70px; /* 距离顶部70px ← 改这个数字上下移动 */
transform: translateX(-50%); /* 把元素向左拉回自身宽度的一半,实现真居中 */
width: 972px; /* 文字区域宽度 ← 改窄一点文字就缩短 */
max-width: 972px; /* 最大宽度,防止被撑破 */
font-size: 48px; /* 字号 ← 改大/改小 */
color: #D82828; /* 文字颜色(红色)← 改成 #FFFFFF 就是白色 */
">标题文字</div>
<!-- ======================================================== -->
<!-- 产品图片:左上角位置,图片链接由程序自动替换 -->
<!-- ======================================================== -->
<!--
这是 <img> 标签,src 属性会被程序替换成真实的图片 URL
data-key="product_image" → 程序用产品图片的URL替换 src
-->
<img data-key="product_image" data-start="0" data-duration="58.000" data-track-index="2"
class="clip anim-fadeIn"
style="
width: 200px; /* 图片宽度 ← 改这个调大小 */
height: 200px; /* 图片高度 ← 改这个调大小 */
left: 0px; /* 距离左边0px(贴左边缘)← 改大往右移 */
top: 200px; /* 距离顶部200px ← 改大往下移 */
"
src="占位图片.jpg" />
<!-- ======================================================== -->
<!-- 产品卖点描述:居中偏下位置 -->
<!-- ======================================================== -->
<div data-key="product_desc" data-start="0" data-duration="58.000" data-track-index="3"
class="clip text-element"
style="
left: 50%; /* 水平居中 */
top: 830px; /* 距离顶部830px ← 改大就往下移 */
transform: translateX(-50%); /* 水平居中补偿 */
width: 972px; /* 文本宽度 */
max-width: 972px; /* 最大宽度 */
font-size: 34px; /* 字号 ← 改大/改小 */
color: #333333; /* 文字颜色(深灰色)← 改成 #000 就是黑色 */
">产品卖点描述</div>
<!-- ======================================================== -->
<!-- 底部免责声明:背景半透明黑底白字,圆角,贴底部 -->
<!-- ======================================================== -->
<div data-key="disclaimer" data-start="0" data-duration="58.000" data-track-index="5"
class="clip text-element"
style="
left: 50%; /* 水平居中 */
top: calc(100% - 45px); /* 距离底部45px(100%是父容器高度)← 改45调距离 */
transform: translateX(-50%); /* 水平居中补偿 */
width: 972px; /* 文本宽度 */
max-width: 972px;
font-size: 20px; /* 字号 */
color: #FFFFFF; /* 文字颜色(白色) */
">
<!--
内部 span:负责背景和间距
background: rgba(0,0,0,0.70) → 黑色半透明背景,70%不透明度
← 改最后一个数字(0~1)调透明度:0=全透,1=全黑
padding: 12px 24px → 内边距:上下12px,左右24px
← 改这个让背景范围变大/变小
border-radius: 8px → 圆角大小
← 改大更圆(如 20px),改 0 就是直角
-->
<span style="background:rgba(0,0,0,0.70); padding:12px 24px; border-radius:8px;">
免责声明文字
</span>
</div>
</div>
<!-- ================================================================ -->
<!-- GSAP 时间轴初始化脚本 -->
<!-- 当前只初始化了一个空时间轴,给程序框架用,一般不需要改 -->
<!-- ================================================================ -->
<script>
(function() {
var tl = gsap.timeline({ paused: true }); // 建一个暂停的时间轴
tl.to('#stage', { duration: 0, opacity: 1 }, 0); // 舞台立刻显示(第0秒)
window.__timelines = window.__timelines || {}; // 存到全局,供程序调用
window.__timelines['main'] = tl; // 时间轴名字叫 'main'
})();
</script>
</body>
</html>
@@ -166,7 +166,7 @@ function nextImageKey(template: PatchTemplate): string | null {
return null;
}
const DEFAULT_TEMPLATE_URL = 'https://cdn.redpowerfuture.com/tenantid-1/template_example1.html';
const DEFAULT_TEMPLATE_URL = 'https://cdn.redpowerfuture.com/tenantid-1/vertical_template_example.html';
const props = defineProps<{
modelValue: PatchTemplate[];
@@ -290,14 +290,24 @@ const handleImageUpload = async (templateIndex: number, imgIdx: number, file: an
}
};
// 下载参考模板文件
const handleDownload = (_e?: Event) => {
const a = document.createElement('a');
a.href = '/template_example.html';
a.download = 'template_example.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// 下载参考模板文件:统一走 CDN,转 blob 触发下载(避免跨域 <a download> 直接打开页面)
const handleDownload = async (_e?: Event) => {
try {
const res = await fetch(DEFAULT_TEMPLATE_URL);
if (!res.ok) throw new Error('参考模板获取失败');
const blob = await res.blob();
const fileName = decodeURIComponent(DEFAULT_TEMPLATE_URL.split('/').pop() || 'vertical_template_example.html');
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (error: any) {
ElMessage.error(error?.message || '参考模板下载失败');
}
};
</script>
+15
View File
@@ -1234,6 +1234,17 @@ const snapshotFormValuesToMsg = (msg: ChatMessage, formValues: Record<string, an
});
};
// 提交时把用户填的贴片模板写回消息对象 patchLayout 节点(与表单值快照同理):
// 会话认领(virtual→UUID 触发 MainContent 重建)/ 历史回显重挂载时,WorkflowFormCard 从 msg.form
// 恢复贴片模板能取到本次填写值,避免新建会话首次执行后展开贴片模板回显空白
const snapshotTemplatesToMsg = (msg: ChatMessage, templates: any[]) => {
const nodes = msg.form?.nodeInputParams;
if (!Array.isArray(nodes)) return;
nodes.forEach((n: any) => {
if (n.patchLayout === true) n.templates = templates;
});
};
const startWorkflowFromCard = async (
msg: ChatMessage,
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] }
@@ -1254,6 +1265,10 @@ const startWorkflowFromCard = async (
// 会话认领(virtual→UUID 触发 MainContent 重建)/ 历史回显重挂载时能从消息对象恢复已填值,
// 避免执行后表单被清空
snapshotFormValuesToMsg(msg, payload.formValues);
// 贴片模板同样写回消息对象 patchLayout 节点(保持与后端执行副本一致),重建/重挂载后能恢复回显
if (Array.isArray(payload.templates)) {
snapshotTemplatesToMsg(msg, payload.templates);
}
// 标记本轮发送中:工作流执行期间 InputBar 显示停止按钮(isGenerating 依赖 sendingSessions
sendingSessions[sid] = true;