Files
zkxw/docs/superpowers/plans/2026-07-01-attachment-drag-upload.md
T
2026-07-01 16:37:33 +08:00

13 KiB
Raw Blame History

附件拖拽上传 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在方案 1 范围内(类型 A/B/C 附件上传)为所有上传区增加拖拽落文件能力,保留现有点击上传行为。

Architecture: 新增 useAttachmentDropZone composable 封装 dragenter/dragleave/dragover/drop;各页面在 .attachment-upload(或 C 类 .paper-att-list)独立挂载,drop 后复用现有 handleAttachmentChange / handlePaperAttachmentChange。类型 B 仅改 ProtocolFinanceConfirmFields 一处并回归 6 个消费页。全局 .is-dragover 样式放 app.scss。

Tech Stack: Vue 3 · Element Plus el-upload · TypeScript · SCSS · fetchUploadAttachment

设计文档: docs/superpowers/specs/2026-07-01-attachment-drag-upload-design.md


文件结构

路径 职责
web-admin/src/composables/useAttachmentDropZone.ts 新建 — drop zone 事件与 isDragover 状态
web-admin/src/assets/styles/core/app.scss 修改 — .attachment-upload.is-dragover、.paper-att-list.is-dragover
web-admin/src/components/business/protocol-finance-confirm-fields/index.vue 修改 — 类型 B 共享上传
web-admin/src/components/business/outbound-material-form-sections/index.vue 修改 — 类型 C 行内上传
22 个类型 A .vue 文件 修改 — 各页独立挂载 composable
openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md 修改 — 补充拖拽 SHALL

Task 0: 基础设施

Files:

  • Create: web-admin/src/composables/useAttachmentDropZone.ts

  • Modify: web-admin/src/assets/styles/core/app.scss

  • Modify: openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md

  • Step 1: 创建 composable

// web-admin/src/composables/useAttachmentDropZone.ts
import { ref, type Ref } from 'vue'

export type AttachmentDropZoneOptions = {
  /** 收到有效文件列表时回调(已过滤空项) */
  onFiles: (files: File[]) => void | Promise<void>
  /** 为 true 时不响应拖拽 */
  disabled?: Ref<boolean> | (() => boolean)
}

export function useAttachmentDropZone(options: AttachmentDropZoneOptions) {
  const isDragover = ref(false)
  let dragDepth = 0

  function isDisabled() {
    const d = options.disabled
    if (!d) return false
    return typeof d === 'function' ? d() : d.value
  }

  function onDragEnter(e: DragEvent) {
    if (isDisabled()) return
    e.preventDefault()
    dragDepth += 1
    isDragover.value = true
  }

  function onDragLeave(e: DragEvent) {
    if (isDisabled()) return
    e.preventDefault()
    dragDepth = Math.max(0, dragDepth - 1)
    if (dragDepth === 0) isDragover.value = false
  }

  function onDragOver(e: DragEvent) {
    if (isDisabled()) return
    e.preventDefault()
    if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
  }

  function onDrop(e: DragEvent) {
    if (isDisabled()) return
    e.preventDefault()
    dragDepth = 0
    isDragover.value = false
    const list = e.dataTransfer?.files
    if (!list?.length) return
    const files = Array.from(list).filter((f) => f.size >= 0)
    if (files.length) void options.onFiles(files)
  }

  const dropZoneHandlers = {
    onDragenter: onDragEnter,
    onDragleave: onDragLeave,
    onDragover: onDragOver,
    onDrop: onDrop
  }

  return { isDragover, dropZoneHandlers }
}
  • Step 2: 全局 dragover 样式(app.scss 附件相关区末尾追加)
