6f80f5126c
Co-Authored-By: Claude <noreply@anthropic.com>
238 lines
8.8 KiB
Markdown
238 lines
8.8 KiB
Markdown
# 备货单车辆明细附件上传 实现计划
|
||
|
||
> **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}` 结构),前端据此回显。
|