2026-04-18-19-08-43 - 六项UI优化:基础字段无下划线、field-value联动高亮、视频按钮整合、视频间距紧凑、签名空行、图片占位符自适应高度

This commit is contained in:
Administrator
2026-04-18 19:14:23 +08:00
parent 5f4ae1ff29
commit 4a7051b6db
6 changed files with 242 additions and 23 deletions

View File

@@ -54,6 +54,7 @@ export default function ReportEditor() {
const prevVideoCountRef = useRef(0); const prevVideoCountRef = useRef(0);
const [activeTab, setActiveTab] = useState<'info' | 'video'>('info'); const [activeTab, setActiveTab] = useState<'info' | 'video'>('info');
const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);
const [multiSelectOptions, setMultiSelectOptions] = useState<Record<string, string[]>>({ const [multiSelectOptions, setMultiSelectOptions] = useState<Record<string, string[]>>({
surgeon: ['张医生', '李医生', '王医生'], surgeon: ['张医生', '李医生', '王医生'],
assistant: ['赵医生', '钱医生', '孙医生'], assistant: ['赵医生', '钱医生', '孙医生'],
@@ -378,13 +379,14 @@ export default function ReportEditor() {
const targetEl = node as HTMLElement | null; const targetEl = node as HTMLElement | null;
if (!targetEl) return; if (!targetEl) return;
// Handle click on field-value: switch to info tab and focus corresponding input // Handle click on field-value: switch to info tab, highlight and focus corresponding input
const fieldValue = targetEl.closest('.field-value') as HTMLElement | null; const fieldValue = targetEl.closest('.field-value') as HTMLElement | null;
if (fieldValue) { if (fieldValue) {
const bindKey = fieldValue.getAttribute('data-bind'); const bindKey = fieldValue.getAttribute('data-bind');
if (bindKey) { if (bindKey) {
setActiveTab('info'); setActiveTab('info');
stateRef.current = { ...stateRef.current, activeTab: 'info' }; stateRef.current = { ...stateRef.current, activeTab: 'info' };
setActiveFieldKey(bindKey);
setTimeout(() => { setTimeout(() => {
const inputEl = document.getElementById(`input-${bindKey}`); const inputEl = document.getElementById(`input-${bindKey}`);
if (inputEl) { if (inputEl) {
@@ -489,11 +491,14 @@ export default function ReportEditor() {
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => { const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
placeholder.innerHTML = ` placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span> <span class="delete-btn" contenteditable="false">×</span>
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false"> <img src="${src}" style="max-width:100%;height:auto;display:block;margin:0 auto;" draggable="false">
`; `;
placeholder.classList.add('has-image'); placeholder.classList.add('has-image');
placeholder.style.border = 'none'; placeholder.style.border = 'none';
placeholder.style.background = 'transparent'; placeholder.style.background = 'transparent';
placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
placeholder.style.lineHeight = 'normal';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML; if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage(); saveDraftToStorage();
}; };
@@ -664,6 +669,9 @@ export default function ReportEditor() {
emptyPlaceholder.classList.add('has-image'); emptyPlaceholder.classList.add('has-image');
emptyPlaceholder.style.border = 'none'; emptyPlaceholder.style.border = 'none';
emptyPlaceholder.style.background = 'transparent'; emptyPlaceholder.style.background = 'transparent';
emptyPlaceholder.style.height = 'auto';
emptyPlaceholder.style.width = 'auto';
emptyPlaceholder.style.lineHeight = 'normal';
contentRef.current = editorRef.current.innerHTML; contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage(); saveDraftToStorage();
} }
@@ -690,11 +698,14 @@ export default function ReportEditor() {
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => { const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
placeholder.innerHTML = ` placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span> <span class="delete-btn" contenteditable="false">×</span>
<img src="${frame.dataUrl}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false"> <img src="${frame.dataUrl}" style="max-width:100%;height:auto;display:block;margin:0 auto;" draggable="false">
`; `;
placeholder.classList.add('has-image'); placeholder.classList.add('has-image');
placeholder.style.border = 'none'; placeholder.style.border = 'none';
placeholder.style.background = 'transparent'; placeholder.style.background = 'transparent';
placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
placeholder.style.lineHeight = 'normal';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML; if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage(); saveDraftToStorage();
}; };
@@ -1453,7 +1464,7 @@ export default function ReportEditor() {
if (field.type === 'text' || field.type === 'date') { if (field.type === 'text' || field.type === 'date') {
const inputType = field.type === 'date' ? 'date' : 'text'; const inputType = field.type === 'date' ? 'date' : 'text';
return ( return (
<div key={field.key} id={`input-${field.key}`} className={field.category === '填空' && formFields.filter(f2 => f2.visibleInForm && f2.type === 'text' && f2.isSystemLocked).length > 1 && (field.key === 'patientName' || field.key === 'hospitalId') ? 'flex-1 space-y-1' : 'space-y-1'}> <div key={field.key} id={`input-${field.key}`} className={`${field.category === '填空' && formFields.filter(f2 => f2.visibleInForm && f2.type === 'text' && f2.isSystemLocked).length > 1 && (field.key === 'patientName' || field.key === 'hospitalId') ? 'flex-1 space-y-1' : 'space-y-1'} p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
<label className="block text-xs font-bold text-text-main"> <label className="block text-xs font-bold text-text-main">
{field.label} {isRequired && <span className="text-red-500">*</span>} {field.label} {isRequired && <span className="text-red-500">*</span>}
</label> </label>
@@ -1473,7 +1484,7 @@ export default function ReportEditor() {
const isOpen = openDropdown === field.key; const isOpen = openDropdown === field.key;
const opts = field.options || (field.key === 'anesthesiaType' ? anesthesiaOptions : []); const opts = field.options || (field.key === 'anesthesiaType' ? anesthesiaOptions : []);
return ( return (
<div key={field.key} id={`input-${field.key}`} className="space-y-1 select-dropdown-root relative"> <div key={field.key} id={`input-${field.key}`} className={`space-y-1 select-dropdown-root relative p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
<label className="block text-xs font-bold text-text-main">{field.label}</label> <label className="block text-xs font-bold text-text-main">{field.label}</label>
<div <div
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text" className="w-full px-3 py-2 border border-border rounded-lg bg-white flex items-center min-h-[42px] cursor-text"
@@ -1582,7 +1593,7 @@ export default function ReportEditor() {
const currentInputText = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText; const currentInputText = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText;
return ( return (
<div key={field.key} id={`input-${field.key}`} className="space-y-1 select-dropdown-root relative"> <div key={field.key} id={`input-${field.key}`} className={`space-y-1 select-dropdown-root relative p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
<label className="block text-xs font-bold text-text-main">{field.label}</label> <label className="block text-xs font-bold text-text-main">{field.label}</label>
<div <div
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text" className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text"
@@ -1663,7 +1674,7 @@ export default function ReportEditor() {
const { h: h12, isPM } = from24h(h24val); const { h: h12, isPM } = from24h(h24val);
return ( return (
<div key={field.key} id={`input-${field.key}`} className="space-y-1"> <div key={field.key} id={`input-${field.key}`} className={`space-y-1 p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}>
<label className="block text-xs font-bold text-text-main">{field.label}</label> <label className="block text-xs font-bold text-text-main">{field.label}</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<select <select
@@ -1777,7 +1788,7 @@ export default function ReportEditor() {
)} )}
{activeTab === 'video' && ( {activeTab === 'video' && (
<div className="space-y-4"> <div className="space-y-2">
<input <input
ref={videoInputRef} ref={videoInputRef}
type="file" type="file"
@@ -1786,20 +1797,17 @@ export default function ReportEditor() {
className="hidden" className="hidden"
onChange={handleVideoUpload} onChange={handleVideoUpload}
/> />
<button
onClick={() => videoInputRef.current?.click()}
className="w-full flex items-center justify-center gap-2 p-3 border border-dashed border-border rounded-lg hover:border-accent hover:bg-slate-50 transition-all"
>
<Video size={18} />
<div className="text-left">
<p className="text-xs font-bold text-text-main"></p>
<p className="text-[10px] text-text-muted"> MP4, MOV </p>
</div>
</button>
{videos.length > 0 && ( {videos.length > 0 && (
<div className="space-y-4"> <div className="space-y-2">
<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar"> <div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar">
<button
onClick={() => videoInputRef.current?.click()}
className="shrink-0 w-24 h-[68px] flex flex-col items-center justify-center gap-1 border-2 border-dashed border-border rounded-xl hover:border-accent hover:bg-slate-50 transition-all text-text-muted hover:text-accent"
>
<Video size={18} />
<span className="text-[10px] font-bold"></span>
</button>
{videos.map((v, i) => ( {videos.map((v, i) => (
<div <div
key={v.id} key={v.id}
@@ -1828,7 +1836,7 @@ export default function ReportEditor() {
</div> </div>
{currentVideoIndex !== -1 && ( {currentVideoIndex !== -1 && (
<div className="space-y-4"> <div className="space-y-2">
<div className="relative bg-slate-900 rounded-2xl overflow-hidden aspect-video shadow-lg"> <div className="relative bg-slate-900 rounded-2xl overflow-hidden aspect-video shadow-lg">
<video <video
ref={videoRef} ref={videoRef}
@@ -1866,7 +1874,7 @@ export default function ReportEditor() {
</div> </div>
</div> </div>
<div className="flex justify-between items-center pt-2"> <div className="flex justify-between items-center pt-1 border-t border-border">
<span className="text-[10px] font-bold text-text-main uppercase tracking-wider"></span> <span className="text-[10px] font-bold text-text-main uppercase tracking-wider"></span>
<button <button
onClick={captureFrame} onClick={captureFrame}

View File

@@ -145,11 +145,14 @@ export default function TemplateManage() {
const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => { const fillPlaceholderSrc = (placeholder: HTMLElement, src: string) => {
placeholder.innerHTML = ` placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span> <span class="delete-btn" contenteditable="false">×</span>
<img src="${src}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false"> <img src="${src}" style="max-width:100%;height:auto;display:block;margin:0 auto;" draggable="false">
`; `;
placeholder.classList.add('has-image'); placeholder.classList.add('has-image');
placeholder.style.border = 'none'; placeholder.style.border = 'none';
placeholder.style.background = 'transparent'; placeholder.style.background = 'transparent';
placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
placeholder.style.lineHeight = 'normal';
saveTemplateContent(); saveTemplateContent();
}; };

View File

@@ -1,4 +1,8 @@
const smartField = (key: string) => `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>&#8203;`; const noUnderlineKeys = ['patientName', 'patientGender', 'patientAge', 'department', 'bedNumber', 'hospitalId'];
const smartField = (key: string) => {
const noUlClass = noUnderlineKeys.includes(key) ? ' no-underline' : '';
return `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value${noUlClass}" data-bind="${key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>&#8203;`;
};
export const defaultReportContent = ` export const defaultReportContent = `
<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 4px;"> <div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 4px;">
@@ -144,6 +148,8 @@ export const defaultReportContent = `
手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-block;text-align:center;width:200px;height:40px;line-height:40px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;display:inline-block;vertical-align:middle;line-height:normal;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span></span> 手术者签名:<span class="image-placeholder" data-placeholder="true" contenteditable="false" data-mode="manual" style="display:inline-block;text-align:center;width:200px;height:40px;line-height:40px;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;position:relative;"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;display:inline-block;vertical-align:middle;line-height:normal;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">插入/点击放置图片</span></span>
</p> </p>
<p style="margin: 0; padding: 0; line-height: 1.5;">&nbsp;</p>
<p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0;"> <p style="text-align: right; font-family: SimSun; line-height: 1.5; margin: 0; padding: 0;">
${smartField('reportDate')} ${smartField('reportDate')}
</p> </p>

View File

@@ -0,0 +1,97 @@
# 实现方案 —— 2026-04-18-19-08-43
## 方案目标
优化编辑器交互体验和模板排版细节,提升视频面板空间利用率和图片占位符自适应能力。
## 需求 1基础信息字段默认无下划线
### 修改文件
`src/utils/defaultContent.ts`
### 修改内容
修改 `smartField()` 函数,对 6 个基础字段自动附加 `.no-underline` 类:
```ts
const noUnderlineKeys = ['patientName', 'patientGender', 'patientAge', 'department', 'bedNumber', 'hospitalId'];
const noUlClass = noUnderlineKeys.includes(key) ? ' no-underline' : '';
```
在生成的 HTML 中,`.field-value` 的 class 变为 `field-value${noUlClass}`
## 需求 2字段联动高亮并居中滚动
### 修改文件
`src/pages/ReportEditor.tsx`
### 修改内容
1. **新增状态**`const [activeFieldKey, setActiveFieldKey] = useState<string | null>(null);`
2. **修改点击处理**:在 `handleEditorClick``.field-value` 点击分支中,增加 `setActiveFieldKey(bindKey)`
3. **修改滚动逻辑**:将 `scrollIntoView``block``'center'` 改为更精确的控制(`block: 'center'` 本身就是居中,满足 1/3~2/3 需求)。
4. **高亮样式**:在右侧表单渲染中,为每个字段容器 `div` 增加动态类名:
```tsx
className={`space-y-1 p-2 -mx-2 rounded-xl transition-all duration-300 ${activeFieldKey === field.key ? 'bg-blue-50 ring-1 ring-accent shadow-sm' : ''}`}
```
## 需求 3视频上传按钮整合进缩略图列表
### 修改文件
`src/pages/ReportEditor.tsx`
### 修改内容
1. 删除原本独立的「上传视频」大按钮区域。
2. 在 `videos.map()` 所在的滚动容器 `<div className="flex gap-2 overflow-x-auto pb-2 no-scrollbar">` 的第一个位置,插入缩小版的上传按钮:
```tsx
<button
onClick={() => videoInputRef.current?.click()}
className="shrink-0 w-24 h-[68px] flex flex-col items-center justify-center gap-1 border-2 border-dashed border-border rounded-xl hover:border-accent hover:bg-slate-50 transition-all text-text-muted hover:text-accent"
>
<Video size={18} />
<span className="text-[10px] font-bold">上传视频</span>
</button>
```
## 需求 4视频模块间距紧凑化
### 修改文件
`src/pages/ReportEditor.tsx`
### 修改内容
1. 最外层容器从 `space-y-4` 改为 `space-y-2`。
2. 视频播放器与控制按钮之间从 `space-y-4` 改为 `space-y-2`。
3. 控制按钮区域(播放/暂停/进度条等)的 `gap` 或 `margin` 适当缩减。
4. 「关键帧摘取」标题区域的 `padding-top` 缩减,可增加 `border-t` 作为视觉分隔。
## 需求 5签名与日期之间增加空行
### 修改文件
`src/utils/defaultContent.ts`
### 修改内容
在「手术者签名」`<p>` 和「撰写时间」`<p>` 之间插入:
```html
<p style="margin: 0; padding: 0; line-height: 1.5;">&nbsp;</p>
```
## 需求 6图片占位符填充后高度自适应
### 修改文件
`src/pages/ReportEditor.tsx` 和 `src/pages/TemplateManage.tsx`
### 修改内容
在所有填充图片的逻辑中(`fillPlaceholderSrc`、`handleDrop`、`autoCaptureFrames` 等),在 `placeholder.classList.add('has-image')` 之后,增加:
```ts
placeholder.style.height = 'auto';
placeholder.style.width = 'auto';
placeholder.style.lineHeight = 'normal';
```
## 涉及文件及修改点
| 文件 | 修改点 |
|------|--------|
| `src/utils/defaultContent.ts` | `smartField()` 注入 `.no-underline`;签名与日期之间插入空行 |
| `src/pages/ReportEditor.tsx` | `activeFieldKey` 状态 + 高亮样式;视频上传按钮整合;视频面板间距缩减;占位符自适应样式 |
| `src/pages/TemplateManage.tsx` | 占位符自适应样式 |
## 风险与注意事项
1. `smartField()` 中硬编码的 6 个 key 需与 `DEFAULT_FORM_FIELDS` 严格一致。
2. `activeFieldKey` 高亮样式使用 `transition-all duration-300`,需确保不会与现有样式冲突。
3. 视频上传按钮移入缩略图列表后,需确保 `videoInputRef` 的点击触发逻辑不受影响。
4. 占位符 `height: auto` 后需验证图片在表格内table cell和正文中的显示是否正常。

View File

@@ -0,0 +1,64 @@
# 测试方案 —— 2026-04-18-19-08-43
## 测试目标
验证六项需求修改的正确性和稳定性。
## 测试用例
### TC-1基础信息字段打印无下划线
**前置条件**:新建报告,默认模板已加载。
**步骤**
1. 点击「打印预览」或「下载 PDF」。
2. 检查「姓名、性别、年龄、科别、床号、住院号」区域。
**预期结果**:这 6 个字段不显示下划线,其他字段(如手术名称、诊断等)正常显示下划线。
### TC-2点击 field-value 联动高亮并居中滚动
**前置条件**:编辑器已加载默认模板,右侧基本信息 Tab 可见。
**步骤**
1. 点击正文中任意 `class="field-value"`(如「手术名称」)。
2. 观察右侧对应输入框。
**预期结果**
- 对应输入框出现蓝色背景高亮(`bg-blue-50 ring-1 ring-accent`)。
- 页面平滑滚动,使该输入框位于可视区域中部。
- 输入框获得焦点。
### TC-3视频上传按钮整合进缩略图列表
**前置条件**:已上传至少一个视频。
**步骤**
1. 切换到「视频分析」Tab。
2. 观察视频缩略图列表。
**预期结果**
- 列表第一个位置是一个缩小版「上传视频」按钮,尺寸与视频卡片一致(约 `w-24`)。
- 点击该按钮能正常打开文件选择器。
- 原本独立的「点击上传手术视频」大按钮已消失。
### TC-4视频模块间距紧凑化
**前置条件**:视频分析面板展开,有视频和关键帧。
**步骤**
1. 观察缩略图列表与播放器之间的间距。
2. 观察播放器与控制按钮之间的间距。
3. 观察控制按钮与「关键帧摘取」标题之间的间距。
**预期结果**:各项间距明显缩小,下方关键帧列表获得更多展示空间。
### TC-5签名与日期之间增加空行
**前置条件**:默认模板已加载。
**步骤**
1. 滚动到模板底部,查看「手术者签名」与「撰写时间」之间。
**预期结果**:两者之间有一个空行(约一行高度的空白)。
### TC-6图片占位符填充后高度自适应
**前置条件**:模板中有空图片占位符,有较小的图片(高度 < 200px
**步骤**
1. 将图片插入占位符(通过上传、拖拽或自动摘取)。
2. 观察占位符区域。
**预期结果**
- 占位符高度随图片实际尺寸自适应,不再保留 200px 固定高度。
- 图片下方不会出现大片空白。
## 回归测试
- 确保打印功能PDF 导出)正常工作。
- 确保视频播放、关键帧摘取、拖拽插入功能正常。
- 确保 `template-manage` 中的图片占位符同样支持高度自适应。
## 测试通过标准
所有用例均通过,无控制台报错,打印样式正常。

View File

@@ -0,0 +1,41 @@
# 需求分析 —— 2026-04-18-19-08-43
## 需求来源
用户基于实际使用体验,提出六项界面交互和排版优化需求。
## 需求概述
### 需求 1基础信息字段默认无下划线
默认模板中的「姓名、性别、年龄、科别、床号、住院号」6 个基础信息字段,在打印时默认不显示下划线。通过在 `smartField()` 函数中根据 key 自动注入 `.no-underline` 类实现。
### 需求 2点击 field-value 联动高亮并居中滚动
`report-editor` 中点击正文 `class="field-value"` 时:
- 右侧基本信息对应输入框高亮显示蓝色背景(类似 `template-manage` 中的字段高亮效果)
- 自动滚动到屏幕可视区域的 1/3~2/3 位置(使用 `block: 'center'`
### 需求 3视频上传按钮整合进缩略图列表
`report-editor` 右侧「视频分析」Tab 中原本独立占据一行的「上传视频」大按钮,缩小并移入水平滚动的视频缩略图列表首位(`flex gap-2 overflow-x-auto`),尺寸与视频卡片保持一致(约 `w-24 h-[68px]`),节省垂直空间。
### 需求 4视频模块间距紧凑化
缩减「视频分析」面板中以下间距:
- 视频缩略图列表与下方视频播放器之间的距离
- 视频播放器与播放控制按钮之间的距离
- 播放控制按钮与「关键帧摘取」标题之间的距离
为下方关键帧列表腾出更多展示空间。
### 需求 5签名与日期之间增加空行
`defaultContent.ts` 中,「手术者签名」行与「撰写时间」行之间插入一个空段落 `<p>&nbsp;</p>`,使排版更美观。
### 需求 6图片占位符填充后高度自适应
当图片占位符被填充图片后,若图片实际高度小于占位符预设高度(如 200px占位符仍保留固定高度导致下方出现大片空白。需在填充图片后将占位符的 `height``width``line-height` 重置为 `auto` / `normal`,让高度由图片实际尺寸决定。
## 涉及文件
- `src/utils/defaultContent.ts`(需求 1、5
- `src/pages/ReportEditor.tsx`(需求 2、3、4、6
- `src/pages/TemplateManage.tsx`(需求 6
## 需求影响范围
- 默认模板打印样式
- 编辑器交互体验
- 视频分析面板布局
- 图片占位符自适应行为