2026-04-17-23-12-52 - 修复时间字段24h脏数据、格式选项分类过滤、字段管理编辑面板滚动对齐

This commit is contained in:
2026-04-17 23:18:59 +08:00
parent 1a766edb90
commit d45e973255
7 changed files with 286 additions and 6 deletions

View File

@@ -952,6 +952,7 @@ export default function ReportEditor() {
const formatTimeDisplay = (timeStr: string, fmt?: string): string => {
if (!timeStr || !fmt) return timeStr || '';
if (fmt === '24h') fmt = 'HH:mm';
const [h24str, mstr] = timeStr.split(':');
const h24 = parseInt(h24str) || 0;
const isPM = h24 >= 12;

View File

@@ -86,7 +86,8 @@ export default function TemplateManage() {
const savedFormats = storage.get<string[]>('customTimeFormats', []);
const defaultFormats = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A'];
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...savedFormats])));
const cleanedSaved = savedFormats.filter(f => f !== '24h' && f !== '12h');
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...cleanedSaved])));
const savedTemplates = storage.get<Template[]>('templates', []);
if (savedTemplates.length === 0) {
@@ -878,13 +879,17 @@ export default function TemplateManage() {
<div
key={field.key}
id={`sidebar-field-${field.key}`}
onClick={() => {
onClick={(e) => {
setEditingFieldKey(field.key);
setEditFieldLabel(field.label);
setEditFieldOptions((field.options || []).join(', '));
setEditFieldTimeFormat(field.timeFormat || '');
setEditFieldTimeDefault(field.timeDefault || 'specific');
setEditFieldFixedTimeValue(field.fixedTimeValue || '');
const target = e.currentTarget;
setTimeout(() => {
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 50);
}}
className={`cursor-pointer rounded border p-2 transition-all ${activeFieldKey === field.key ? 'border-accent bg-blue-50 ring-1 ring-accent' : 'border-slate-100 bg-slate-50 hover:border-slate-200'}`}
>
@@ -954,7 +959,15 @@ export default function TemplateManage() {
placeholder="输入格式,如 YYYY-MM-DD"
/>
<datalist id={`edit-format-list-${field.key}`}>
{customTimeFormats.map(fmt => <option key={fmt} value={fmt} />)}
{customTimeFormats
.filter(fmt => {
const isDateFormat = /YYYY|MM|DD/.test(fmt);
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
if (field.type === 'date') return isDateFormat;
if (field.type === 'time') return isTimeFormat;
return true;
})
.map(fmt => <option key={fmt} value={fmt} />)}
</datalist>
</div>
)}
@@ -1114,7 +1127,15 @@ export default function TemplateManage() {
placeholder="输入格式,如 YYYY-MM-DD"
/>
<datalist id="new-format-list">
{customTimeFormats.map(fmt => <option key={fmt} value={fmt} />)}
{customTimeFormats
.filter(fmt => {
const isDateFormat = /YYYY|MM|DD/.test(fmt);
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
if (newFieldForm.type === 'date') return isDateFormat;
if (newFieldForm.type === 'time') return isTimeFormat;
return true;
})
.map(fmt => <option key={fmt} value={fmt} />)}
</datalist>
</div>
)}

View File

@@ -127,8 +127,8 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
{ key: 'department', label: '科别', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
{ key: 'bedNumber', label: '床号', category: '填空', type: 'text', visibleInForm: true, isSystemLocked: false },
{ key: 'surgeryDate', label: '手术日期', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: true, timeFormat: 'YYYY-MM-DD', timeDefault: 'specific' },
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: 'HH:mm', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: 'HH:mm', timeDefault: 'specific' },
{ key: 'reportDate', label: '撰写时间', category: '时间', type: 'date', visibleInForm: true, isSystemLocked: true, timeFormat: 'YYYY年MM月DD日', timeDefault: 'current' },
{ key: 'surgeon', label: '手术者', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: true, options: ['张医生', '李医生', '王医生'] },
{ key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: true, options: ['赵医生', '钱医生', '孙医生'] },

View File

@@ -0,0 +1,111 @@
# 实现方案 — 2026-04-17-23-12-52
## 根因分析
### 问题1格式选项缺失与旧值残留
- `types.ts``DEFAULT_FORM_FIELDS` 中,`startTime``endTime``timeFormat` 仍被硬编码为 `'24h'`(历史遗留)。
- `TemplateManage.tsx` 中的 `defaultFormats` 虽然已包含 `HH:mm``hh:mm A`,但 `customTimeFormats` 的 datalist 未按字段类型过滤,导致 date/time 格式混在一起显示,用户体验混乱。
- 用户的 `localStorage``customTimeFormats` 可能还残留旧值 `'24h'``'12h'`
### 问题2报告编辑器显示 "24h"
- `formatTimeDisplay('14:30', '24h')` 中,格式字符串 `'24h'` 不包含 `HH`/`hh`/`mm`/`A` 等任何替换 token函数直接原样返回 `'24h'`
- 这是 F1 根因的直接后果:`formFieldsConfig` 中的 `timeFormat = '24h'` 被传入格式化函数。
### 问题3输入框点击失效
- `TemplateManage.tsx` 字段管理列表位于一个 `overflow-y-auto` 的滚动容器中。
- 点击字段卡片后,卡片内部展开编辑表单,高度瞬间增加。如果卡片原本位于可视区域底部,展开后的输入框可能刚好处于容器边缘之外。
- 浏览器的 hit-testing 在布局突变时可能无法正确将点击事件路由到新出现的输入框上,导致需要手动滚动后才能点击。
## 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `src/types.ts` | `DEFAULT_FORM_FIELDS``startTime`/`endTime``timeFormat``'24h'` 改为 `'HH:mm'` |
| `src/pages/ReportEditor.tsx` | `formatTimeDisplay` 开头增加 `if (fmt === '24h') fmt = 'HH:mm';` 兼容兜底 |
| `src/pages/TemplateManage.tsx` | ① `defaultFormats` 初始化时过滤掉旧脏数据 `'24h'`/`'12h'`;② datalist 渲染时按字段类型date/time过滤选项③ 字段编辑卡片 `onClick` 中增加 `scrollIntoView` |
## 具体代码变更
### 1. types.ts
```ts
// 修改前
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: '24h', timeDefault: 'specific' },
// 修改后
{ key: 'startTime', label: '手术开始时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: 'HH:mm', timeDefault: 'specific' },
{ key: 'endTime', label: '手术终止时间', category: '时间', type: 'time', visibleInForm: true, isSystemLocked: true, timeFormat: 'HH:mm', timeDefault: 'specific' },
```
### 2. ReportEditor.tsx
```ts
// 在 formatTimeDisplay 函数开头增加一行兼容兜底
const formatTimeDisplay = (timeStr: string, fmt?: string): string => {
if (!timeStr || !fmt) return timeStr || '';
if (fmt === '24h') fmt = 'HH:mm'; // 兼容旧脏数据
// ... 后续代码不变
};
```
### 3. TemplateManage.tsx
**3.1 清理旧脏数据(初始化时)**
```ts
// 修改前
const savedFormats = storage.get<string[]>('customTimeFormats', []);
const defaultFormats = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A'];
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...savedFormats])));
// 修改后
const savedFormats = storage.get<string[]>('customTimeFormats', []);
const defaultFormats = ['YYYY-MM-DD', 'YYYY年MM月DD日', 'MM-DD', 'MM月DD日', 'HH:mm', 'hh:mm A'];
// 过滤掉历史遗留的无效旧格式
const cleanedSaved = savedFormats.filter(f => f !== '24h' && f !== '12h');
setCustomTimeFormats(Array.from(new Set([...defaultFormats, ...cleanedSaved])));
```
**3.2 按字段类型过滤 datalist**
在编辑字段和新增字段的 format `<datalist>` 渲染处,增加按 `field.type` / `newFieldForm.type` 过滤:
```tsx
<datalist id={`edit-format-list-${field.key}`}>
{customTimeFormats
.filter(fmt => {
const isDateFormat = /YYYY|MM|DD/.test(fmt);
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
if (field.type === 'date') return isDateFormat;
if (field.type === 'time') return isTimeFormat;
return true;
})
.map(fmt => <option key={fmt} value={fmt} />)}
</datalist>
```
新增字段的 datalist 同理。
**3.3 编辑卡片点击后自动滚动对齐**
```tsx
onClick={(e) => {
setEditingFieldKey(field.key);
// ... 其他 setState
const target = e.currentTarget;
setTimeout(() => {
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 50);
}}
```
## 风险点与应对措施
| 风险 | 应对措施 |
|------|---------|
| 清理 `customTimeFormats` 中的 `'24h'`/`'12h'` 可能误删用户真正想保留的自定义格式 | 仅过滤精确匹配 `'24h'``'12h'` 两个字符串,不影响其他自定义格式 |
| 按类型过滤 datalist 可能误过滤 | 使用正则 `/YYYY|MM|DD/` 识别日期格式,`/HH|hh|mm|A/` 识别时间格式;若格式同时满足两者(理论上不应发生)则同时显示 |
| `scrollIntoView` 在极端短容器中频繁触发 | 使用 `block: 'nearest'` 而非 `'start'`减少不必要的滚动50ms 延迟确保 DOM 已撑开 |
## 回滚策略
- `types.ts` 的修改可直接回退两个字段的 `timeFormat` 字符串。
- `ReportEditor.tsx` 的修改仅增加一行,删除即可。
- `TemplateManage.tsx` 的修改均为增量逻辑,回滚时移除条件块即可。

View File

@@ -0,0 +1,77 @@
# 测试方案 — 2026-04-17-23-12-52
## 测试目标
验证时间字段格式选项补全、报告编辑器 "24h" 脏数据显示修复、以及字段管理编辑面板点击失效问题的修复。
## 测试环境
- 本地开发服务器:`npm run dev`(端口 3000
- 浏览器Chrome/Edge
- 测试账号admin / 123456超级管理员
## 测试用例
### TC-1默认时间格式修正
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 清除 localStorage 中 `formFieldsConfig`,重新登录 | 系统加载默认字段配置 |
| 2 | 进入「模板管理」→「字段管理」 | 字段列表正常显示 |
| 3 | 点击「手术开始时间」进入编辑模式 | `timeFormat` 输入框显示为 `HH:mm`,而非 `24h` |
| 4 | 点击「手术终止时间」进入编辑模式 | 同上,显示 `HH:mm` |
### TC-2报告编辑器 "24h" 脏数据兼容
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 进入「报告编辑器」新建报告 | 加载默认模板 |
| 2 | 在基本信息中填写手术开始时间为 `09:30` | 编辑器中「手术开始时间」smart field 显示 `09:30`,而非 `24h` |
| 3 | 填写手术终止时间为 `14:00` | 编辑器中「手术终止时间」smart field 显示 `14:00`,而非 `24h` |
| 4 | (进阶)手动将 `formFieldsConfig` 中某 time 字段的 `timeFormat` 改回 `'24h'`,刷新后再新建报告 | 该时间字段仍能正常显示时间值(通过兼容兜底) |
### TC-3格式选项按类型过滤
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 进入「模板管理」→「字段管理」 | — |
| 2 | 点击「手术日期」date 类型)进入编辑模式,聚焦格式输入框 | datalist 下拉中**只出现日期格式**(如 `YYYY-MM-DD``YYYY年MM月DD日``MM-DD``MM月DD日`**不出现** `HH:mm``hh:mm A` |
| 3 | 点击「手术开始时间」time 类型)进入编辑模式,聚焦格式输入框 | datalist 下拉中**只出现时间格式**(如 `HH:mm``hh:mm A`**不出现** `YYYY-MM-DD` 等日期格式 |
| 4 | 新增字段 → category 选「时间」→ type 选「日期」→ 聚焦格式输入框 | 同上,只显示日期格式 |
| 5 | type 切换为「时分」→ 聚焦格式输入框 | 只显示时间格式 |
### TC-4旧脏数据清理
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 在浏览器 DevTools → Application → Local Storage 中,手动将 `customTimeFormats` 设为 `["YYYY-MM-DD", "24h", "12h", "HH:mm"]` | — |
| 2 | 刷新页面,进入「模板管理」 | — |
| 3 | 聚焦任意时间字段的格式输入框 | datalist 中**不出现** `24h``12h`,只出现 `YYYY-MM-DD``YYYY年MM月DD日``HH:mm``hh:mm A` 等有效格式 |
### TC-5字段编辑面板点击与滚动
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 进入「模板管理」→「字段管理」 | — |
| 2 | 将页面滚动到底部,使最后一个字段(如「撰写时间」)刚好位于可视区域底部边缘 | — |
| 3 | 点击该字段卡片进入编辑模式 | 编辑面板展开后,卡片**自动平滑滚动**到可视区域内,所有输入框和下拉框均可正常点击获取焦点,无需手动滚动 |
| 4 | 重复测试中间位置的字段 | 展开后同样能正常点击所有控件 |
### TC-6类型检查
| 步骤 | 操作 | 预期结果 |
|------|------|---------|
| 1 | 在项目根目录执行 `npm run lint` | `tsc --noEmit` 通过0 errors |
## 验收标准
- [ ] `npm run lint` 无 TypeScript 编译错误
- [ ] 默认字段 `startTime`/`endTime``timeFormat``HH:mm`,不再出现 `24h`
- [ ] 报告编辑器中时间字段正常显示时间值,不出现 "24h" 字样
- [ ] date 字段的 format datalist 只显示日期格式time 字段只显示时间格式
- [ ] localStorage 中旧脏数据 `24h`/`12h` 被自动清理
- [ ] 字段编辑面板展开后自动滚动对齐,底部输入框可正常点击
## 测试方式
全部使用手工功能验证(项目无单元测试框架)。

View File

@@ -817,3 +817,40 @@ if ((settings.autoInsertDelay || 0) > 0) {
- 当字段配置了「固定默认值」或「自动填充当前值」时,必须在所有「创建新数据」的入口(初始 state、切换模板、重置表单等中显式遍历字段配置并注入不能依赖单个 `useEffect` 来兜底——因为 `useEffect` 的触发条件可能与数据创建时机不一致。
- 对于「格式→UI 形态」的联动判断,应使用**包含性判断**`includes`)而非**精确匹配**,以兼容用户自定义格式。如果判断逻辑较为复杂,建议抽离为独立工具函数(如 `is12HourFormat(fmt: string): boolean`)。
- 当某个字段在初始化时被赋予了「看似合理的默认值」(如 `surgeryDate: new Date()`),必须评估这是否会拦截后续基于字段配置的自动填充逻辑。若会拦截,应改为空值并在最后做兜底赋值。
---
## 记录 27DEFAULT_FORM_FIELDS 遗留 '24h' 默认值导致报告显示异常 + 格式选项未分类 + 编辑面板点击失效
**A. 具体问题**
1. `template-manage` 字段管理中,时间字段的格式 datalist 只显示 `YYYY-MM-DD` 和 `24h`,缺少 `YYYY年MM月DD日` 和 `HH:mm`/`hh:mm A`。
2. `report-editor` 中手术终止时间 smart field 显示为 "24h" 字样,而非正常时间值。
3. `template-manage` 字段管理中,点击底部字段进入编辑模式后,部分输入框/下拉框点击无响应,需手动滚动后才能获取焦点。
**B. 产生问题原因**
1. **`DEFAULT_FORM_FIELDS` 遗留旧值**`types.ts` 中 `startTime` 和 `endTime` 的 `timeFormat` 仍被硬编码为 `'24h'`(历史遗留简写别名)。当新用户登录或重置系统时,该值被加载到 `formFieldsConfig` 中。`ReportEditor.tsx` 的 `formatTimeDisplay` 函数用 `'24h'` 作为格式模板进行 token 替换,但 `'24h'` 中不含 `HH`/`hh`/`mm`/`A` 等任何可替换 token函数直接原样返回 `'24h'`,导致编辑器中显示 "24h"。
2. **`customTimeFormats` 未按类型过滤**`TemplateManage.tsx` 的 datalist 直接渲染了 `customTimeFormats` 数组中的所有格式(日期和时间混在一起)。当用户编辑 time 字段时,会看到 `YYYY-MM-DD` 等日期格式;编辑 date 字段时,会看到 `HH:mm` 等时间格式,选项混乱。
3. **布局突变导致点击穿透失效**:字段管理列表位于 `overflow-y-auto` 滚动容器内。点击字段卡片后,内部编辑表单展开,高度瞬间增加。若卡片原本位于可视区域底部边缘,新出现的输入框可能刚好处于容器裁剪区域之外,浏览器 hit-testing 无法将点击事件正确路由到输入框上。
**C. 解决问题方案**
1. **修正默认值**`types.ts` 中 `startTime`/`endTime` 的 `timeFormat` 从 `'24h'` 改为 `'HH:mm'`。
2. **兼容兜底**`ReportEditor.tsx` 的 `formatTimeDisplay` 开头增加 `if (fmt === '24h') fmt = 'HH:mm';`,防止已有用户的 `formFieldsConfig` 中仍残留 `'24h'` 导致显示异常。
3. **清理旧缓存**`TemplateManage.tsx` 初始化 `customTimeFormats` 时,对 `savedFormats` 增加 `.filter(f => f !== '24h' && f !== '12h')`,自动清理历史遗留的无效旧格式。
4. **按类型过滤 datalist**:编辑字段和新增字段的 format `<datalist>` 渲染时,增加 `.filter`
```ts
.filter(fmt => {
const isDateFormat = /YYYY|MM|DD/.test(fmt);
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
if (field.type === 'date') return isDateFormat;
if (field.type === 'time') return isTimeFormat;
return true;
})
```
5. **自动滚动对齐**:字段卡片 `onClick` 中,在设置完编辑状态后增加 `setTimeout(() => { e.currentTarget.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 50);`,确保编辑面板展开后卡片位于可视区域内。
**D. 后续如何避免问题**
- 当将格式简写别名(如 `'24h'`)迁移为标准 token 格式(如 `'HH:mm'`)时,必须**全局搜索**所有硬编码默认值(`DEFAULT_FORM_FIELDS`、测试数据、mock 数据等),确保源头不再产生脏数据。
- `customTimeFormats` 这类用户可扩展的缓存数组,在初始化时应建立**无效值清理机制**,防止历史版本残留的数据污染后续 UI。
- `datalist` / `select` 的选项如果存在明显的类型分组(日期 vs 时间),应在渲染层做过滤,而不是将所有选项平铺展示。
- 任何在滚动容器内通过点击展开/折叠的交互组件,都应考虑增加 `scrollIntoView` 兜底,防止布局突变导致的点击失效问题。

View File

@@ -0,0 +1,33 @@
# 需求分析 — 2026-04-17-23-12-52
## 原始需求摘要
1. `template-manage` 字段管理 → 时间字段 → 格式第三栏中,当前只显示 `YYYY-MM-DD``24h`,缺少 `YYYY年MM月DD日``HH:mm` / `hh:mm A` 等选项;希望将 `24h` 改为 `HH:mm24H` 的友好显示。
2. `report-editor` 中手术终止时间 smart field 显示为 "24h" 字样,而不是正常的时间值。
3. `template-manage` 字段管理中,点击字段进入编辑模式后,部分输入框/下拉框在特定位置(如滚动容器底部边缘)点击无响应,需要滚动屏幕后才能正常获取焦点。
## 需求拆解
### 功能点
- **F1**:修正 `types.ts``DEFAULT_FORM_FIELDS``startTime`/`endTime``timeFormat` 默认值,从 `'24h'` 改为 `'HH:mm'`
- **F2**:在 `ReportEditor.tsx``formatTimeDisplay` 函数中增加对旧脏数据 `'24h'` 的兼容兜底,自动映射为 `'HH:mm'`
- **F3**:在 `TemplateManage.tsx` 中按字段类型date/time过滤 `customTimeFormats` 的 datalist 显示,避免 date 字段看到 time 格式、time 字段看到 date 格式。
- **F4**:在 `TemplateManage.tsx` 的字段编辑卡片 `onClick` 中增加 `scrollIntoView`,解决编辑面板撑开后底部输入框被滚动容器裁切导致的点击失效问题。
### 非功能点
- 向后兼容:已有用户的 `formFieldsConfig` 中可能仍存在 `'24h'`,通过 F2 的兼容处理确保不影响现有报告。
- 不引入新的 localStorage key复用现有的 `customTimeFormats` 缓存。
## 影响范围预估
| 模块 | 影响程度 | 说明 |
|------|---------|------|
| `src/types.ts` | 中 | `DEFAULT_FORM_FIELDS``startTime`/`endTime``timeFormat` 默认值修改 |
| `src/pages/ReportEditor.tsx` | 低 | `formatTimeDisplay` 增加一行兼容兜底 |
| `src/pages/TemplateManage.tsx` | 中 | datalist 过滤逻辑 + 编辑卡片滚动对齐 |
## 待确认问题
无(用户已明确需求,且本次无需人工确认)。