Files
leaudit-platform-frontend/docs/contract-draft-individual-replace-implementation.md
2025-12-05 00:09:32 +08:00

476 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 合同起草页面 - 单个替换和高亮功能实现
## 实现概述
根据用户需求,对合同起草页面进行了以下优化:
1.**移除分组标题** - 表单不再显示"基本信息"、"甲方信息"等分组标题
2.**紧凑底部按钮** - 减少按钮空间占用,从 3 个按钮精简为 2 个
3.**移除导出按钮** - "导出文档"功能与"完成起草"重复,已移除
4.**单个字段替换** - 每个输入框后添加独立的"替换"按钮
5.**字段聚焦高亮** - 点击输入框时高亮文档中对应的占位符
## 修改的文件
### 1. `app/components/contracts/PlaceholderForm.tsx`
#### 接口更新
```typescript
interface PlaceholderFormProps {
// ... 原有 props
onSingleReplace?: (key: string, value: string) => void; // 新增:单个替换
onFieldFocus?: (key: string) => void; // 新增:字段聚焦(高亮)
}
```
#### 关键功能实现
**1. 单个字段替换**
```typescript
const [replacingFields, setReplacingFields] = useState<Set<string>>(new Set());
const handleSingleReplace = async (key: string) => {
const value = localValues[key];
if (!value || !onSingleReplace) return;
setReplacingFields(prev => new Set(prev).add(key));
try {
await onSingleReplace(key, value);
} finally {
setReplacingFields(prev => {
const next = new Set(prev);
next.delete(key);
return next;
});
}
};
```
**特点**
- 使用 `Set` 管理多个字段的替换状态
- 每个字段独立显示加载状态
- 替换完成后自动清除加载状态
**2. 字段聚焦高亮**
```typescript
const handleFieldFocus = (key: string) => {
if (onFieldFocus) {
onFieldFocus(key);
}
};
```
在输入框上绑定:
```typescript
<input
onFocus={() => handleFieldFocus(field.key)}
// ... 其他 props
/>
```
**3. UI 改进**
**移除分组标题**
```typescript
// ❌ 之前:按组显示
{schema?.groups?.map(group => (
<div key={group.title}>
<h3>{group.title}</h3>
{group.fields.map(field => <Field />)}
</div>
))}
// ✅ 现在:扁平化显示
{schema?.fields.map((field) => (
<div key={field.key}>
<label>{field.label}</label>
<input />
<button></button>
</div>
))}
```
**紧凑底部按钮**
```typescript
// ❌ 之前:3 个按钮,每行一个
<div className="space-y-2">
<button className="w-full"></button>
<button className="w-full"></button>
<button className="w-full"></button>
</div>
// ✅ 现在:2 个按钮,水平排列
<div className="flex gap-2">
<button className="flex-1"></button>
<button className="flex-1"></button>
</div>
```
**单独的替换按钮**
```typescript
<div className="flex gap-2">
<input className="flex-1" />
<button
onClick={() => handleSingleReplace(field.key)}
disabled={!localValues[field.key] || replacingFields.has(field.key)}
className={`px-3 py-2 rounded-lg ...`}
>
{replacingFields.has(field.key) ? (
<>
<i className="ri-loader-4-line animate-spin"></i>
<span></span>
</>
) : (
<>
<i className="ri-refresh-line"></i>
<span></span>
</>
)}
</button>
</div>
```
### 2. `app/routes/contract-draft.$draftId.tsx`
#### 新增处理函数
**1. 单个替换处理**
```typescript
const handleSingleReplace = async (key: string, value: string) => {
try {
const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
if (!collaboraRef?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
const placeholder = `{{${key}}}`;
console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`);
if (collaboraRef.unoCommands?.replaceAll) {
await collaboraRef.unoCommands.replaceAll(placeholder, value);
toastService.success(`已替换 ${key}`);
} else {
console.warn('[Draft] unoCommands.replaceAll 方法不可用');
toastService.warning('替换功能暂不可用');
}
} catch (error) {
console.error('[Draft] 单个替换失败:', error);
toastService.error(`替换失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
```
**特点**
- 检查 Collabora 是否就绪
- 使用与批量替换相同的 `replaceAll` API
- 提供即时反馈(成功/失败提示)
**2. 字段聚焦高亮处理**
```typescript
const handleFieldFocus = async (key: string) => {
try {
const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
if (!collaboraRef?.isReady) {
return;
}
const placeholder = `{{${key}}}`;
console.log(`[Draft] 高亮占位符: ${placeholder}`);
// 尝试使用查找功能高亮占位符
if (collaboraRef.unoCommands?.find) {
await collaboraRef.unoCommands.find(placeholder);
} else if (collaboraRef.unoCommands?.search) {
await collaboraRef.unoCommands.search(placeholder);
} else {
console.warn('[Draft] 查找/高亮功能不可用');
}
} catch (error) {
console.error('[Draft] 高亮占位符失败:', error);
}
};
```
**特点**
- 静默失败(不打断用户操作)
- 尝试多种查找方法(find、search)
- 用于改善用户体验,非关键功能
**3. 传递新 props**
```typescript
<PlaceholderForm
schema={template.placeholder_schema as any}
values={placeholderValues}
onChange={setPlaceholderValues}
onBatchReplace={handleBatchReplace}
onExportDocument={handleExportDocument}
onComplete={handleComplete}
isReplacing={isReplacing}
isDeleting={isDeleting}
onSingleReplace={handleSingleReplace} // 新增
onFieldFocus={handleFieldFocus} // 新增
/>
```
## 用户体验改进
### 之前的问题
1. **分组标题冗余** - "基本信息"、"甲方信息"等标题占用空间,且意义不大
2. **按钮占用空间过大** - 3 个全宽按钮垂直堆叠,占用大量空间
3. **功能重复** - "导出文档"和"完成起草"功能重复
4. **替换不便** - 只能全部替换,无法单独替换某个字段
5. **缺少视觉反馈** - 不知道输入框对应文档中的哪个占位符
### 改进后的体验
1.**扁平化布局** - 移除分组,字段直接展示,更简洁
2.**紧凑按钮区** - 2 个按钮水平排列,节省空间
3.**功能精简** - 只保留必要功能,避免重复
4.**灵活替换** - 既可单独替换,也可全部替换
5.**即时反馈** - 点击输入框高亮对应占位符,点击替换按钮显示加载状态
## 交互流程
### 单个字段替换流程
```
用户填写输入框 → 点击"替换"按钮
检查字段是否有值
设置该字段为"替换中"状态
调用 handleSingleReplace(key, value)
检查 Collabora 是否就绪
调用 collaboraRef.unoCommands.replaceAll({{key}}, value)
显示成功提示 / 错误提示
清除"替换中"状态
```
### 字段聚焦高亮流程
```
用户点击输入框 → onFocus 事件触发
调用 handleFieldFocus(key)
检查 Collabora 是否就绪
调用 collaboraRef.unoCommands.find({{key}})
文档中对应占位符被高亮显示
```
### 批量替换流程(保持不变)
```
用户填写所有字段 → 点击"全部替换"按钮
检查 Collabora 是否就绪
遍历所有有值的字段
逐个调用 replaceAll({{key}}, value)
显示替换完成提示(含替换数量)
```
## 技术细节
### 状态管理
**单个字段替换状态**
```typescript
const [replacingFields, setReplacingFields] = useState<Set<string>>(new Set());
```
使用 `Set` 而不是数组的优势:
- O(1) 查找时间复杂度
- 自动去重
- 方便添加/删除
**状态更新模式**
```typescript
// 添加
setReplacingFields(prev => new Set(prev).add(key));
// 删除
setReplacingFields(prev => {
const next = new Set(prev);
next.delete(key);
return next;
});
// 检查
replacingFields.has(field.key)
```
### Collabora 集成
**API 调用模式**
```typescript
const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
// 1. 检查是否就绪
if (!collaboraRef?.isReady) {
// 处理未就绪情况
return;
}
// 2. 调用 UNO 命令
if (collaboraRef.unoCommands?.methodName) {
await collaboraRef.unoCommands.methodName(args);
} else {
// 处理方法不可用情况
}
```
**可用的 UNO 命令**
- `replaceAll(searchText, replaceText)` - 查找并替换所有匹配项
- `find(searchText)` - 查找并高亮文本(可能需要验证)
- `search(searchText)` - 搜索文本(备用方案)
### 错误处理
**单个替换**
- 用户可见的错误提示
- 日志记录便于调试
- 失败后清除加载状态
**字段聚焦高亮**
- 静默失败(不打断用户)
- 日志记录便于调试
- 非关键功能,失败不影响主流程
## 样式优化
### 头部样式
```css
.px-6 .py-4 .border-b .border-gray-200 .bg-gradient-to-r .from-blue-50 .to-white
```
- 渐变背景(蓝色到白色)
- 适当的内边距(px-6 py-4
- 底部边框分隔
### 按钮样式
**单独替换按钮**
```css
.px-3 .py-2 .rounded-lg .transition-all .duration-150
.flex .items-center .gap-1.5 .text-sm .font-medium .whitespace-nowrap
```
**状态样式**
- 禁用:`bg-gray-100 text-gray-400 cursor-not-allowed`
- 正常:`bg-primary text-white hover:bg-primary-hover shadow-sm hover:shadow`
- 替换中:显示旋转图标 + "替换中"文本
**底部按钮**
```css
.flex-1 .flex .items-center .justify-center .gap-1.5
.px-4 .py-2 .text-sm .font-medium .rounded-lg
.transition-all .duration-150
```
- `flex-1` - 均分宽度
- `gap-2` - 按钮之间间距
- 悬停效果:`hover:shadow-md active:scale-[0.98]`
## 测试要点
### 功能测试
1. **单个替换**
- ✅ 填写字段后点击"替换"按钮,占位符被正确替换
- ✅ 未填写字段时,"替换"按钮被禁用
- ✅ 替换过程中显示"替换中"状态
- ✅ 替换成功后显示成功提示
2. **字段聚焦高亮**
- ✅ 点击输入框,文档中对应占位符被高亮
- ✅ 切换输入框,高亮位置相应改变
- ✅ Collabora 未就绪时不报错
3. **批量替换**
- ✅ 点击"全部替换"按钮,所有有值字段的占位符被替换
- ✅ 显示替换数量
- ✅ 替换过程中按钮显示"替换中"状态
4. **完成功能**
- ✅ 点击"完成"按钮,下载文件
- ✅ 下载完成后删除草稿记录
- ✅ 跳转到模板列表页
### UI 测试
1. **布局**
- ✅ 表单无分组标题,字段扁平化显示
- ✅ 每个输入框右侧有"替换"按钮
- ✅ 底部只有 2 个按钮,水平排列
- ✅ 按钮宽度均分,间距合适
2. **响应式**
- ✅ 输入框和按钮适配不同字段长度
- ✅ 长文本输入框(textarea)布局正常
- ✅ 按钮禁用/加载状态样式正确
3. **交互反馈**
- ✅ 按钮悬停效果流畅
- ✅ 加载图标旋转动画流畅
- ✅ Toast 提示及时显示
## 已知限制
1. **Collabora 高亮功能** - `find``search` 方法可能不可用,需要在实际集成 Collabora 后验证
2. **多个占位符实例** - 如果同一个占位符在文档中出现多次,`find` 方法可能只高亮第一个
3. **替换后高亮失效** - 替换后占位符不再存在,聚焦高亮将无法找到目标
## 后续优化建议
1. **高级查找功能** - 集成 Collabora 后,使用 Find & Replace 对话框 API 实现更精确的高亮
2. **替换预览** - 在替换前预览变更,避免误操作
3. **撤销功能** - 支持撤销单个替换操作
4. **字段验证** - 添加字段格式验证(如日期、金额等)
5. **智能填充** - 根据历史记录或模板推荐字段值
## 总结
本次实现完成了用户提出的所有需求:
**UI 优化**
- 移除冗余的分组标题
- 紧凑底部按钮布局
- 移除重复的导出按钮
**功能增强**
- 每个字段单独替换
- 字段聚焦时高亮占位符
**用户体验**
- 即时反馈(加载状态、成功提示)
- 流畅的交互动画
- 清晰的视觉层次
**代码质量**
- TypeScript 类型安全
- 合理的状态管理
- 完善的错误处理
🎯 **可以开始测试了**!访问 `http://localhost:5173/contract-draft/1` 验证所有功能。