Files
art-design/docs/superpowers/plans/2026-08-06-stock-item-attachment-upload.md
T
2026-08-06 14:36:44 +08:00

8.8 KiB
Raw Blame History

备货单车辆明细附件上传 实现计划

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: 在 Add.vue 车辆明细每行增加行内附件上传,附件随车辆提交到 stock_order_items.attachment。

Architecture: 数据层改共用的 useStockingForm.ts:ItemRow 加 attachment_files(展示用 FileItem[])、newItem 初始化、mappedItems 映射成 attachment({url, original_name} 数组,与 contract 实际格式一致)、新增上传/删除方法并暴露 uploadHeaders/itemUploadAction。UI 在 Add.vue 明细行加一个 ElCol:el-upload 按钮 + 已传文件 el-tag 列表。上传走现成 /ticket/upload?scene=stock_orders,无需改上传接口。

Tech Stack: Vue 3 <script setup>、TypeScript、element-plus 2.8、useStockingForm composable、useUploadHeaders(已导入)。

Global Constraints

  • 无前端测试框架:验收 = npm run build(含 vue-tsc --noEmit 类型检查)通过 + dev 手动核对。
  • attachment 提交格式:{ url, original_name } 对象数组(参照 baseForm.contract 实际代码 useStockingForm.ts:697-700),不是 file_name 数组——这样回填/预览有 url 可用。
  • useUploadHeaders 已在 useStockingForm.ts:6 导入、:155 取出 uploadHeaders,直接复用、暴露。
  • 后端 stock_order_items.attachment 字段已加(DB 就绪);接口层需对接 veh_list[].attachment 的接收与返回。
  • commit 规范:每任务一次 commit,feat: 开头。

文件结构

文件 责任 改动
src/composables/useStockingForm.ts 车辆表单数据/上传方法(Add/edit 共用) Modify
src/views/crmpc/stocking/Add.vue 添加备货单页,车辆明细行内附件 UI Modify

Task 1: useStockingForm.ts 数据层 + 上传方法

Files:

  • Modify: src/composables/useStockingForm.ts(ItemRow 接口 ~49、newItem ~218、上传方法区 ~706、mappedItems ~1234、populateFromDetail items 映射 ~974、return ~1515)

Interfaces:

  • Consumes: useUploadHeaders(已导入)、items: ref<ItemRow[]>(已有)、/ticket/upload 返回 {code, data:{original_name, file_url, file_name}}。

  • Produces: ItemRow.attachment_files;暴露 uploadHeaders/itemUploadAction/beforeItemAttachmentUpload/handleItemAttachmentUpload/removeItemAttachment。

  • Step 1: ItemRow 接口加字段

在 export interface ItemRow { ... } 内(如 remark: string 之后)加:

  attachment_files?: { name: string; url: string; file_name: string }[]
  • Step 2: newItem() 初始化

在 const newItem = (): ItemRow => ({ ... }) 对象内(如 remark: '' 之后)加:

    attachment_files: [],
  • Step 3: 新增上传 action 与方法

