Compare commits
17 Commits
786082305d
...
9c49da6cc5
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c49da6cc5 | |||
| 2b925a1d37 | |||
| d695281424 | |||
| 2c6a2dafec | |||
| 6ab4fc9b9a | |||
| 77ac384d29 | |||
| 0dfc9a8bcc | |||
| 6e23f83e78 | |||
| b3f6d591b5 | |||
| bd1524f4f1 | |||
| f809bf05e3 | |||
| 6f80f5126c | |||
| b5d414d9af | |||
| a275a98671 | |||
| 1d9ef001c1 | |||
| aa23d0949b | |||
| 76baee2fe5 |
@@ -0,0 +1,237 @@
|
||||
# 备货单车辆明细附件上传 实现计划
|
||||
|
||||
> **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}` 结构),前端据此回显。
|
||||
@@ -0,0 +1,272 @@
|
||||
# 备货单 / 终版列表「全部 / 我的」视图切换 实现计划
|
||||
|
||||
> **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:** 在 `stocking_list.vue` 与 `final_list.vue` 给 `userId === 73` 的特权用户增加「全部 / 我的」视图切换,切换时给列表接口传 `view_scope=all|self`。
|
||||
|
||||
**Architecture:** 纯前端 UI + 传参改动。新增本地状态 `viewScope`,工具栏渲染条件 `canViewAll = userStore.getUserInfo.userId === 73`;`getList` 把 `viewScope.value` 作为 `view_scope` 随请求发出;切换时重置页码并重新请求。是否返回全量数据由后端按 token 鉴权决定(后端改造见 spec 第 3.4 节,不在本计划内)。
|
||||
|
||||
**Tech Stack:** Vue 3 `<script setup>` + TypeScript、element-plus 2.8、Pinia(`useUserStore`)、`unplugin-auto-import`(`ref/reactive/computed/onMounted/watch` 自动导入,无需手写 import)。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **无前端单元测试框架**:本项目 `package.json` 无 `test` 脚本。验收方式 = `lint` 通过 + 本地 `dev` 手动核对,不写自动化测试。
|
||||
- **后端是前置依赖**:前端「全部」真正生效需后端 `GET /crm/stock/order/list` 支持 `view_scope` 参数(见 spec 3.4)。前端可先行独立完成 UI 与传参;完整功能联调待后端就绪。
|
||||
- **安全边界在后端**:前端 `userId === 73` 仅控制按钮显隐,非授权依据。
|
||||
- **代码风格**:遵循现有写法——`<script setup lang="ts">`、element-plus 组件、snake_case 请求参数。
|
||||
- **commit 规范**:每任务一次 commit,信息以 `feat:` 开头。
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 责任 | 改动类型 |
|
||||
|---|---|---|
|
||||
| `src/views/crmpc/stocking/stocking_list.vue` | 备货单列表,加视图切换 + `view_scope` 传参 | Modify |
|
||||
| `src/views/crmpc/stocking/final_list.vue` | 终版列表,同构改动 | Modify |
|
||||
|
||||
两文件改动完全同构,仅 `is_final`(0 vs 1)不同。不新建文件。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: stocking_list.vue 增加视图切换
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/crmpc/stocking/stocking_list.vue`
|
||||
- 模板工具栏(约 `line 9-22` 的 `toolbar-right`)
|
||||
- `getList` 参数(约 `line 339-344`)
|
||||
- script 状态(约 `line 280-281` `useAuth` 之后)
|
||||
- script import(约 `line 264-274`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useUserStore`(来自 `@/store/modules/user`),其 `getUserInfo: computed => Partial<{ userId: number }>`;`CrmService.getStockList(params)`(已存在)。
|
||||
- Produces: 列表请求新增参数字段 `view_scope: 'all' | 'self'`。
|
||||
|
||||
- [ ] **Step 1: 添加 import**
|
||||
|
||||
在 `<script setup lang="ts">` 顶部 import 区(`import { useAuth } from '@/composables/useAuth'` 同处)增加一行:
|
||||
|
||||
```ts
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加状态与切换处理**
|
||||
|
||||
在 `const { hasAuth } = useAuth()`(约 line 281)之后插入:
|
||||
|
||||
```ts
|
||||
const userStore = useUserStore()
|
||||
// 仅 userId=73 的特权用户可切换「全部/我的」
|
||||
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
|
||||
const viewScope = ref<'all' | 'self'>('self')
|
||||
const onScopeChange = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
```
|
||||
|
||||
说明:`computed` / `ref` 由 `unplugin-auto-import` 提供,无需 import。`getUserInfo.userId` 类型为 `number | undefined`,`undefined === 73` 为 `false`,判空安全。
|
||||
|
||||
- [ ] **Step 3: 修改 `getList` 请求参数**
|
||||
|
||||
定位 `getList` 内(约 line 339-344):
|
||||
|
||||
```ts
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 0, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
}
|
||||
```
|
||||
|
||||
改为(增加 `view_scope`):
|
||||
|
||||
```ts
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 0, // 只获取备货单,非终版
|
||||
keyword: keyword.value,
|
||||
view_scope: viewScope.value
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 工具栏添加切换控件**
|
||||
|
||||
定位模板 `toolbar-right`(约 line 9-21),在 `<el-input>` 之前插入 `el-radio-group`:
|
||||
|
||||
原:
|
||||
|
||||
```html
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```html
|
||||
<div class="toolbar-right">
|
||||
<el-radio-group
|
||||
v-if="canViewAll"
|
||||
v-model="viewScope"
|
||||
@change="onScopeChange"
|
||||
style="margin-right: 12px"
|
||||
>
|
||||
<el-radio-button value="self">我的</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
```
|
||||
|
||||
说明:用 `value`(element-plus 2.6+ 推荐)而非 `label` 作值,避免 2.8 的 label 弃用告警。文字走默认插槽。
|
||||
|
||||
- [ ] **Step 5: Lint 校验**
|
||||
|
||||
Run: `npm run fix -- src/views/crmpc/stocking/stocking_list.vue`
|
||||
Expected: 无 error。若有,按提示修复(常见:prettier 格式)。
|
||||
|
||||
- [ ] **Step 6: 手动核对(前端独立部分)**
|
||||
|
||||
Run: `npm run dev`,浏览器打开备货单列表页。
|
||||
|
||||
核对项:
|
||||
1. 用**非 73 账号**登录:工具栏**不出现**「全部/我的」控件,列表行为同改动前。
|
||||
2. 打开 DevTools Network,看 `/crm/stock/order/list` 请求 Payload:无论是否 73,都应带上 `view_scope=self`(默认值)。
|
||||
3. (需 73 登录态)用 73 账号登录:出现「全部/我的」,默认选中「我的」;点「全部」后 Network 请求带 `view_scope=all`,点回「我的」带 `view_scope=self`,且每次切换分页回到第 1 页。
|
||||
4. 「全部」是否真返回全量数据,取决于后端是否已支持——后端未就绪时,切到「全部」数据可能不变,属预期。
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/views/crmpc/stocking/stocking_list.vue
|
||||
git commit -m "feat(stocking): 备货单列表为特权用户增加「全部/我的」视图切换
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: final_list.vue 增加视图切换(同构)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/crmpc/stocking/final_list.vue`
|
||||
- 模板工具栏(约 `line 7-19` 的 `toolbar-right`)
|
||||
- `getList` 参数(约 `line 278-283`)
|
||||
- script 状态(约 `line 229` `useAuth` 之后)
|
||||
- script import(约 `line 206-216`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 同 Task 1(`useUserStore`、`CrmService.getStockList`)。
|
||||
- Produces: 同 Task 1(`view_scope` 参数)。
|
||||
|
||||
- [ ] **Step 1: 添加 import**
|
||||
|
||||
在 `<script setup lang="ts">` 顶部 import 区增加:
|
||||
|
||||
```ts
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加状态与切换处理**
|
||||
|
||||
在 `const { hasAuth } = useAuth()`(约 line 229)之后插入:
|
||||
|
||||
```ts
|
||||
const userStore = useUserStore()
|
||||
// 仅 userId=73 的特权用户可切换「全部/我的」
|
||||
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
|
||||
const viewScope = ref<'all' | 'self'>('self')
|
||||
const onScopeChange = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 `getList` 请求参数**
|
||||
|
||||
定位 `getList` 内(约 line 278-283):
|
||||
|
||||
```ts
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 1, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```ts
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 1, // 终版
|
||||
keyword: keyword.value,
|
||||
view_scope: viewScope.value
|
||||
}
|
||||
```
|
||||
|
||||
(注:原代码注释「非终版」是笔误,此处顺手修正为「终版」。)
|
||||
|
||||
- [ ] **Step 4: 工具栏添加切换控件**
|
||||
|
||||
定位模板 `toolbar-right`(约 line 7-19),在 `<el-input>` 之前插入:
|
||||
|
||||
原:
|
||||
|
||||
```html
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```html
|
||||
<div class="toolbar-right">
|
||||
<el-radio-group
|
||||
v-if="canViewAll"
|
||||
v-model="viewScope"
|
||||
@change="onScopeChange"
|
||||
style="margin-right: 12px"
|
||||
>
|
||||
<el-radio-button value="self">我的</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Lint 校验**
|
||||
|
||||
Run: `npm run fix -- src/views/crmpc/stocking/final_list.vue`
|
||||
Expected: 无 error。
|
||||
|
||||
- [ ] **Step 6: 手动核对(前端独立部分)**
|
||||
|
||||
Run: `npm run dev`,浏览器打开终版列表页。核对项同 Task 1 Step 6(注意此处是 `is_final=1` 的终版页)。
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/views/crmpc/stocking/final_list.vue
|
||||
git commit -m "feat(stocking): 终版列表为特权用户增加「全部/我的」视图切换
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端联调清单(交付后端,不在本计划执行范围)
|
||||
|
||||
接口 `GET /crm/stock/order/list` 增加可选参数 `view_scope`:
|
||||
|
||||
| 请求 | 后端行为 |
|
||||
|---|---|
|
||||
| 不传 / `view_scope=self` | 按现状:依 token 用户过滤 |
|
||||
| `view_scope=all` 且 token 用户 id ∈ {73} | 返回全量 |
|
||||
| `view_scope=all` 且 token 用户 id ∉ {73} | 降级为 `self`(返回该用户自己的) |
|
||||
|
||||
要点:**只信 token,忽略前端任何用户标识**,防 IDOR。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 备货单车辆明细附件上传 设计
|
||||
|
||||
- 日期:2026-08-06
|
||||
- 分支:feature/crm_pc
|
||||
- 涉及前端:`src/views/crmpc/stocking/Add.vue`、`src/composables/useStockingForm.ts`
|
||||
- 涉及后端:`stock_order_items.attachment`(已加字段)、备货单 store/update/show 接口
|
||||
|
||||
## 1. 背景
|
||||
|
||||
添加备货单(`Add.vue`)时,车辆明细(`items`,每行一辆车)目前只能录入车型 / 颜色 / 价格等字段,**没有附件**。业务需要每辆车能上传若干附件(如该车相关文件),随车辆一起保存。
|
||||
|
||||
后端 `stock_order_items` 表已新增 `attachment` 字段(本次由后端同事完成)。
|
||||
|
||||
## 2. 需求
|
||||
|
||||
- 添加备货单时,车辆明细**每辆车**可上传**多个**附件。
|
||||
- UI 为**行内紧凑**形态:每行一个「上传附件」入口,已传文件以文件名标签列出、可删除,不撑高行高。
|
||||
- 附件随车辆数据一起提交,存入 `stock_order_items.attachment`。
|
||||
- 字段名统一用 `attachment`,值为 `file_name` 字符串数组(与备货单级 `contract`/`pi_doc`/`transaction_slip` 的存储模式一致)。
|
||||
|
||||
## 3. 现状(复用基础)
|
||||
|
||||
- 车辆明细 `items` 由 `useStockingForm(false)` 提供,每行 `item` 是 `ItemRow`。
|
||||
- 备货单级文件已有成熟模式:前端维护 `FileItem[]`(`{name, url, file_name}`,带 url 供预览/下载),提交时映射成 `file_name[]` 存入 `baseForm.contract` 等。
|
||||
- 上传走通用接口 `${VITE_API_URL}/ticket/upload?scene=stock_orders`,header 带 token(`useUploadHeaders`),成功返回 `{ data: { original_name, file_url, file_name } }`。**此接口无需改动。**
|
||||
- 现成组件 `MultiFileUpload`(120px 大拖拽区)适合整页文件区,不适合塞进行内——故行内用 `el-upload` 直接内联。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 数据层 `useStockingForm.ts`
|
||||
|
||||
- `ItemRow` 类型增加 `attachment_files: { name: string; url: string; file_name: string }[]`。
|
||||
- `newItem()` 初始化 `attachment_files: []`。
|
||||
- 提交映射 `mappedItems` 增加:
|
||||
```ts
|
||||
attachment: (it.attachment_files || []).map((f) => f.file_name)
|
||||
```
|
||||
- 详情回填 `populateFromDetail`:从后端返回的 `it.attachment` 反解为 `attachment_files`(格式见 4.4,Add 为新建不走回填,但 composable 与 edit 共享,一并支持)。
|
||||
- `copyRow` 已是 `JSON.parse(JSON.stringify(...))`,自动带上附件,无需改。
|
||||
- 草稿 `pickDraftPayload` 已含 `items: toRaw(items.value)`,附件自然入草稿,无需改。
|
||||
- 新增车辆附件上传/删除方法(仿现有 `handleContractUpload` 等):
|
||||
- `handleItemAttachmentUpload(response: any, idx: number)`:校验 `response.code===200`,把 `{ name: data.original_name, url: data.file_url, file_name: data.file_name }` push 进 `items.value[idx].attachment_files`。
|
||||
- `removeItemAttachment(idx: number, fileIdx: number)`:从 `items.value[idx].attachment_files` 删除指定项。
|
||||
|
||||
### 4.2 UI 层 `Add.vue`
|
||||
|
||||
在车辆明细 `v-for="(item, idx) in items"` 的行内末尾增加一个 `ElCol`「附件」:
|
||||
|
||||
```html
|
||||
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
|
||||
<ElFormItem label="附件" label-width="85px">
|
||||
<el-upload
|
||||
:action="`${VITE_API_URL}/ticket/upload?scene=stock_orders`"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:before-upload="(file) => beforeItemAttachmentUpload(file)"
|
||||
:on-success="(res) => handleItemAttachmentUpload(res, idx)"
|
||||
multiple
|
||||
:accept="itemAttachmentAccept"
|
||||
>
|
||||
<el-button size="small" type="primary" plain>上传附件</el-button>
|
||||
</el-upload>
|
||||
<div class="item-attachment-tags" v-if="item.attachment_files?.length">
|
||||
<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>
|
||||
```
|
||||
|
||||
- `VITE_API_URL`、`uploadHeaders`(`useUploadHeaders`)、`beforeItemAttachmentUpload`(类型/大小校验,复用 `MultiFileUpload` 的 accept 与 10MB 限制)、`handleItemAttachmentUpload`、`removeItemAttachment` 均由 `useStockingForm` return,模板解构使用,与现有 `contractFiles` 等用法一致。
|
||||
|
||||
### 4.3 后端配合
|
||||
|
||||
- `stock_order_items.attachment` 字段已加(DB 就绪)。
|
||||
- 接口层需对接:
|
||||
- **store / update**:接收 `items[].attachment`(`file_name` 数组),写入 `stock_order_items.attachment`(按 json/序列化存储)。
|
||||
- **show / detail**:返回 `items[].attachment`(`file_name` 数组,或含 `original_name`/`file_url` 的对象数组——见 4.4 约定)。
|
||||
|
||||
### 4.4 回填格式约定(需与后端对齐)
|
||||
|
||||
详情接口返回的 `items[].attachment` 有两种可能,择一与后端确认:
|
||||
- **A**:`file_name` 字符串数组(如 `["xxx.pdf"]`)——前端回填时 `url` 需另行拼接,预览受限。
|
||||
- **B(推荐)**:对象数组(如 `[{ original_name, file_url, file_name }]`,与 `/ticket/upload` 返回一致)——前端直接还原成 `attachment_files`,预览/下载可用。
|
||||
|
||||
实现(plan)阶段以前端实际请求详情接口的返回为准;若为 A,前端按现有 `populateFromDetail` 中文件字段的拼 url 方式处理。
|
||||
|
||||
## 5. 安全 / 限制
|
||||
|
||||
- 复用 `beforeUpload`:类型限制 `.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt`,单文件 ≤ 10MB。
|
||||
- 上传 header 带 token(`useUploadHeaders`),鉴权由后端 `/ticket/upload` 既有机制处理。
|
||||
- `attachment` 只存 `file_name`,不含敏感信息。
|
||||
|
||||
## 6. 改动清单
|
||||
|
||||
| 端 | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| 前端 | `src/composables/useStockingForm.ts` | `ItemRow` 加 `attachment_files`;`newItem` 初始化;`mappedItems` 映射 `attachment`;`populateFromDetail` 回填;新增 `handleItemAttachmentUpload`/`removeItemAttachment`/`beforeItemAttachmentUpload`/暴露 `uploadHeaders` |
|
||||
| 前端 | `src/views/crmpc/stocking/Add.vue` | 车辆明细行末尾加「附件」`ElCol`(`el-upload` + 标签列表);解构新方法 |
|
||||
| 后端 | store/update/show 接口 | 对接 `items[].attachment`(DB 字段已就绪) |
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 添加备货单:每辆车行有「上传附件」按钮,点选多文件逐个上传成功后,文件名以标签列出。
|
||||
2. 标签可点 × 删除;删除后提交不包含该文件。
|
||||
3. 提交备货单,Network 里 `items[].attachment` 为对应 `file_name` 数组;后端 `stock_order_items.attachment` 落库正确。
|
||||
4. (后端 show 返回后)进入编辑/详情,已存附件能回显为标签列表。
|
||||
5. 超格式 / 超 10MB 文件被拦截并提示。
|
||||
|
||||
## 8. 未做项 / 后续
|
||||
|
||||
- 本次只改 `Add.vue` 模板;`stocking_edit.vue` 共用 `useStockingForm`,数据层已支持,如需在编辑页也展示附件上传,后续加同款 `ElCol` 即可。
|
||||
- 终版(final_add/final_edit)是否需要同等能力,待业务确认。
|
||||
@@ -0,0 +1,99 @@
|
||||
# 备货单 / 终版列表「全部 / 我的」视图切换 设计
|
||||
|
||||
- 日期:2026-08-06
|
||||
- 分支:feature/crm_pc
|
||||
- 涉及页面:`src/views/crmpc/stocking/stocking_list.vue`、`src/views/crmpc/stocking/final_list.vue`
|
||||
- 涉及接口:`GET /crm/stock/order/list`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
备货单列表(`stocking_list`,`is_final=0`)与终版列表(`final_list`,`is_final=1`)当前共用同一个列表接口 `GET /crm/stock/order/list`。后端依据请求 token 中的用户 id 过滤数据,即每个登录用户只能看到自己作为业务员的备货单 / 终版。前端请求参数只有 `current / size / is_final / keyword`,无任何范围控制。
|
||||
|
||||
业务上存在一个固定特权用户(user_id = 73),需要在「看自己」之外,额外能查看「全公司所有备货单 / 终版」。
|
||||
|
||||
## 2. 需求
|
||||
|
||||
- 给 user_id = 73 这一个用户,在两个列表页增加「全部 / 我的」视图切换。
|
||||
- 选「全部」时,该用户看到全公司数据;选「我的」时,行为同现状(只看自己)。
|
||||
- 其他用户(非 73)完全不受影响:不显示切换控件,行为同现状。
|
||||
- 默认视图为「我的」。
|
||||
|
||||
## 3. 设计
|
||||
|
||||
### 3.1 UI
|
||||
|
||||
- 在两个列表页的工具栏增加一组互斥切换控件「全部 / 我的」(`el-radio-group` button 样式,二选一)。
|
||||
- 控件位置:工具栏右侧、搜索框左边,两个页面位置保持一致(`final_list` 现有 `toolbar-left` 被注释,不在那里放,统一放右侧,改动更小)。
|
||||
- 渲染条件:`useUserStore().getUserInfo.userId === 73`。`getUserInfo` 是 `userStore.info` 的 computed,类型 `Partial<UserInfo>`,`userId` 可能为 `undefined`,`undefined === 73` 为 `false`,判断安全,无需额外判空。
|
||||
|
||||
### 3.2 前端逻辑
|
||||
|
||||
- 新增本地状态 `viewScope = ref<'all' | 'self'>('self')`,初值 `'self'`(即默认「我的」)。
|
||||
- 切换控件绑定 `viewScope`;`@change` 时:
|
||||
1. `pagination.currentPage = 1`(切回第一页,避免越界空页);
|
||||
2. 调用 `getList()`。
|
||||
- `getList()` 构造参数时,当 `userId === 73` 把 `viewScope.value` 作为 `view_scope` 一并传入:
|
||||
- 「全部」→ `view_scope: 'all'`
|
||||
- 「我的」→ `view_scope: 'self'`
|
||||
- 非 73 用户:不渲染控件,`viewScope` 恒为 `'self'`,传 `view_scope: 'self'`(或不传,见 3.3)。为简单起见统一传,后端对非特权用户会忽略该值。
|
||||
|
||||
### 3.3 请求参数
|
||||
|
||||
列表请求参数由
|
||||
|
||||
```jsonc
|
||||
{ "current": 1, "size": 10, "is_final": 0, "keyword": "" }
|
||||
```
|
||||
|
||||
扩展为(73 用户)
|
||||
|
||||
```jsonc
|
||||
{ "current": 1, "size": 10, "is_final": 0, "keyword": "", "view_scope": "all" }
|
||||
```
|
||||
|
||||
- 参数名 `view_scope` 沿用现有 snake_case 风格(与 `is_final` / `keyword` 一致)。
|
||||
- 取值为语义化的 `'all' | 'self'`,**不传用户 id**。
|
||||
|
||||
### 3.4 后端配合
|
||||
|
||||
`GET /crm/stock/order/list` 增加可选参数 `view_scope`,处理逻辑:
|
||||
|
||||
| 请求情况 | 后端行为 |
|
||||
|---|---|
|
||||
| 不传 / `view_scope=self` | 现状逻辑:按 token 用户过滤,返回自己的数据 |
|
||||
| `view_scope=all` 且 token 用户 id ∈ {73} | 返回全公司全量数据 |
|
||||
| `view_scope=all` 且 token 用户 id ∉ {73} | 降级:当作 `self` 处理,返回该用户自己的数据(忽略 `all`) |
|
||||
|
||||
后端「可看全部」白名单暂写死为 `{73}`,与前端判断一致。
|
||||
|
||||
### 3.5 默认值
|
||||
|
||||
73 用户打开页面,`viewScope` 初值 `'self'`,即默认「我的」。需看全部时手动切换。
|
||||
|
||||
## 4. 安全考量
|
||||
|
||||
- **后端只认 token**:是否返回全量数据,由后端依据 token 中的用户 id 判断,绝不信任前端传入的任何用户标识。前端 `userId` 存于 localStorage,可被篡改。
|
||||
- 前端写死 `userId === 73` 仅用于**控件显隐**的便捷实现,不是安全边界。即便有人改前端强制显示按钮并传 `view_scope=all`,后端鉴权不通过仍只返回其自己的数据。
|
||||
- 结论:语义化参数 + 后端 token 鉴权,可兜住水平越权(IDOR)风险。
|
||||
|
||||
## 5. 改动清单
|
||||
|
||||
| 端 | 文件 / 接口 | 改动 |
|
||||
|---|---|---|
|
||||
| 前端 | `stocking_list.vue` | 工具栏加「全部/我的」`el-radio-group`(仅 73 显示);`getList` 传 `view_scope`;切换重置页码并重新请求 |
|
||||
| 前端 | `final_list.vue` | 同上(`is_final=1`) |
|
||||
| 后端 | `GET /crm/stock/order/list` | 接收 `view_scope`;token 用户 ∈ {73} 且 `=all` 时返回全量,否则按现状过滤 |
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. 以非 73 账号登录两个列表页:不出现「全部/我的」控件,数据范围与改动前一致。
|
||||
2. 以 73 账号登录:
|
||||
- 默认显示「我的」选中态,列表为 73 自己的数据。
|
||||
- 切到「全部」,列表变为全公司数据;切回「我的」恢复。
|
||||
- 切换时分页重置到第 1 页。
|
||||
3. 用任意账号在浏览器手动把请求改成 `view_scope=all`:后端只返回该账号自己的数据(非 73 不越权)。
|
||||
|
||||
## 7. 未做项 / 后续 TODO
|
||||
|
||||
- 前端 `userId === 73` 为硬编码,加人需改前端发版。后续建议:`getUserInfo` 下发权限标记(如 `stock@view_all` / `final_stock@view_all`),前端用现有 `hasAuth(...)` 控制显隐;后端白名单同步改为权限点判断。本次保留此扩展点,先不动。
|
||||
- `view_scope` 选择不持久化(每次进页面回到默认「我的」)。
|
||||
@@ -0,0 +1,104 @@
|
||||
# 备货单车辆明细附件上传 — 后端对接说明
|
||||
|
||||
> 前端已完成:添加备货单页,每辆车行有「上传附件」入口。本文档仅说明后端需配合的部分。
|
||||
|
||||
## 1. 功能说明
|
||||
|
||||
备货单(`stock_orders`)的车辆明细(`stock_order_items`),**每辆车**需要支持上传若干附件(如该车相关单据)。附件随车辆一起保存、详情回显。
|
||||
|
||||
## 2. 数据库(已就绪)
|
||||
|
||||
`stock_order_items` 表已新增 `attachment` 字段。
|
||||
|
||||
- 字段名:`attachment`
|
||||
- 类型:建议 `varchar` / `text` / `json`,存 **JSON 字符串**
|
||||
- 含义:该车辆的附件列表
|
||||
|
||||
## 3. 字段格式约定
|
||||
|
||||
`attachment` 存「对象数组」的 JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "url": "https://xxx.com/file.pdf", "original_name": "装箱单.pdf" }
|
||||
]
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `url` | string | 文件访问地址(即 `/ticket/upload` 返回的 `file_url`) |
|
||||
| `original_name` | string | 原始文件名 |
|
||||
|
||||
- 每辆车 0~N 个附件;无附件时为空数组 `[]` 或 `null`。
|
||||
|
||||
> **重要**:此格式与备货单级 `contract` 字段**完全一致**(`contract` 也是 `[{url, original_name}]`)。后端可直接复用现有处理 `contract` 的读写逻辑,区别仅在于:`attachment` 挂在 `veh_list` 每一项、落到 `stock_order_items` 表。
|
||||
|
||||
## 4. 需要修改的接口
|
||||
|
||||
均在前端调用路径 `/crm/stock/order/` 下。
|
||||
|
||||
### 4.1 新增 `POST /crm/stock/order/store`
|
||||
- 请求体 `veh_list` 每一项现在多一个 `attachment` 字段(`[{url, original_name}]`)。
|
||||
- 解析 `veh_list` 入库时,把每项 `attachment` 写入对应 `stock_order_items` 行的 `attachment` 字段(JSON 序列化)。
|
||||
|
||||
### 4.2 编辑 `POST /crm/stock/order/update`
|
||||
- 同 `store`。
|
||||
|
||||
### 4.3 详情 `GET /crm/stock/order/show`
|
||||
- 返回的 `veh_list` 每一项需带上 `attachment`(`[{url, original_name}]`)。
|
||||
|
||||
### 4.4 详情 `GET /crm/stock/order/detail`
|
||||
- 同 `show`,返回 `veh_list` 每一项带 `attachment`。
|
||||
|
||||
## 5. 请求 / 响应示例
|
||||
|
||||
**提交(store / update)payload:**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// ...备货单级字段(含 contract / pi_doc / transaction_slip)
|
||||
"veh_list": [
|
||||
{
|
||||
"sku_id": "123",
|
||||
"model_snapshot": "丰田 卡罗拉",
|
||||
// ...其他车辆字段
|
||||
"attachment": [
|
||||
{ "url": "https://xxx/a.pdf", "original_name": "装箱单.pdf" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"account_list": [],
|
||||
"approval_status": 0
|
||||
}
|
||||
```
|
||||
|
||||
**详情(show / detail)响应:**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"veh_list": [
|
||||
{
|
||||
"sku_id": "123",
|
||||
"model_snapshot": "丰田 卡罗拉",
|
||||
"attachment": [
|
||||
{ "url": "https://xxx/a.pdf", "original_name": "装箱单.pdf" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 上传接口(无需改动)
|
||||
|
||||
`/ticket/upload`(通用文件上传)保持原样。前端已用它上传文件,拿到 `{ file_url, file_name, original_name }`,再自行拼成 `{ url, original_name }` 存进 `attachment`。**后端无需在上传接口做任何业务关联。**
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 前端添加备货单、每辆车上传附件 → 提交 → `stock_order_items.attachment` 正确落库(JSON 数组)。
|
||||
2. 调 `show` / `detail`,返回的 `veh_list` 每项 `attachment` 与落库内容一致。
|
||||
3. (编辑页后续加 UI 后)编辑备货单时,已存附件可回显、可增删。
|
||||
|
||||
## 8. 备注
|
||||
|
||||
- 本次前端只在「添加备货单」页加了上传 UI;编辑页(`stocking_edit`)共用同一套提交/回填逻辑,数据层已支持 `attachment`,编辑页 UI 后续补,**不影响后端接口契约**。
|
||||
- 终版(final)备货单如需同等能力,接口字段一致,可一并支持。
|
||||
@@ -59,6 +59,7 @@
|
||||
"@wangeditor/editor-for-vue": "next",
|
||||
"axios": "^1.7.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "^5.4.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"element-plus": "^2.8.0",
|
||||
|
||||
Generated
+1895
-5387
File diff suppressed because it is too large
Load Diff
@@ -60,9 +60,29 @@
|
||||
<template #default>
|
||||
<ElTableColumn type="expand" width="30">
|
||||
<template #default="scope">
|
||||
<div style="padding: 6px;display: flex">
|
||||
<div><strong>备注:</strong></div>
|
||||
<div><span v-html="scope.row.remark || '暂无备注'"></span></div>
|
||||
<div style="padding: 6px">
|
||||
<div style="display: flex">
|
||||
<div><strong>备注:</strong></div>
|
||||
<div><span v-html="scope.row.remark || '暂无备注'"></span></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="getItemAttachments(scope.row).length"
|
||||
style="display: flex; margin-top: 6px"
|
||||
>
|
||||
<div><strong>附件:</strong></div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px">
|
||||
<a
|
||||
v-for="(f, aIdx) in getItemAttachments(scope.row)"
|
||||
:key="aIdx"
|
||||
:href="f.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="color: #3471ff; text-decoration: none"
|
||||
>
|
||||
<i class="iconfont-sys"></i> {{ f.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -460,6 +480,25 @@
|
||||
const vehItems = computed(() => {
|
||||
return (props.orderData as any).veh_list || (props.orderData as any).items || []
|
||||
})
|
||||
|
||||
// 车辆附件(后端返回数组;兼容历史字符串数据)
|
||||
const getItemAttachments = (row: any) => {
|
||||
const raw = row?.attachment
|
||||
let list: any[] = []
|
||||
if (Array.isArray(raw)) {
|
||||
list = raw
|
||||
} else if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed)) list = parsed
|
||||
} catch {
|
||||
list = []
|
||||
}
|
||||
}
|
||||
return list
|
||||
.filter((f: any) => f && f.url)
|
||||
.map((f: any) => ({ name: f.original_name || f.name || '附件', url: f.url }))
|
||||
}
|
||||
const tableRef = ref()
|
||||
const expandWhenReady = () => {
|
||||
nextTick(() => {
|
||||
@@ -487,7 +526,8 @@
|
||||
paymentTableData,
|
||||
vehItems,
|
||||
formatPrice,
|
||||
tableRef
|
||||
tableRef,
|
||||
getItemAttachments
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface ItemRow {
|
||||
fob_transport_fee: number | undefined
|
||||
port_misc_fee: string
|
||||
remark: string
|
||||
attachment_files?: { name: string; url: string; file_name: string }[]
|
||||
ocean_freight: string
|
||||
ocean_freight_currency: string
|
||||
ocean_freight_currency_snapshot: string
|
||||
@@ -157,7 +158,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
// 回显填充抑制标记:为 true 时不触发自动计算覆盖接口值
|
||||
const isPopulating = ref(false)
|
||||
const userEditing = ref<Set<number>>(new Set()) // 跟踪用户正在编辑的行
|
||||
const manualTaxRefundRows = ref<Set<number>>(new Set()) // 跟踪用户手动编辑过预估退税额的行
|
||||
|
||||
// 选项数据
|
||||
const channelOptions = ref<any>()
|
||||
@@ -235,6 +235,7 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
fob_transport_fee: undefined,
|
||||
port_misc_fee: '',
|
||||
remark: '',
|
||||
attachment_files: [],
|
||||
ocean_freight: '',
|
||||
ocean_freight_currency: 'CNY',
|
||||
ocean_freight_currency_snapshot: '人民币',
|
||||
@@ -486,7 +487,7 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
)
|
||||
}
|
||||
|
||||
// 利润=收款价-官方指导价/二手挂牌价+优惠-手续费-改装费-配件费-fob运费-港杂-海运-佣金-挂账-购置税+预估退税
|
||||
// 利润=收款价-官方指导价/二手挂牌价+优惠-手续费-改装费-配件费-fob运费-港杂-海运-挂账-购置税+预估退税(佣金不参与)
|
||||
const calcProfit = (it: any): number => {
|
||||
const targetCurrency = 'CNY' // 固定使用人民币
|
||||
const unit = convertCurrency(it.unit_price, it.unit_price_currency, targetCurrency)
|
||||
@@ -515,11 +516,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
(it as any).ocean_freight_currency,
|
||||
targetCurrency
|
||||
)
|
||||
const commission = convertCurrency(
|
||||
it.commission ?? (it as any).commission,
|
||||
(it as any).commission_currency,
|
||||
targetCurrency
|
||||
)
|
||||
const purchaseTax = convertCurrency(
|
||||
it.purchase_tax ?? (it as any).purchase_tax,
|
||||
(it as any).purchase_tax_currency,
|
||||
@@ -569,7 +565,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
handling +
|
||||
port +
|
||||
ocean +
|
||||
commission +
|
||||
purchaseTax +
|
||||
retention +
|
||||
increase +
|
||||
@@ -598,7 +593,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
handling +
|
||||
port +
|
||||
ocean +
|
||||
commission +
|
||||
purchaseTax +
|
||||
retention +
|
||||
increase +
|
||||
@@ -620,26 +614,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
return unit - retention
|
||||
}
|
||||
|
||||
// 计算预估退税额 = (官方价 - 折扣价) * 11.5% (固定人民币)
|
||||
const calcTaxRefund = (it: any): number => {
|
||||
const mileageVal = Number(it.mileage ?? 0)
|
||||
if (mileageVal > 0) return 0
|
||||
const targetCurrency = 'CNY' // 固定使用人民币
|
||||
const official = convertCurrency(
|
||||
it.official_price ?? (it as any).guide_price,
|
||||
(it as any).guide_price_currency,
|
||||
targetCurrency
|
||||
)
|
||||
const discount = convertCurrency(
|
||||
it.discount_price ?? (it as any).discount,
|
||||
(it as any).discount_currency,
|
||||
targetCurrency
|
||||
)
|
||||
const base = official - discount
|
||||
const refund = base * 0.115
|
||||
return refund < 0 ? 0 : Number(refund.toFixed(2))
|
||||
}
|
||||
|
||||
// 计算车价:车价 = 报关价 - FOB运费 - 港杂费 - 海运费
|
||||
const calcCarPrice = (it: any): number => {
|
||||
const targetCurrency = it.car_price_currency || baseForm.settlement_currency || 'CNY'
|
||||
@@ -705,6 +679,48 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 车辆明细附件上传
|
||||
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('附件已删除')
|
||||
}
|
||||
}
|
||||
|
||||
const removeContractFile = (index: number) => {
|
||||
contractFiles.value.splice(index, 1)
|
||||
// 更新合同字段为文件名数组
|
||||
@@ -767,49 +783,14 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
const copyRow = (idx: number) => {
|
||||
const copied = JSON.parse(JSON.stringify(items.value[idx])) as ItemRow
|
||||
items.value.splice(idx + 1, 0, copied)
|
||||
// 如果原行是手动编辑的,复制的行也标记为手动编辑
|
||||
if (manualTaxRefundRows.value.has(idx)) {
|
||||
// 需要调整后续行的索引
|
||||
const newManualRows = new Set<number>()
|
||||
manualTaxRefundRows.value.forEach((rowIdx) => {
|
||||
if (rowIdx <= idx) {
|
||||
newManualRows.add(rowIdx)
|
||||
} else {
|
||||
newManualRows.add(rowIdx + 1)
|
||||
}
|
||||
})
|
||||
newManualRows.add(idx + 1) // 新复制的行
|
||||
manualTaxRefundRows.value = newManualRows
|
||||
}
|
||||
}
|
||||
|
||||
const addRowAfter = (idx: number) => {
|
||||
items.value.splice(idx + 1, 0, newItem())
|
||||
// 调整后续行的手动编辑标记索引
|
||||
const newManualRows = new Set<number>()
|
||||
manualTaxRefundRows.value.forEach((rowIdx) => {
|
||||
if (rowIdx <= idx) {
|
||||
newManualRows.add(rowIdx)
|
||||
} else {
|
||||
newManualRows.add(rowIdx + 1)
|
||||
}
|
||||
})
|
||||
manualTaxRefundRows.value = newManualRows
|
||||
}
|
||||
|
||||
const removeRow = (idx: number) => {
|
||||
items.value.splice(idx, 1)
|
||||
// 移除该行的手动编辑标记,并调整后续行的索引
|
||||
const newManualRows = new Set<number>()
|
||||
manualTaxRefundRows.value.forEach((rowIdx) => {
|
||||
if (rowIdx < idx) {
|
||||
newManualRows.add(rowIdx)
|
||||
} else if (rowIdx > idx) {
|
||||
newManualRows.add(rowIdx - 1)
|
||||
}
|
||||
// rowIdx === idx 的行被删除,不添加到新集合中
|
||||
})
|
||||
manualTaxRefundRows.value = newManualRows
|
||||
}
|
||||
|
||||
// 收款项操作方法
|
||||
@@ -930,6 +911,20 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
}
|
||||
|
||||
// 数据回显方法
|
||||
// 后端 attachment 可能返回数组或 JSON 字符串(未 decode),统一转数组
|
||||
const normalizeAttachment = (raw: any): any[] => {
|
||||
if (Array.isArray(raw)) return raw
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const populateFromDetail = (res: any) => {
|
||||
if (!res) return
|
||||
isPopulating.value = true
|
||||
@@ -977,6 +972,11 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
_echo_vin: Boolean(it.vin),
|
||||
mileage: it.mileage ?? 0,
|
||||
model_snapshot: it.model_snapshot || '',
|
||||
attachment_files: normalizeAttachment(it.attachment).map((f: any) => ({
|
||||
name: f.original_name || f.name || '附件',
|
||||
url: f.url || f.file_url || '',
|
||||
file_name: f.file_name || f.url || ''
|
||||
})),
|
||||
listing_price: it.listing_price?.amount ? Number(it.listing_price.amount) : undefined,
|
||||
listing_price_currency: it.listing_price?.currency || 'CNY',
|
||||
listing_price_currency_snapshot: '',
|
||||
@@ -1134,16 +1134,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
}
|
||||
// 等待本轮变更完成后,再恢复自动计算
|
||||
nextTick(() => {
|
||||
// 检查哪些行的预估退税额与自动计算值不同,标记为手动编辑
|
||||
items.value.forEach((it: any, index: number) => {
|
||||
const currentValue = String(it.tax_refund || '')
|
||||
const autoCalculatedValue = String(calcTaxRefund(it) || '0')
|
||||
if (currentValue !== autoCalculatedValue && currentValue !== '' && currentValue !== '0') {
|
||||
manualTaxRefundRows.value.add(index)
|
||||
}
|
||||
})
|
||||
// 初始化 prevTaxRefundValues
|
||||
prevTaxRefundValues.value = items.value.map((item: any) => String(item.tax_refund || ''))
|
||||
isPopulating.value = false
|
||||
})
|
||||
}
|
||||
@@ -1231,7 +1221,11 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
),
|
||||
retention_price: wrapMoney(it.retention_price, (it as any).retention_price_currency, '挂账'),
|
||||
car_price: wrapMoney(it.car_price, (it as any).car_price_currency, '车价'),
|
||||
increase_price: wrapMoney(it.increase_price, (it as any).increase_price_currency, '加价')
|
||||
increase_price: wrapMoney(it.increase_price, (it as any).increase_price_currency, '加价'),
|
||||
attachment: (it.attachment_files || []).map((f) => ({
|
||||
url: f.url,
|
||||
original_name: f.name
|
||||
}))
|
||||
}))
|
||||
|
||||
// 从提交参数中移除 settlement_currency
|
||||
@@ -1375,15 +1369,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 自动计算预估退税额(跳过用户手动编辑过的行)
|
||||
if (!manualTaxRefundRows.value.has(index)) {
|
||||
const refund = calcTaxRefund(it)
|
||||
if (items.value[index]) {
|
||||
items.value[index].tax_refund = isNaN(refund) ? '0' : String(Number(refund.toFixed(2)))
|
||||
;(items.value[index] as any).tax_refund_currency = 'CNY'
|
||||
}
|
||||
}
|
||||
|
||||
// 自动计算车价
|
||||
const carPrice = calcCarPrice(it)
|
||||
if (items.value[index]) {
|
||||
@@ -1433,40 +1418,15 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 存储上一次的 tax_refund 值,用于检测用户手动输入
|
||||
const prevTaxRefundValues = ref<string[]>([])
|
||||
|
||||
// 当用户手动输入预估退税额时,重新计算利润
|
||||
// 预估退税额为手动输入,输入变化时重新计算利润
|
||||
watch(
|
||||
() => items.value.map((item: any) => item.tax_refund),
|
||||
(newValues) => {
|
||||
() => {
|
||||
if (isPopulating.value) return
|
||||
|
||||
// 初始化 prevTaxRefundValues
|
||||
if (prevTaxRefundValues.value.length === 0) {
|
||||
prevTaxRefundValues.value = newValues.map((v: any) => String(v || ''))
|
||||
}
|
||||
|
||||
items.value.forEach((it: any, index: number) => {
|
||||
if (!items.value[index]) return
|
||||
|
||||
const currentValue = String(it.tax_refund || '')
|
||||
const prevValue = prevTaxRefundValues.value[index] || ''
|
||||
const autoCalculatedValue = String(calcTaxRefund(it) || '0')
|
||||
|
||||
// 如果当前值与自动计算值不同,且与上一次值不同,说明是用户手动输入的
|
||||
if (
|
||||
currentValue !== autoCalculatedValue &&
|
||||
currentValue !== prevValue &&
|
||||
currentValue !== ''
|
||||
) {
|
||||
// 标记该行已被用户手动编辑
|
||||
manualTaxRefundRows.value.add(index)
|
||||
}
|
||||
|
||||
// 更新上一次的值
|
||||
prevTaxRefundValues.value[index] = currentValue
|
||||
|
||||
// 重新计算利润
|
||||
const profit = calcProfit(it)
|
||||
items.value[index].profit = isNaN(profit) ? 0 : Number(profit.toFixed(2))
|
||||
@@ -1540,6 +1500,10 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
// 上传相关
|
||||
actionImageUplode,
|
||||
uploadHeaders,
|
||||
itemUploadAction,
|
||||
beforeItemAttachmentUpload,
|
||||
handleItemAttachmentUpload,
|
||||
removeItemAttachment,
|
||||
|
||||
// 方法
|
||||
initializeForm,
|
||||
@@ -1550,7 +1514,6 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
|
||||
wrapMoney,
|
||||
calcProfit,
|
||||
calcClearanceFee,
|
||||
calcTaxRefund,
|
||||
handleUserEdit,
|
||||
|
||||
// 合同文件方法
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
|
||||
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
|
||||
</el-dialog>
|
||||
<el-dialog v-model="audetDialogVisible" title="审核" width="700px">
|
||||
<!-- 客户标记无效 原因与附件回显 -->
|
||||
@@ -327,9 +328,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -145,7 +145,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
|
||||
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
|
||||
</el-dialog>
|
||||
<!-- 分配跟单对话框 -->
|
||||
<el-dialog v-model="zyDialogVisible" title="分配" width="360px">
|
||||
@@ -348,9 +349,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -238,7 +238,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
|
||||
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
|
||||
</el-dialog>
|
||||
<!-- 转移跟单对话框 -->
|
||||
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
|
||||
@@ -528,9 +529,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -144,7 +144,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
|
||||
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
|
||||
</el-dialog>
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-image-viewer
|
||||
@@ -237,9 +238,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -341,7 +341,14 @@
|
||||
v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'"
|
||||
v-html="detailRow.desc_collect"
|
||||
class="detils-content"
|
||||
@click="onDescImgClick"
|
||||
></div>
|
||||
<el-image-viewer
|
||||
v-if="descViewerUrl"
|
||||
:url-list="[descViewerUrl]"
|
||||
hide-on-click-modal
|
||||
@close="descViewerUrl = ''"
|
||||
/>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="reportDialogVisible" title="标记无效" width="600px">
|
||||
<el-form :model="reportForm" label-width="80px">
|
||||
@@ -701,9 +708,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
|
||||
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -204,9 +205,18 @@
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
img.style.cursor = 'zoom-in'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
|
||||
const descViewerUrl = ref('')
|
||||
function onDescImgClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
descViewerUrl.value = (target as HTMLImageElement).src
|
||||
}
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
|
||||
@@ -457,7 +457,7 @@
|
||||
<ElFormItem label="预估退税额" label-width="85px">
|
||||
<ElInput
|
||||
v-model="item.tax_refund"
|
||||
placeholder="正在计算预估退税额"
|
||||
placeholder="请输入预估退税额"
|
||||
@input="handleUserEdit(idx)"
|
||||
>
|
||||
<template #suffix>
|
||||
@@ -555,6 +555,37 @@
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :sm="24" :md="24" :lg="24" :xl="24">
|
||||
<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>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24" style="text-align: right">
|
||||
<el-tag effect="light" type="success" @click="copyRow(idx)" class="row-actions">
|
||||
@@ -1207,7 +1238,14 @@
|
||||
getVehicleList,
|
||||
handleVehicleSizeChange,
|
||||
handleVehicleCurrentChange,
|
||||
chooseVehicle
|
||||
chooseVehicle,
|
||||
|
||||
// 车辆附件上传
|
||||
uploadHeaders,
|
||||
itemUploadAction,
|
||||
beforeItemAttachmentUpload,
|
||||
handleItemAttachmentUpload,
|
||||
removeItemAttachment
|
||||
} = useStockingForm(false) // isFinal = false
|
||||
|
||||
// ---------- 草稿缓存:保存/恢复/清理 ----------
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
<ElFormItem label="预估退税额" label-width="85px">
|
||||
<ElInput
|
||||
v-model="item.tax_refund"
|
||||
placeholder="正在计算预估退税额"
|
||||
placeholder="请输入预估退税额"
|
||||
@input="handleUserEdit(idx)"
|
||||
>
|
||||
<template #suffix>
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
|
||||
</div> -->
|
||||
<div class="toolbar-right">
|
||||
<el-radio-group
|
||||
v-if="canViewAll"
|
||||
v-model="viewScope"
|
||||
@change="onScopeChange"
|
||||
style="margin-right: 12px"
|
||||
>
|
||||
<el-radio-button value="self">我的</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
@@ -214,6 +223,7 @@
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import FilePreview from '@/components/custom/FilePreview.vue'
|
||||
import InternalStaffViews from '@/components/custom/InternalStaffViews.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const nqCurrency = ref('')
|
||||
const nqData = ref<any[]>([])
|
||||
const timelineDialogVisible = ref(false)
|
||||
@@ -227,6 +237,14 @@
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
// 仅 userId=73 的特权用户可切换「全部/我的」
|
||||
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
|
||||
const viewScope = ref<'all' | 'self'>('self')
|
||||
const onScopeChange = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function statusClass(status: any) {
|
||||
if (status === 'rejected') return 'status-lost'
|
||||
@@ -278,8 +296,9 @@
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 1, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
is_final: 1, // 终版
|
||||
keyword: keyword.value,
|
||||
view_scope: viewScope.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await CrmService.getStockList(params)
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
<ElFormItem label="预估退税额" label-width="85px">
|
||||
<ElInput
|
||||
v-model="item.tax_refund"
|
||||
placeholder="正在计算预估退税额"
|
||||
placeholder="请输入预估退税额"
|
||||
@input="handleUserEdit(idx)"
|
||||
>
|
||||
<template #suffix>
|
||||
@@ -577,6 +577,37 @@
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :sm="24" :md="24" :lg="24" :xl="24">
|
||||
<ElFormItem label="附件" label-width="95px">
|
||||
<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>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24" style="text-align: right">
|
||||
<el-tag effect="light" class="row-actions" type="success" @click="copyRow(idx)"> 复制 </el-tag>
|
||||
@@ -1208,7 +1239,13 @@
|
||||
getVehicleList,
|
||||
handleVehicleSizeChange,
|
||||
handleVehicleCurrentChange,
|
||||
chooseVehicle
|
||||
chooseVehicle,
|
||||
|
||||
// 车辆附件上传(uploadHeaders 页面已解构,复用)
|
||||
itemUploadAction,
|
||||
beforeItemAttachmentUpload,
|
||||
handleItemAttachmentUpload,
|
||||
removeItemAttachment
|
||||
} = useStockingForm(false, stocking_id as string) // isFinal = true, stocking_id
|
||||
|
||||
// 获取详情数据
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-radio-group
|
||||
v-if="canViewAll"
|
||||
v-model="viewScope"
|
||||
@change="onScopeChange"
|
||||
style="margin-right: 12px"
|
||||
>
|
||||
<el-radio-button value="self">我的</el-radio-button>
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
@@ -272,6 +281,7 @@
|
||||
import FilePreview from '@/components/custom/FilePreview.vue'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const nqCurrency = ref('')
|
||||
const nqData = ref<any[]>([])
|
||||
const timelineDialogVisible = ref(false)
|
||||
@@ -279,6 +289,14 @@
|
||||
const isFinites = ref(false)
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
// 仅 userId=73 的特权用户可切换「全部/我的」
|
||||
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
|
||||
const viewScope = ref<'all' | 'self'>('self')
|
||||
const onScopeChange = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
defineOptions({ name: 'CrmStockingList' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
@@ -340,7 +358,8 @@
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 0, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
keyword: keyword.value,
|
||||
view_scope: viewScope.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await CrmService.getStockList(params)
|
||||
|
||||
Reference in New Issue
Block a user