13 KiB
附件拖拽上传 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。
- Subagent 驱动(推荐) — 按 Task 0→6 逐批派发,每批独立 PR/提交
- 同会话连续执行 — 从 Task 0 开始直接改代码
请确认是否从 Task 0(composable + 样式 + OpenSpec) 开始实施。