在 handleContractUpload 之后(约 line 706 后)插入:

  // 车辆明细附件上传
  const itemUploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=stock_orders`

  const beforeItemAttachmentUpload = (file: File) => {
    const isValidType = /\.(jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt)$/i.test(file.name)
    if (!isValidType) {
      ElMessage.error('只能上传图片或文档文件!')
      return false
    }
    const isLt10M = file.size / 1024 / 1024 < 10
    if (!isLt10M) {
      ElMessage.error('文件大小不能超过 10MB!')
      return false
    }
    return true
  }

  const handleItemAttachmentUpload = (response: any, idx: number) => {
    if (response.code === 200) {
      const fileInfo = {
        name: response.data.original_name || '附件',
        url: response.data.file_url,
        file_name: response.data.file_name
      }
      if (!items.value[idx].attachment_files) {
        items.value[idx].attachment_files = []
      }
      items.value[idx].attachment_files!.push(fileInfo)
      ElMessage.success('附件上传成功!')
    } else {
      ElMessage.error(response.message || '附件上传失败!')
    }
  }

  const removeItemAttachment = (idx: number, fileIdx: number) => {
    const list = items.value[idx].attachment_files
    if (list) {
      list.splice(fileIdx, 1)
      ElMessage.success('附件已删除')
    }
  }
  • Step 4: mappedItems 映射 attachment

在 mappedItems 的 increase_price: wrapMoney(...) 之后(约 line 1234 后、})) 之前)加:

      attachment: (it.attachment_files || []).map((f) => ({
        url: f.url,
        original_name: f.name
      }))
  • Step 5: populateFromDetail 回填

在 populateFromDetail 中 items.value = list.map((it: any) => ({ ... })) 的映射对象内(与 model_snapshot 等同层)加,兼容后端两种返回格式:

      attachment_files: (it.attachment || []).map((f: any) => ({
        name: f.original_name || f.name || '附件',
        url: f.url || f.file_url || '',
        file_name: f.file_name || f.url || ''
      }))
  • Step 6: return 暴露

在 return {(约 line 1515)对象内加:

    uploadHeaders,
    itemUploadAction,
    beforeItemAttachmentUpload,
    handleItemAttachmentUpload,
    removeItemAttachment,
  • Step 7: 类型检查(阶段性)

Run: npx vue-tsc --noEmit 2>&1 | grep -E "useStockingForm|error TS" | head Expected: 无 useStockingForm.ts 相关 error。(dayjs 等无关报错忽略)

  • Step 8: Commit
git add src/composables/useStockingForm.ts
git commit -m "feat(stocking): useStockingForm 支持车辆明细附件上传

ItemRow 增加 attachment_files,mappedItems 映射 attachment,
新增车辆附件上传/删除方法。

Co-Authored-By: Claude <noreply@anthropic.com>"

Task 2: Add.vue 行内附件 UI

Files:

  • Modify: src/views/crmpc/stocking/Add.vue(解构 ~1189-1211、车辆明细行 ~116+)

Interfaces:

  • Consumes: Task 1 暴露的 uploadHeaders/itemUploadAction/beforeItemAttachmentUpload/handleItemAttachmentUpload/removeItemAttachment;items、idx(已有)。

  • Produces: 每行附件上传交互。

  • Step 1: 解构新方法

在 Add.vue 从 useStockingForm(false) 解构的块内(chooseVehicle 等同处,约 line 1206-1210)加:

    uploadHeaders,
    itemUploadAction,
    beforeItemAttachmentUpload,
    handleItemAttachmentUpload,
    removeItemAttachment
  • Step 2: 明细行加「附件」ElCol

在车辆明细 v-for="(item, idx) in items" 的行内(建议放在「里程」ElCol 之后,约 line 196 后)加:

            <ElCol :sm="12" :md="12" :lg="6" :xl="4">
              <ElFormItem label="附件" label-width="85px">
                <el-upload
                  :action="itemUploadAction"
                  :headers="uploadHeaders"
                  :show-file-list="false"
                  :before-upload="beforeItemAttachmentUpload"
                  :on-success="(res: any) => handleItemAttachmentUpload(res, idx)"
                  multiple
                  accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt"
                >
                  <el-button size="small" type="primary" plain>上传附件</el-button>
                </el-upload>
                <div
                  v-if="item.attachment_files && item.attachment_files.length"
                  style="margin-top: 4px"
                >
                  <el-tag
                    v-for="(f, fIdx) in item.attachment_files"
                    :key="fIdx"
                    closable
                    @close="removeItemAttachment(idx, fIdx)"
                    style="margin: 2px 4px 2px 0"
                  >
                    {{ f.name }}
                  </el-tag>
                </div>
              </ElFormItem>
            </ElCol>
  • Step 3: 类型检查 + 构建

Run: npm run build Expected: vue-tsc 无 error,vite build 产出 dist。

  • Step 4: 手动核对(dev)

Run: npm run dev,进入添加备货单页。 核对:每辆车行有「上传附件」按钮;选文件上传后出现文件名 tag;tag × 可删;提交时 Network veh_list[].attachment 为 {url, original_name} 数组。

  • Step 5: Commit
git add src/views/crmpc/stocking/Add.vue
git commit -m "feat(stocking): 添加备货单车辆明细行内附件上传

Co-Authored-By: Claude <noreply@anthropic.com>"

后端联调清单(交付后端)

  • stock_order_items.attachment 字段已就绪。
  • store/update:接收 veh_list[].attachment({url, original_name} 对象数组),写入 attachment(json 序列化)。
  • show/detail:返回 items[].attachment(同 {url, original_name} 结构),前端据此回显。