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

12 KiB
Raw Permalink Blame History

合同起草页面 - 单个替换和高亮功能实现

实现概述

根据用户需求,对合同起草页面进行了以下优化:

  1. 移除分组标题 - 表单不再显示"基本信息"、"甲方信息"等分组标题
  2. 紧凑底部按钮 - 减少按钮空间占用,从 3 个按钮精简为 2 个
  3. 移除导出按钮 - "导出文档"功能与"完成起草"重复,已移除
  4. 单个字段替换 - 每个输入框后添加独立的"替换"按钮
  5. 字段聚焦高亮 - 点击输入框时高亮文档中对应的占位符

修改的文件

1. app/components/contracts/PlaceholderForm.tsx

接口更新

interface PlaceholderFormProps {
  // ... 原有 props
  onSingleReplace?: (key: string, value: string) => void;  // 新增:单个替换
  onFieldFocus?: (key: string) => void;  // 新增:字段聚焦(高亮)
}

关键功能实现

1. 单个字段替换

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. 字段聚焦高亮

const handleFieldFocus = (key: string) => {
  if (onFieldFocus) {
    onFieldFocus(key);
  }
};

在输入框上绑定:

<input
  onFocus={() => handleFieldFocus(field.key)}
  // ... 其他 props
/>

3. UI 改进

移除分组标题

// ❌ 之前:按组显示
{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>
))}

紧凑底部按钮

// ❌ 之前: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>

单独的替换按钮

<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. 单个替换处理

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. 字段聚焦高亮处理

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

<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)
  ↓
显示替换完成提示(含替换数量)

技术细节

状态管理

单个字段替换状态

const [replacingFields, setReplacingFields] = useState<Set<string>>(new Set());

使用 Set 而不是数组的优势:

  • O(1) 查找时间复杂度
  • 自动去重
  • 方便添加/删除

状态更新模式

// 添加
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 调用模式

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) - 搜索文本(备用方案)

错误处理

单个替换

  • 用户可见的错误提示
  • 日志记录便于调试
  • 失败后清除加载状态

字段聚焦高亮

  • 静默失败(不打断用户)
  • 日志记录便于调试
  • 非关键功能,失败不影响主流程

样式优化

头部样式

.px-6 .py-4 .border-b .border-gray-200 .bg-gradient-to-r .from-blue-50 .to-white
  • 渐变背景(蓝色到白色)
  • 适当的内边距(px-6 py-4
  • 底部边框分隔

按钮样式

单独替换按钮

.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
  • 替换中:显示旋转图标 + "替换中"文本

底部按钮

.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 高亮功能 - findsearch 方法可能不可用,需要在实际集成 Collabora 后验证
  2. 多个占位符实例 - 如果同一个占位符在文档中出现多次,find 方法可能只高亮第一个
  3. 替换后高亮失效 - 替换后占位符不再存在,聚焦高亮将无法找到目标

后续优化建议

  1. 高级查找功能 - 集成 Collabora 后,使用 Find & Replace 对话框 API 实现更精确的高亮
  2. 替换预览 - 在替换前预览变更,避免误操作
  3. 撤销功能 - 支持撤销单个替换操作
  4. 字段验证 - 添加字段格式验证(如日期、金额等)
  5. 智能填充 - 根据历史记录或模板推荐字段值

总结

本次实现完成了用户提出的所有需求:

UI 优化

  • 移除冗余的分组标题
  • 紧凑底部按钮布局
  • 移除重复的导出按钮

功能增强

  • 每个字段单独替换
  • 字段聚焦时高亮占位符

用户体验

  • 即时反馈(加载状态、成功提示)
  • 流畅的交互动画
  • 清晰的视觉层次

代码质量

  • TypeScript 类型安全
  • 合理的状态管理
  • 完善的错误处理

🎯 可以开始测试了!访问 http://localhost:5173/contract-draft/1 验证所有功能。