.attachment-upload.is-dragover {
  border: 1px dashed var(--primary, #10b981);
  border-radius: 8px;
  background: var(--primary-bg, #ecfdf5);
  box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.15);
}

.paper-att-list.is-dragover {
  border: 1px dashed var(--primary, #10b981);
  border-radius: 8px;
  background: var(--primary-bg, #ecfdf5);
}
  • Step 3: OpenSpec 补充

在 ui-conventions/spec.md「附件上传卡片化布局」的 el-upload 配置表后增加:

  • Drop zone SHALL 为 .attachment-upload 根容器(表格行内纸质材料为 .paper-att-list)

  • SHALL 支持拖拽与点击两种方式;.upload-hint 含「支持拖拽或点击上传」

  • dragover 时使用 .is-dragover 修饰类(Fresh Mint 虚线边框 + 浅绿底)

  • Step 4: 手工冒烟

本地 pnpm dev,临时在任意页挂载 composable 验证 drop 能触发 onFiles(可在 Step 1 完成前跳过,于 Task 1 一并验)。


Task 1: 试点 — ProtocolFormDialog(A1)

Files:

  • Modify: web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue

  • Step 1: 协议附件区挂载 composable

在 <script setup> 中:

import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'

// 在 handleAttachmentChange 定义之后:
const { isDragover: isProtocolAttachDragover, dropZoneHandlers: protocolAttachDropHandlers } =
  useAttachmentDropZone({
    onFiles: (files) => {
      for (const file of files) {
        void handleAttachmentChange({ raw: file })
      }
    }
  })

模板中协议附件的 .attachment-upload 改为:

<div
  class="attachment-upload"
  :class="{ 'is-dragover': isProtocolAttachDragover }"
  v-bind="protocolAttachDropHandlers"
>

.upload-hint 文案改为含「支持拖拽或点击上传」。

  • Step 2: 缴费凭证区(第二处 upload-card--sm)

新增独立 composable 实例(变量名如 isPaymentVoucherDragover / paymentVoucherDropHandlers),绑定到缴费明细内 .attachment-upload 容器;onFiles 调用现有缴费凭证 handleXxxChange(与 @change 同一函数)。

  • Step 3: 回归测试

打开采集协议录入弹窗 → 协议附件:点击 1 文件、拖拽 1 文件、拖拽 3 文件、拖拽 >20MB 文件。缴费凭证区重复上述。确认提交拦截「上传中」仍有效。

  • Step 4: Commit
git add web-admin/src/composables/useAttachmentDropZone.ts \
  web-admin/src/assets/styles/core/app.scss \
  web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue \
  openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
git commit -m "feat(web-admin): 附件上传支持拖拽(试点 ProtocolFormDialog)"

Task 2: 细胞存储域(A2–A5 + B0 + B2–B4)

Files:

  • Modify: web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue(A2,2 区)

  • Modify: web-admin/src/views/cell-storage/protocol-audit/StorageCertTab.vue(A3)

  • Modify: web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue(A4)

  • Modify: web-admin/src/views/research-donation/list/DonationFormDialog.vue(A5,2 区)

  • Modify: web-admin/src/components/business/protocol-finance-confirm-fields/index.vue(B0)

  • Modify: web-admin/src/views/cell-storage/payment-confirm/index.vue(B2,回归)

  • Modify: web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue(B3,回归)

  • Modify: web-admin/src/views/cell-storage/payment-confirm/InvoiceRequestTab.vue(B4,回归)

  • Step 1: 改 ProtocolFinanceConfirmFields(B0)

import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'

const { isDragover, dropZoneHandlers } = useAttachmentDropZone({
  onFiles: (files) => {
    for (const file of files) {
      void handleAttachmentChange({ raw: file })
    }
  }
})

.attachment-upload 加 :class 与 v-bind="dropZoneHandlers";更新 .upload-hint 文案。

  • Step 2: A2–A5 各页按 Task 1 模式改

每处 .attachment-upload 一个 composable 实例;多上传区 = 多实例。DonationFormDialog、PaymentInfoTab 各 2 实例。

  • Step 3: 回归 B2–B4 + A2–A5
页面 入口
付款信息看板 PaymentInfoTab 协议审核
储存证 StorageCertTab 协议审核
满月待办 FullmoonTodoTab 协议审核
捐赠 DonationFormDialog 科研捐赠
付款确认 payment-confirm 细胞存储-付款确认 三 Tab
  • Step 4: Commit
git commit -m "feat(web-admin): 细胞存储域附件上传支持拖拽"

Task 3: 质检域(A6–A7 + B1 + B6)

Files:

  • Modify: web-admin/src/views/quality-inspection/env-detect-record/index.vue(A6)

  • Modify: web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue(A7)

  • Modify: web-admin/src/views/quality-inspection/sample-reception/index.vue(B1,2 区 — 仅回归 hint 若 B0 已改)

  • Modify: web-admin/src/views/quality-inspection/qc-material/QcMaterialBatchAttachmentDialog.vue(B6,回归)

  • Step 1: A6、A7 挂载 composable(同 Task 1 模式)

  • Step 2: 确认 B1/B6 经 B0 已生效

打开样本接收(单条 + 批量弹窗)、质检物料批次附件弹窗,测拖拽。

  • Step 3: Commit
git commit -m "feat(web-admin): 质检域附件上传支持拖拽"

Task 4: 中试服务(上)— 渠道与付款(A10–A15)

Files:

  • Modify: web-admin/src/views/pilot-service/channel/index.vue(A10)

  • Modify: web-admin/src/views/pilot-service/channel/ChannelServiceOrderTab.vue(A11,2 区)

  • Modify: web-admin/src/views/pilot-service/channel-service-order/ServiceOrderTxnDialog.vue(A12)

  • Modify: web-admin/src/views/pilot-service/channel-service-order-finance/index.vue(A13)

  • Modify: web-admin/src/views/pilot-service/clinical-payment-confirm/index.vue(A14,2 区)

  • Modify: web-admin/src/views/pilot-service/clinical-payment-confirm/RefundConfirmTab.vue(A15,2 区)

  • Step 1: 逐文件挂载 composable(共 9 个上传区)

ServiceOrderTxnDialog / ChannelServiceOrderTab 若存在本地 uploadAttachmentItem,drop 的 onFiles 应调用同一入口。

  • Step 2: 回归

渠道管理、渠道服务单、财务确认、临床付款确认(含退费 Tab)。

  • Step 3: Commit
git commit -m "feat(web-admin): 中试渠道与临床付款确认附件支持拖拽"

Task 5: 中试服务(下)— 临床申请与出库(A16–A22 + C1–C2)

Files:

  • Modify: web-admin/src/views/pilot-service/pilot-application/ApplicationFormDialog.vue(A16,4 区)

  • Modify: web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue(A17,5 区)

  • Modify: web-admin/src/views/pilot-service/pilot-application/ClinicalPaymentTab.vue(A18,2 区)

  • Modify: web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseReviewDialog.vue(A19)

  • Modify: web-admin/src/views/pilot-service/infusion-schedule-management/InfusionScheduleHandoverDialog.vue(A20,交接附件区)

  • Modify: web-admin/src/views/pilot-service/checkup-management/CheckupDetailDialog.vue(A21)

  • Modify: web-admin/src/views/pilot-service/checkup-management/CheckupMarkCheckedDialog.vue(A22)

  • Modify: web-admin/src/components/business/outbound-material-form-sections/index.vue(C1)

  • Modify: web-admin/src/views/pilot-service/outbound-release-review/OutboundReleaseMaterialDialog.vue(C2)

  • Step 1: A16–A22 卡片化区

ApplicationDetailDialog 中非标准 attachment-trigger-card 区域:drop zone 绑外层 .attachment-upload 或等价 flex 容器(5 处逐一改)。

  • Step 2: C1 行内纸质材料
function usePaperRowDropZone(rowIndex: number) {
  return useAttachmentDropZone({
    onFiles: (files) => {
      for (const file of files) {
        void handlePaperAttachmentChange(rowIndex, { raw: file })
      }
    }
  })
}

模板:每行 .paper-att-list 绑 :class="{ 'is-dragover': ... }" 与 handlers(可用 v-for 内联或按 index 数组存 ref)。

  • Step 3: C2 与 C1 对齐

OutboundReleaseMaterialDialog.vue 行内上传逻辑与 C1 保持一致。

  • Step 4: 回归

临床申请新建/详情、出库审核、输注交接(交接附件 + 物料纸质材料)、体检报告上传。

  • Step 5: Commit
git commit -m "feat(web-admin): 临床申请与出库物料附件支持拖拽"

Task 6: 生产域(A8–A9 + B5)

Files:

  • Modify: web-admin/src/views/production/sop/index.vue(A8)

  • Modify: web-admin/src/views/production/cell-product/index.vue(A9)

  • Modify: web-admin/src/views/production/production-work-order/TaskPaperAttachmentsPanel.vue(B5,回归)

  • Step 1: A8、A9 挂载 composable

  • Step 2: 回归 B5

生产工单 → 任务执行 → 线下纸质文档 Tab。

  • Step 3: 全量收尾

./scripts/update-graph.sh frontend(若 composable 被大量引用)

  • Step 4: Commit
git commit -m "feat(web-admin): 生产域附件上传支持拖拽"

验收总表(方案 1 完成标准)

类别 文件数 上传区约数 状态
A 内联卡片 22 ~33 Task 1–6
B 共享组件 + 回归 1 + 6 ~8 Task 2–3、6
C 行内纸质 2 动态 Task 5
合计 ~31 文件 ~40+

不在范围(二期): 类型 D 8 文件、类型 E 3 文件 — 见设计文档 §2.4。


执行选项

Plan 已保存至 docs/superpowers/plans/2026-07-01-attachment-drag-upload.md。

  1. Subagent 驱动(推荐) — 按 Task 0→6 逐批派发,每批独立 PR/提交
  2. 同会话连续执行 — 从 Task 0 开始直接改代码

请确认是否从 Task 0(composable + 样式 + OpenSpec) 开始实施。