diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx
index 21b683f..90a452b 100644
--- a/src/pages/ReportEditor.tsx
+++ b/src/pages/ReportEditor.tsx
@@ -489,16 +489,24 @@ export default function ReportEditor() {
}, []);
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
+ const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
+ const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
placeholder.innerHTML = `
×
-
+
`;
placeholder.classList.add('has-image');
placeholder.style.border = 'none';
placeholder.style.background = 'transparent';
- placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
+ placeholder.style.height = 'auto';
placeholder.style.lineHeight = 'normal';
+ placeholder.style.maxWidth = mw;
+ placeholder.style.maxHeight = mh;
+ placeholder.style.textAlign = 'left';
+ placeholder.style.verticalAlign = 'top';
+ placeholder.style.justifyContent = 'flex-start';
+ placeholder.style.alignItems = 'flex-start';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
};
@@ -664,14 +672,20 @@ export default function ReportEditor() {
if (emptyPlaceholder) {
emptyPlaceholder.innerHTML = `
×
-
+
`;
emptyPlaceholder.classList.add('has-image');
emptyPlaceholder.style.border = 'none';
emptyPlaceholder.style.background = 'transparent';
- emptyPlaceholder.style.height = 'auto';
emptyPlaceholder.style.width = 'auto';
+ emptyPlaceholder.style.height = 'auto';
emptyPlaceholder.style.lineHeight = 'normal';
+ emptyPlaceholder.style.maxWidth = emptyPlaceholder.style.maxWidth || emptyPlaceholder.style.width || '200px';
+ emptyPlaceholder.style.maxHeight = emptyPlaceholder.style.maxHeight || emptyPlaceholder.style.height || '200px';
+ emptyPlaceholder.style.textAlign = 'left';
+ emptyPlaceholder.style.verticalAlign = 'top';
+ emptyPlaceholder.style.justifyContent = 'flex-start';
+ emptyPlaceholder.style.alignItems = 'flex-start';
contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
}
@@ -696,16 +710,24 @@ export default function ReportEditor() {
};
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
+ const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
+ const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
placeholder.innerHTML = `
×
-
+
`;
placeholder.classList.add('has-image');
placeholder.style.border = 'none';
placeholder.style.background = 'transparent';
- placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
+ placeholder.style.height = 'auto';
placeholder.style.lineHeight = 'normal';
+ placeholder.style.maxWidth = mw;
+ placeholder.style.maxHeight = mh;
+ placeholder.style.textAlign = 'left';
+ placeholder.style.verticalAlign = 'top';
+ placeholder.style.justifyContent = 'flex-start';
+ placeholder.style.alignItems = 'flex-start';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
};
@@ -1798,45 +1820,43 @@ export default function ReportEditor() {
onChange={handleVideoUpload}
/>
- {videos.length > 0 && (
-
-
-
@@ -1982,7 +2000,7 @@ export default function ReportEditor() {
html = `
×${hintText}
`;
} else {
let styleStr = 'display:inline-block;text-align:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;';
- styleStr += `width:${w}px;height:${h}px;line-height:${h}px;`;
+ styleStr += `width:${w}px;height:${h}px;max-width:${w}px;max-height:${h}px;line-height:${h}px;`;
const showShortText = w > 0 && w < 80;
const text = showShortText ? '插图' : hintText;
html = `
×${text}`;
diff --git a/src/pages/TemplateManage.tsx b/src/pages/TemplateManage.tsx
index bb61d03..37f7ba4 100644
--- a/src/pages/TemplateManage.tsx
+++ b/src/pages/TemplateManage.tsx
@@ -143,16 +143,24 @@ export default function TemplateManage() {
}, [currentUser]);
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
+ const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
+ const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
placeholder.innerHTML = `
×
-

+

`;
placeholder.classList.add('has-image');
placeholder.style.border = 'none';
placeholder.style.background = 'transparent';
- placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
+ placeholder.style.height = 'auto';
placeholder.style.lineHeight = 'normal';
+ placeholder.style.maxWidth = mw;
+ placeholder.style.maxHeight = mh;
+ placeholder.style.textAlign = 'left';
+ placeholder.style.verticalAlign = 'top';
+ placeholder.style.justifyContent = 'flex-start';
+ placeholder.style.alignItems = 'flex-start';
saveTemplateContent();
};
@@ -1359,7 +1367,7 @@ export default function TemplateManage() {
html = `
×${hintText}
`;
} else {
let styleStr = 'display:inline-block;text-align:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;';
- styleStr += `width:${w}px;height:${h}px;line-height:${h}px;`;
+ styleStr += `width:${w}px;height:${h}px;max-width:${w}px;max-height:${h}px;line-height:${h}px;`;
const showShortText = w > 0 && w < 80;
const text = showShortText ? '插图' : hintText;
html = `
×${text}`;
diff --git a/工程分析/实现方案-2026-04-18-19-23-31.md b/工程分析/实现方案-2026-04-18-19-23-31.md
new file mode 100644
index 0000000..18b3d42
--- /dev/null
+++ b/工程分析/实现方案-2026-04-18-19-23-31.md
@@ -0,0 +1,82 @@
+# 实现方案 —— 2026-04-18-19-23-31
+
+## 方案目标
+修复视频分析模块空白问题,重构图片占位符的填充后尺寸逻辑。
+
+## 需求 1:修复视频分析模块空白
+
+### 修改文件
+`src/pages/ReportEditor.tsx`
+
+### 修改内容
+将「上传视频」按钮和视频缩略图列表从 `videos.length > 0` 条件内部移出,使其始终渲染。仅保留视频播放器和关键帧网格在 `currentVideoIndex !== -1 && videos.length > 0` 条件下渲染。
+
+修改后结构:
+```tsx
+{activeTab === 'video' && (
+
+
+
+ {/* 始终可见:上传按钮 + 视频缩略图列表 */}
+
+ 上传视频
+ {videos.map(...)}
+
+
+ {/* 条件渲染:视频播放器和关键帧 */}
+ {currentVideoIndex !== -1 && videos.length > 0 && (
+
...
+ )}
+
+)}
+```
+
+## 需求 2:图片占位符尺寸自适应
+
+### 核心逻辑
+1. **插入占位符时**:在 `style` 中注入 `max-width` 和 `max-height`,与 `width`/`height` 相同,便于后续读取限制值。
+2. **填充图片时**:
+ - 读取占位符当前的 `max-width` / `max-height`(或回退到 `width` / `height`)
+ - 将这两个值赋给内部 `
![]()
` 的 `max-width` / `max-height`
+ - 设置 `object-fit: contain; object-position: left top`
+ - 将占位符外壳的 `width`、`height`、`line-height` 设为 `auto` / `normal`
+ - 保留 `max-width`、`max-height` 作为硬限制
+ - 设置 `text-align: left; vertical-align: top`
+
+### 修改文件及位置
+| 文件 | 函数/位置 | 修改内容 |
+|------|-----------|----------|
+| `src/pages/ReportEditor.tsx` | `fillPlaceholderSrc` | 填充后读取限制值,设置 img 和外壳样式 |
+| `src/pages/ReportEditor.tsx` | `fillPlaceholder` | 同上 |
+| `src/pages/ReportEditor.tsx` | `autoCaptureFrames` | 同上 |
+| `src/pages/ReportEditor.tsx` | placeholderModal 确认插入 | style 中增加 `max-width` / `max-height` |
+| `src/pages/TemplateManage.tsx` | `fillPlaceholderSrc` | 同上 |
+| `src/pages/TemplateManage.tsx` | placeholderModal 确认插入 | style 中增加 `max-width` / `max-height` |
+
+### 样式值示例
+```ts
+const mw = placeholder.style.maxWidth || placeholder.style.width || '200px';
+const mh = placeholder.style.maxHeight || placeholder.style.height || '200px';
+
+placeholder.innerHTML = `
+
×
+

+`;
+
+placeholder.style.width = 'auto';
+placeholder.style.height = 'auto';
+placeholder.style.maxWidth = mw;
+placeholder.style.maxHeight = mh;
+placeholder.style.lineHeight = 'normal';
+placeholder.style.textAlign = 'left';
+placeholder.style.verticalAlign = 'top';
+```
+
+## 需求 3:Logo 框大小保持 65px × 65px
+默认模板中 Logo 占位符的 `width:65px;height:65px` 保持不变。此需求通过不修改 Logo 占位符相关代码即可满足。
+
+## 风险与注意事项
+1. 视频按钮移出条件渲染后,需确保 `videoInputRef` 的引用始终有效。
+2. 占位符 `width:auto` 后,在表格单元格(`td`)内的表现需要验证,确保不会超出单元格。
+3. `object-position: left top` 仅在 `object-fit: contain` 时生效。
+4. 需确保 `max-width` / `max-height` 在打印样式中不会被 `@media print` 规则覆盖。
diff --git a/工程分析/测试方案-2026-04-18-19-23-31.md b/工程分析/测试方案-2026-04-18-19-23-31.md
new file mode 100644
index 0000000..a471edc
--- /dev/null
+++ b/工程分析/测试方案-2026-04-18-19-23-31.md
@@ -0,0 +1,54 @@
+# 测试方案 —— 2026-04-18-19-23-31
+
+## 测试目标
+验证视频分析模块空白修复和图片占位符自适应逻辑。
+
+## 测试用例
+
+### TC-1:视频分析模块无视频时显示上传按钮
+**前置条件**:新建报告,切换到「视频分析」Tab,尚未上传任何视频。
+**步骤**:
+1. 点击「视频分析」Tab。
+**预期结果**:
+- 面板显示「上传视频」按钮(缩小版,在水平滚动区域首位)。
+- 面板不显示视频播放器和关键帧区域。
+- 点击上传按钮可正常打开文件选择器。
+
+### TC-2:视频分析模块有视频时正常显示
+**前置条件**:已上传至少一个视频。
+**步骤**:
+1. 切换到「视频分析」Tab。
+**预期结果**:
+- 上传按钮和视频缩略图列表均可见。
+- 选中视频后,播放器和关键帧区域正常显示。
+
+### TC-3:图片占位符填充后尺寸自适应(小图片)
+**前置条件**:模板中有 200×200 的图片占位符,准备一张 100×80 的小图片。
+**步骤**:
+1. 将小图片插入占位符。
+**预期结果**:
+- 占位符宽度收缩为约 100px,高度收缩为约 80px。
+- 图片靠左上方放置,无多余空白。
+
+### TC-4:图片占位符填充后尺寸自适应(大图片)
+**前置条件**:模板中有 200×200 的图片占位符,准备一张 800×600 的大图片。
+**步骤**:
+1. 将大图片插入占位符。
+**预期结果**:
+- 图片等比例缩小,最大不超过 200×200。
+- 占位符宽度收缩为缩小后的图片宽度(≤200px),高度同理。
+- 图片靠左上方放置。
+
+### TC-5:Logo 占位符大小保持 65px × 65px
+**前置条件**:默认模板已加载。
+**步骤**:
+1. 检查顶部 Logo 占位符。
+**预期结果**:占位符尺寸为 65px × 65px,不受本次修改影响。
+
+## 回归测试
+- 确保视频播放、关键帧摘取、拖拽插入功能正常。
+- 确保 `template-manage` 中的图片占位符同样支持尺寸自适应。
+- 确保打印样式正常,图片不会被截断。
+
+## 测试通过标准
+所有用例均通过,无控制台报错,视频模块和图片占位符行为符合预期。
diff --git a/工程分析/需求分析-2026-04-18-19-23-31.md b/工程分析/需求分析-2026-04-18-19-23-31.md
new file mode 100644
index 0000000..62eab4d
--- /dev/null
+++ b/工程分析/需求分析-2026-04-18-19-23-31.md
@@ -0,0 +1,30 @@
+# 需求分析 —— 2026-04-18-19-23-31
+
+## 需求来源
+用户在实际使用中发现两个问题,要求进行修复和优化。
+
+## 需求概述
+
+### 需求 1:修复视频分析模块空白问题
+在 `ReportEditor` 中,上一轮修改将「上传视频」按钮移入了 `videos.length > 0` 的条件渲染内部,导致当没有视频时,整个「视频分析」面板变为空白,用户无法上传第一个视频。
+
+**预期行为**:无论是否有已上传视频,「上传视频」按钮和缩略图滚动列表都应始终可见。
+
+### 需求 2:图片占位符尺寸自适应与等比例缩放限制
+当前图片占位符填充图片后,虽然高度变为 `auto`,但宽度仍保持预设值(如 200px),导致图片在占位符内居中显示,周围仍有大量空白。用户希望:
+- 预设的宽高仅作为**最大限制**(`max-width` / `max-height`)
+- 如果图片超出限制,则等比例缩小
+- 图片靠左上方放置(`object-position: left top`)
+- 占位符自身的虚线框大小要**紧缩包围(shrink-wrap)**成图片实际缩放后的尺寸
+
+### 需求 3:Logo 框大小保持 65px × 65px
+默认模板中顶部医院 Logo 占位符的尺寸应保持 65px × 65px 不变。
+
+## 涉及文件
+- `src/pages/ReportEditor.tsx`(需求 1、2)
+- `src/pages/TemplateManage.tsx`(需求 2)
+
+## 需求影响范围
+- 视频分析面板的可见性逻辑
+- 图片占位符的填充后样式行为
+- 打印/预览时的图片尺寸表现