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

238 lines
8.8 KiB
Markdown
Raw 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.
# 备货单车辆明细附件上传 实现计划
> **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` 之后)加:
```ts
attachment_files?: { name: string; url: string; file_name: string }[]
```
- [ ] **Step 2: `newItem()` 初始化**
在 `const newItem = (): ItemRow => ({ ... })` 对象内(如 `remark: ''` 之后)加:
```ts
attachment_files: [],
```
- [ ] **Step 3: 新增上传 action 与方法**
在 `handleContractUpload` 之后(约 line 706 后)插入:
```ts
// 车辆明细附件上传
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 后、`}))` 之前)加:
```ts
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` 等同层)加,兼容后端两种返回格式:
```ts
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)对象内加:
```ts
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**
```bash
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)加:
```ts
uploadHeaders,
itemUploadAction,
beforeItemAttachmentUpload,
handleItemAttachmentUpload,
removeItemAttachment
```
- [ ] **Step 2: 明细行加「附件」ElCol**
在车辆明细 `v-for="(item, idx) in items"` 的行内(建议放在「里程」`ElCol` 之后,约 line 196 后)加:
```html
<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**
```bash
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}` 结构),前端据此回显。