Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c49da6cc5 | |||
| 2b925a1d37 | |||
| d695281424 | |||
| 2c6a2dafec | |||
| 6ab4fc9b9a | |||
| 77ac384d29 | |||
| 0dfc9a8bcc | |||
| 6e23f83e78 | |||
| b3f6d591b5 | |||
| bd1524f4f1 | |||
| f809bf05e3 | |||
| 6f80f5126c | |||
| b5d414d9af | |||
| a275a98671 | |||
| 1d9ef001c1 | |||
| aa23d0949b | |||
| 76baee2fe5 | |||
| 786082305d | |||
| ce0e9cd377 | |||
| 75d44a8eae | |||
| cdece9fd79 | |||
| 7c5d0127cc | |||
| a11d090273 | |||
| 9b2776e533 | |||
| 437f5bb567 | |||
| 15e86e0f4f | |||
| 65b2cb1037 | |||
| 2648c49a3b | |||
| 3b396dc762 | |||
| 0ed507efd5 | |||
| 4281cb3740 | |||
| 7638eaf80f | |||
| 963a95732b | |||
| 6e2cd44f55 | |||
| 0e8cec4af0 | |||
| 6ae4fd872d | |||
| 58a6732f1b | |||
| b527715b3a | |||
| 0f7aae3c72 | |||
| 8e2dd0600f | |||
| 926823df32 | |||
| 37e75014d0 | |||
| a70ad7a65c | |||
| 631ca4ec30 | |||
| cd1bbca4a6 | |||
| 2ba64b94b0 | |||
| 3aa1b0f79b | |||
| ff0fb83bd0 | |||
| 222d07d483 | |||
| 16abb6364c | |||
| 153bb37d10 | |||
| 0b89bade20 | |||
| dee800f90f | |||
| 9925e2e2e4 | |||
| 4f18a1b8bf | |||
| a514bf4009 |
@@ -309,6 +309,7 @@
|
||||
"watchWithFilter": true,
|
||||
"whenever": true,
|
||||
"ElMessage": true,
|
||||
"ElTag": true
|
||||
"ElTag": true,
|
||||
"ElMessageBox": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ VITE_BASE_URL = /
|
||||
# VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
|
||||
# VITE_API_URL = http://219.157.255.213:25703
|
||||
# VITE_API_URL = https://mzl.dev.hnxinyouhe.com
|
||||
# VITE_API_URL = https://fdesk.hnxinyouhe.com
|
||||
VITE_API_URL = https://x-server.hnxinyouhe.com
|
||||
VITE_API_URL = https://fdesk.hnxinyouhe.com
|
||||
# VITE_API_URL = https://x-server.hnxinyouhe.com
|
||||
|
||||
# VITE_API_URL_WSS = wss://mzl.dev.hnxinyouhe.com
|
||||
# VITE_API_URL_WSS = wss://fdesk.hnxinyouhe.com
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import appLogo from '@/assets/img/common/logox.png'
|
||||
const router = useRouter()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language, accessToken } = storeToRefs(userStore)
|
||||
@@ -151,6 +152,17 @@
|
||||
|
||||
// 监听ws消息,用于系统级通知
|
||||
mittBus.on('ws:message', handleWsMessageNotification)
|
||||
|
||||
// 初始化菜单徽标系统(全局初始化,避免重复)
|
||||
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
|
||||
const { initBadgeSystem } = useMenuBadge()
|
||||
initBadgeSystem()
|
||||
})
|
||||
|
||||
// 加载菜单徽标测试工具(仅开发环境)
|
||||
if (import.meta.env.DEV) {
|
||||
import('@/utils/menuBadgeTest')
|
||||
}
|
||||
})
|
||||
|
||||
// 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新)
|
||||
@@ -165,6 +177,12 @@
|
||||
) {
|
||||
initWebSocket(token)
|
||||
}
|
||||
|
||||
// 重新初始化徽标系统(用户重新登录时)
|
||||
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
|
||||
const { initBadgeSystem } = useMenuBadge()
|
||||
initBadgeSystem()
|
||||
})
|
||||
} else {
|
||||
// 退出登录时,关闭并重置
|
||||
const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient')
|
||||
@@ -218,7 +236,7 @@
|
||||
.el-dialog__footer {
|
||||
background: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: inherit;
|
||||
}
|
||||
.el-dialog__header {
|
||||
display: flex;
|
||||
@@ -241,4 +259,7 @@
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.item-row-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class invitemService {
|
||||
// 物品列表
|
||||
static getinvitemlist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/inv/item/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 添加物品
|
||||
static invitemadd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/item/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 修改物品
|
||||
static invitemupdate(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/item/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 删除物品
|
||||
static invitemdelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/item/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 借用审批相关接口
|
||||
static getBorrowList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/inv/requisition/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 借用申请详情
|
||||
static getBorrowDetail(params: { id: number }) {
|
||||
return request.get<any>({
|
||||
url: '/admin/inv/requisition/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 借用审批
|
||||
static borrowApproval(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/requisition/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 归还审批列表
|
||||
static getReturnList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/mobile/inv/requisition/return/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 归还申请详情
|
||||
static getReturnDetail(params: { id: number }) {
|
||||
return request.get<any>({
|
||||
url: '/admin/inv/requisition/return/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 确认归还
|
||||
static returnconfirm(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/requisition/return/confirm',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 生成二维码
|
||||
static getQrCode(params: { id: number; qr_claim_limit?: number }) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/item/qr-code',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 删除二维码
|
||||
static deleteQrCode(params: { id: number }) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inv/item/qr-code/delete',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 管理员带领
|
||||
static proxyClaim(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/requisition/proxy-claim',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 获取库存变化记录
|
||||
static getStockLogList(params: {
|
||||
item_id: number
|
||||
current?: number
|
||||
size?: number
|
||||
change_type?: number
|
||||
}) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/stock-log/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 获取扫码记录
|
||||
static getQrClaimsList(params: {
|
||||
item_id: number
|
||||
current?: number
|
||||
size?: number
|
||||
claim_typeid?: number
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
}) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/requisition/qr-claims',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class dashboardService {
|
||||
//卡片统计
|
||||
static officeclerkcard() {
|
||||
return request.get<any>({
|
||||
url: '/admin/dashboard/officeclerk/card'
|
||||
})
|
||||
}
|
||||
//待审批
|
||||
static officeclerkpending() {
|
||||
return request.get<any>({
|
||||
url: '/admin/dashboard/officeclerk/approval'
|
||||
})
|
||||
}
|
||||
//业务员备货单统计
|
||||
static salestockingcard() {
|
||||
return request.get<any>({
|
||||
url: '/admin/dashboard/officeclerk/curve'
|
||||
})
|
||||
}
|
||||
//待认领
|
||||
static officeclerkunclaimed(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dashboard/officeclerk/claim',
|
||||
params
|
||||
})
|
||||
}
|
||||
//财务仪表盘统计
|
||||
static officeclerkchart() {
|
||||
return request.get<any>({
|
||||
url: '/admin/fin/statistics'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class financeService {
|
||||
// 收款记录-列表
|
||||
static getReceiptList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/receipt/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-详情
|
||||
static getReceiptDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/receipt/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-添加
|
||||
static addReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-更新
|
||||
static updateReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-删除
|
||||
static deleteReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-列表
|
||||
static getPaymentList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/payment/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-详情
|
||||
static getPaymentDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/payment/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-添加
|
||||
static addPayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-更新
|
||||
static updatePayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-删除
|
||||
static deletePayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/delete',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 绑定付款记录到收款记录
|
||||
static bindPaymentRecord(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/payment',
|
||||
params
|
||||
})
|
||||
}
|
||||
//收款记录历史
|
||||
static getReceiptHistory(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/receipt/his/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//确认收款
|
||||
static confirmReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/credit',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class kpiService {
|
||||
// 月度-业绩 列表
|
||||
static getmonthlylist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/targets/monthly/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 添加
|
||||
static addmonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 删除
|
||||
static deletemonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 修改
|
||||
static updatemonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 详情
|
||||
static detailmonthly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/monthly/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-业绩 列表
|
||||
static getyearlylist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/targets/annual/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 添加
|
||||
static addyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 删除
|
||||
static deleteyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 详情
|
||||
static detailyearly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/annual/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 修改
|
||||
static updateyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-公司目标完成率
|
||||
static statisticstotal(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/total',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人销售数量排名
|
||||
static statisticsranking(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/ranking',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-业务KPI分月完成数量表
|
||||
static statisticsmonthly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/month',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人年度目标完成度(百分比)
|
||||
static statisticspercentage(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/progressh',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人累积完成数量(辆)
|
||||
static statisticscumulative(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/achieved',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class logService {
|
||||
// 更新日志-列表
|
||||
static getChangelogList(params:any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/update/log/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 更新日志-添加
|
||||
static addChangelog(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/update/log/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 更新日志-删除
|
||||
static deleteChangelog(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/update/log/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 更新日志-详情
|
||||
static detailChangelog(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/update/log/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 更新日志-修改
|
||||
static updateChangelog(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/update/log/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//操作日志-列表
|
||||
static getOperationLogList(params: any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/op/log/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class purchaseService {
|
||||
// 采购单预置
|
||||
static purchaseorderpredata(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/predata',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单列表
|
||||
static purchaseorderlist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/purchase/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单添加
|
||||
static purchaseorderadd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单更新
|
||||
static purchaseorderupdate(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单删除
|
||||
static purchaseorderdelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单详情
|
||||
static purchaseorderdetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/purchase/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class userConService {
|
||||
//分组列表
|
||||
static getGroupList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/user/group/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加分组
|
||||
static addGroup(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/group/store',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
//编辑分组
|
||||
static editGroup(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/group/update',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
//删除分组
|
||||
static deleteGroup(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/group/destroy',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,37 +4,43 @@ export class manageService {
|
||||
//仪表盘 总
|
||||
static statisticswhole() {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/whole'
|
||||
// url: '/admin/inquiry/statistics/whole'
|
||||
url: 'admin/crm/statistics/whole'
|
||||
})
|
||||
}
|
||||
//各渠道统计
|
||||
static channelcount() {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/channel/count'
|
||||
// url: '/admin/inquiry/statistics/channel/count'
|
||||
url: '/admin/crm/statistics/channel/count'
|
||||
})
|
||||
}
|
||||
//各国家统计
|
||||
static countrycount() {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/country/count'
|
||||
// url: '/admin/inquiry/statistics/country/count'
|
||||
url: '/admin/crm/statistics/country/count'
|
||||
})
|
||||
}
|
||||
//跟单人统计
|
||||
static trackcount() {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/track/count'
|
||||
// url: '/admin/inquiry/statistics/track/count'
|
||||
url: '/admin/crm/statistics/track/count'
|
||||
})
|
||||
}
|
||||
//每日录单统计
|
||||
static daycount() {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/day/count'
|
||||
// url: '/admin/inquiry/statistics/day/count'
|
||||
url: '/admin/crm/statistics/day/count'
|
||||
})
|
||||
}
|
||||
//每日录单统计
|
||||
static inquirycount(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/statistics/inquiry/count',
|
||||
// url: '/admin/inquiry/statistics/inquiry/count',
|
||||
url: '/admin/crm/statistics/customer/count',
|
||||
params
|
||||
})
|
||||
}
|
||||
@@ -205,4 +211,126 @@ export class manageService {
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单审核列表
|
||||
static getstocklist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/approval/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单审核
|
||||
static stockapproval(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
// 审核更新
|
||||
static stockapprovalupdate(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//审核详情
|
||||
static stockapprovaldetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审核删除
|
||||
static stockapprovaldelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovalrecord(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/todo',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批通过/拒绝
|
||||
static stockapprovalpass(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/records/action',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovallogs(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/logs',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单
|
||||
static getStockList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/stock/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加备货单
|
||||
static addStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/stock/order/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//详情备货单
|
||||
static getStockDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单查看
|
||||
static checkStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/detail',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单删除
|
||||
static delectStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//打印
|
||||
static printStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/print',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审核详情
|
||||
static stockapprovaldetailborder(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/border',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批转交
|
||||
static stockapprovaltransfer(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/records/transfer',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批时间轴
|
||||
static stockapprovaltimeline(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/timeline',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,149 @@ export class commonService {
|
||||
url: '/common/notice/unread'
|
||||
})
|
||||
}
|
||||
//货币下拉列表
|
||||
static getCurrencySelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/currency'
|
||||
})
|
||||
}
|
||||
|
||||
//国家下拉列表
|
||||
static getCountrySelect() {
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/getCountry'
|
||||
})
|
||||
}
|
||||
//品牌下拉列表
|
||||
static getBrandSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/brand'
|
||||
})
|
||||
}
|
||||
//车系下拉列表
|
||||
static getSeriesSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/brand/series',
|
||||
params
|
||||
})
|
||||
}
|
||||
//车型下拉列表
|
||||
static getModelsSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/vehicle/sku/preset',
|
||||
params
|
||||
})
|
||||
}
|
||||
//港口下拉列表
|
||||
static getPortSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/port',
|
||||
params
|
||||
})
|
||||
}
|
||||
//颜色下拉列表
|
||||
static getColorSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/color',
|
||||
params
|
||||
})
|
||||
}
|
||||
//改装下拉列表
|
||||
static getModificationSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/modification'
|
||||
})
|
||||
}
|
||||
//字典下拉框
|
||||
static getDictSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/scope',
|
||||
params
|
||||
})
|
||||
}
|
||||
//字典项下拉框
|
||||
static getDictItemSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/dictItem',
|
||||
params
|
||||
})
|
||||
}
|
||||
//字典分类下拉框
|
||||
static getDictTypeSelect() {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/group'
|
||||
})
|
||||
}
|
||||
//账户下拉框
|
||||
static getAccountSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/account'
|
||||
})
|
||||
}
|
||||
//名下客户下拉框
|
||||
static getCustomerSelect() {
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/preset'
|
||||
})
|
||||
}
|
||||
//角色下拉框
|
||||
static getRoleSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/roles'
|
||||
})
|
||||
}
|
||||
//用户下拉框
|
||||
static getUserSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/users'
|
||||
})
|
||||
}
|
||||
//用户下拉框
|
||||
static getUserSelectparams(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/users',
|
||||
params
|
||||
})
|
||||
}
|
||||
//签名 编辑
|
||||
static signatureEdit(params: any) {
|
||||
return request.post<any>({
|
||||
url: '/admin/user/update/sign',
|
||||
params
|
||||
})
|
||||
}
|
||||
//人员分组场景
|
||||
static getUserGroupScene() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/usergroup/scene'
|
||||
})
|
||||
}
|
||||
//人员分组
|
||||
static getUserGroupSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/usergroup',
|
||||
params
|
||||
})
|
||||
}
|
||||
//关键字抓取车辆
|
||||
static getcrawlKeyword(params: any) {
|
||||
return request.get<any>({
|
||||
// url: '/common/brand/series/crawlKeyword',
|
||||
url: '/common/brand/series/crawl',
|
||||
params
|
||||
})
|
||||
}
|
||||
//关键字抓取车辆保存
|
||||
static crawlModelCreate(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/common/brand/series/crawlKeywordCreate',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘选择下拉
|
||||
static getDashboardSelect() {
|
||||
return request.get<any>({
|
||||
url: '/admin/resources/list/menu?is_home=1'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class CrmService {
|
||||
}
|
||||
//国家下拉
|
||||
static getCountry() {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/getCountry'
|
||||
})
|
||||
}
|
||||
@@ -207,4 +207,117 @@ export class CrmService {
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//备货单
|
||||
static getStockList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/stock/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加备货单
|
||||
static addStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//详情备货单
|
||||
static getStockDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//编辑备货单
|
||||
static editStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单查看
|
||||
static checkStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/detail',
|
||||
params
|
||||
})
|
||||
}
|
||||
//提交备货单
|
||||
static submitStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
//打印
|
||||
static printStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/print',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovalrecord(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/approval/records/todo',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批通过/拒绝
|
||||
static stockapprovalpass(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/approval/records/action',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovallogs(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/approval/records/logs',
|
||||
params
|
||||
})
|
||||
}
|
||||
//终极备货单 提交
|
||||
static stockultimateapproval(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/stock/order/ultimate/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单撤回
|
||||
static stockrecall(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/stock/order/revoke/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单作废
|
||||
static stockinvalid(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/stock/order/nullify/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单删除
|
||||
static stockdelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/stock/order/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审核详情
|
||||
static stockapprovaldetailborder(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/approval/records/border',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批转交
|
||||
static stockapprovaltransfer(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/approval/records/transfer',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class vehicleService {
|
||||
// 颜色管理
|
||||
static getColorList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/color/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/color/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车辆列表
|
||||
static getVehicleList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/vehicle/sku/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/vehicle/sku/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 改装管理
|
||||
static getModificationList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/modification/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/modification/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 港口管理
|
||||
static getPortList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/port/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/port/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/port/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/port/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/port/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆品牌列表
|
||||
static getBrandList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/brand/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆品牌新增/详情/编辑/删除
|
||||
static brandAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车辆系列列表
|
||||
static getSeriesList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/brand/series/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆系列新增/详情/编辑/删除
|
||||
static seriesAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/series/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/series/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/series/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/series/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车系采集相关接口
|
||||
static collectVehicleSeries(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/crawlSeries',
|
||||
params
|
||||
})
|
||||
}
|
||||
static saveVehicleSeries(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/crawlSeriesCreate',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车型采集相关接口
|
||||
static collectVehicleModels(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/series/crawlModels',
|
||||
params
|
||||
})
|
||||
}
|
||||
static saveVehicleModels(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/series/crawlModelCreate',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class financeService {
|
||||
// 货币列表
|
||||
static getCurrencyList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/currency/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币添加
|
||||
static currencyAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币详情
|
||||
static currencyDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/currency/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币编辑
|
||||
static currencyEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币删除
|
||||
static currencyDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户列表
|
||||
static getAccountList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/account/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户添加
|
||||
static accountAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户详情
|
||||
static accountDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/account/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户编辑
|
||||
static accountEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户删除
|
||||
static accountDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -245,4 +245,104 @@ export class systemService {
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典类型列表
|
||||
static getDictTypeList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/dict/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典类型新增/详情/编辑/删除
|
||||
static dictTypeAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 字典项列表
|
||||
static getDictItemList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/dict/item/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典项新增/详情/编辑/删除
|
||||
static dictItemAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/item/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘设置列表
|
||||
static getDashboardList(params: any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/dashboard/rule/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘设置新增
|
||||
static dashboardAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dashboard/rule/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘设置详情
|
||||
static dashboardDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dashboard/rule/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘设置编辑
|
||||
static dashboardEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dashboard/rule/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//仪表盘设置删除
|
||||
static dashboardDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dashboard/rule/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class vehicleService {
|
||||
// 车辆品牌列表
|
||||
static getBrandList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/brand/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆品牌新增/详情/编辑/删除
|
||||
static brandAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/brand/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车辆系列列表
|
||||
static getSeriesList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/brand/series/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆系列新增/详情/编辑/删除
|
||||
static seriesAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/brand/series/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 颜色管理
|
||||
static getColorList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/color/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/color/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 改装管理
|
||||
static getModificationList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/modification/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/modification/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 港口管理
|
||||
static getPortList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/port/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/port/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆列表
|
||||
static getVehicleList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/vehicle/sku/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/vehicle/sku/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车系采集相关接口
|
||||
static collectVehicleSeries(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/crawlSeries',
|
||||
params
|
||||
})
|
||||
}
|
||||
static saveVehicleSeries(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/crawlSeriesCreate',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车型采集相关接口
|
||||
static collectVehicleModels(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/brand/series/crawlModels',
|
||||
params
|
||||
})
|
||||
}
|
||||
static saveVehicleModels(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/brand/series/crawlModelCreate',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 582 B |
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -6,7 +6,7 @@
|
||||
//
|
||||
'white': #ffffff,
|
||||
'black': #000000,
|
||||
'success': ('base': #13deb9),
|
||||
'success': ('base': #00A43F),
|
||||
'warning': ('base': #ffae1f),
|
||||
'danger': ('base': #ff4d4f),
|
||||
'error': ('base': #fa896b)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//
|
||||
'white': #ffffff,
|
||||
'black': #000000,
|
||||
'success': ('base': #13deb9),
|
||||
'success': ('base': #00A43F),
|
||||
'warning': ('base': #ffae1f),
|
||||
'danger': ('base': #ff4d4f),
|
||||
'error': ('base': #fa896b)
|
||||
|
||||
@@ -400,3 +400,12 @@
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 修复 el-table 空数据块样式
|
||||
.el-table__empty-block {
|
||||
height: 100% !important;
|
||||
min-height: 200px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="multi-file-upload">
|
||||
<!-- 上传按钮 -->
|
||||
<el-upload
|
||||
class="upload-box"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUpload"
|
||||
:before-upload="beforeUpload"
|
||||
multiple
|
||||
:accept="accept"
|
||||
>
|
||||
<div class="upload-trigger">
|
||||
<img class="upload-icon" :src="iconSrc" />
|
||||
<div class="upload-text">
|
||||
<span>{{ uploadText }}</span>
|
||||
<span v-if="showHint" class="upload-hint">{{ hintText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
|
||||
<!-- 文件列表显示 -->
|
||||
<div v-if="fileList.length > 0" class="files-list">
|
||||
<div v-for="(file, index) in fileList" :key="index" class="file-item">
|
||||
<div class="file-info">
|
||||
<img v-if="isImageFile(file)" :src="file.url" class="file-preview-image" />
|
||||
<div v-else class="file-icon">
|
||||
<i class="el-icon-document"></i>
|
||||
</div>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<el-button type="text" size="small" @click="filePreview(file)" icon="el-icon-download">
|
||||
预览
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="downloadFile(file)" icon="el-icon-download">
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
@click="removeFile(index)"
|
||||
icon="el-icon-delete"
|
||||
style="color: #f56c6c"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<files-previews v-model="fileshow" :file="filepviews" @close="onClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="MultiFileUpload">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
import FilesPreviews from '@/components/custom/files_previews.vue'
|
||||
const actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=stock_orders`
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
const fileshow = ref(false)
|
||||
const filepviews = ref({} as FileItem)
|
||||
function onClose() {
|
||||
console.log('文件预览已关闭')
|
||||
}
|
||||
interface FileItem {
|
||||
name: string
|
||||
url: string
|
||||
file_name: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: FileItem[]
|
||||
uploadText?: string
|
||||
hintText?: string
|
||||
showHint?: boolean
|
||||
iconSrc?: string
|
||||
accept?: string
|
||||
maxSize?: number // MB
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: FileItem[]): void
|
||||
(e: 'change', value: FileItem[]): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
uploadText: '点击上传文件',
|
||||
hintText: '支持多个文件上传',
|
||||
showHint: true,
|
||||
iconSrc: '/src/assets/img/crm/contract.png',
|
||||
accept: '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt',
|
||||
maxSize: 10
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const fileList = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
})
|
||||
const filePreview = (file: FileItem) => {
|
||||
filepviews.value = file
|
||||
fileshow.value = true
|
||||
}
|
||||
const isImageFile = (file: FileItem) => {
|
||||
const stripQuery = (s?: string) => (s ? s.split('?')[0] : '')
|
||||
const byExt = (s?: string) => /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(stripQuery(s || ''))
|
||||
// Prefer extension checks on url, then file_name, then name
|
||||
if (byExt(file.url)) return true
|
||||
if (byExt(file.file_name)) return true
|
||||
if (byExt(file.name)) return true
|
||||
// Fallback: some backends may return mime in file_name like 'image/jpeg'
|
||||
return (file.file_name || '').toLowerCase().startsWith('image/')
|
||||
}
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
const isValidType = new RegExp(props.accept.replace(/\./g, '\\.').replace(/,/g, '|'), 'i').test(
|
||||
file.name
|
||||
)
|
||||
if (!isValidType) {
|
||||
ElMessage.error(`只能上传${props.accept}格式的文件!`)
|
||||
return false
|
||||
}
|
||||
const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize
|
||||
if (!isLtMaxSize) {
|
||||
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB!`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUpload = (response: any) => {
|
||||
if (response.code === 200) {
|
||||
const fileInfo: FileItem = {
|
||||
name: response.data.original_name || '文件',
|
||||
url: response.data.file_url,
|
||||
file_name: response.data.file_name
|
||||
}
|
||||
const newFileList = [...fileList.value, fileInfo]
|
||||
fileList.value = newFileList
|
||||
ElMessage.success('文件上传成功!')
|
||||
} else {
|
||||
ElMessage.error(response.message || '文件上传失败!')
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const newFileList = [...fileList.value]
|
||||
newFileList.splice(index, 1)
|
||||
fileList.value = newFileList
|
||||
ElMessage.success('文件已删除')
|
||||
}
|
||||
|
||||
const downloadFile = (file: FileItem) => {
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
// 导出组件
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.multi-file-upload {
|
||||
.upload-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
// border: 2px dashed #d9d9d9;
|
||||
// border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
// background: #fafafa;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// 文件列表样式
|
||||
.files-list {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
background: #fafafa;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f9ff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-preview-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -334,13 +334,8 @@
|
||||
const goPage = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
const { systemsid } = useUserStore()
|
||||
const gouserInfo = () => {
|
||||
if (systemsid == '1') {
|
||||
router.push('/admin/user/user_info')
|
||||
} else {
|
||||
router.push('/system/user-center')
|
||||
}
|
||||
router.push('/admin/user/user_info')
|
||||
}
|
||||
|
||||
// const toDocs = () => {
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
v-html="item.meta.icon"
|
||||
></i>
|
||||
<span>{{ formatMenuTitle(item.meta.title) }}</span>
|
||||
<div class="badge" v-if="item.meta.showBadge"></div>
|
||||
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge"></div>
|
||||
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
|
||||
{{ getMenuTextBadge(item.path) }}
|
||||
</div>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
@@ -41,6 +44,7 @@
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { handleMenuJump } from '@/utils/navigation'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import { useMenuBadge } from '@/composables/useMenuBadge'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -60,6 +64,9 @@
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 使用菜单徽标系统
|
||||
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
|
||||
|
||||
// 计算当前项是否有子菜单
|
||||
const hasChildren = computed(() => {
|
||||
return props.item.children && props.item.children.length > 0
|
||||
|
||||
@@ -112,16 +112,17 @@
|
||||
top: 0;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
// padding: 0 5px;
|
||||
margin: auto;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: #fd4e4e;
|
||||
border-radius: 5px;
|
||||
// border-radius: 5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<template #title>
|
||||
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
|
||||
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
|
||||
<div v-if="item.meta.showBadge" class="badge" style="right: 35px" />
|
||||
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" style="right: 35px" />
|
||||
<div v-if="getMenuTextBadge(item.path)" class="text-badge" style="right: 35px">
|
||||
{{ getMenuTextBadge(item.path) }}
|
||||
</div>
|
||||
</template>
|
||||
<SidebarSubmenu
|
||||
:list="item.children"
|
||||
@@ -26,9 +29,9 @@
|
||||
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
|
||||
<template #title>
|
||||
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
|
||||
<div v-if="item.meta.showBadge" class="badge" />
|
||||
<div v-if="item.meta.showTextBadge" class="text-badge">
|
||||
{{ item.meta.showTextBadge }}
|
||||
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" />
|
||||
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
|
||||
{{ getMenuTextBadge(item.path) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
@@ -40,6 +43,7 @@
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import { handleMenuJump } from '@/utils/navigation'
|
||||
import { useMenuBadge } from '@/composables/useMenuBadge'
|
||||
|
||||
// 类型定义
|
||||
interface Props {
|
||||
@@ -66,6 +70,9 @@
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// 使用菜单徽标系统
|
||||
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
|
||||
|
||||
// 计算属性
|
||||
const filteredMenuItems = computed(() => filterRoutes(props.list))
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<RouterView
|
||||
v-if="isRefresh"
|
||||
:key="$route.fullPath"
|
||||
:key="$route.path"
|
||||
v-slot="{ Component, route }"
|
||||
:style="{ minHeight: containerMinHeight }"
|
||||
>
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
<!-- 路由动画 -->
|
||||
<Transition :name="pageTransition" mode="out-in" appear>
|
||||
<!-- <KeepAlive :max="10" :include="['StockingAdd', 'StockingFinalAdd']"> -->
|
||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
||||
<component :is="Component" :key="route.path" v-if="route.meta.keepAlive" />
|
||||
<component :is="Component" v-if="route.meta.keepAlive" />
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
@@ -37,6 +38,7 @@
|
||||
const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore())
|
||||
const { keepAliveExclude } = storeToRefs(useWorktabStore())
|
||||
|
||||
|
||||
const { containerMinHeight } = useCommon()
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
:row-key="rowKey"
|
||||
:row-class-name="rowClassName"
|
||||
:height="height"
|
||||
:max-height="maxHeight"
|
||||
:show-header="showHeader"
|
||||
@@ -123,6 +124,8 @@
|
||||
marginTop?: number
|
||||
/** 表格大小 */
|
||||
size?: 'small' | 'default' | 'large'
|
||||
/** 行样式类名函数,透传给 el-table 的 row-class-name */
|
||||
rowClassName?: (args: { row: any; rowIndex: number }) => string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TableProps>(), {
|
||||
@@ -146,7 +149,8 @@
|
||||
paginationSize: 'default',
|
||||
paginationLayout: '',
|
||||
showHeaderBackground: null,
|
||||
marginTop: 20
|
||||
marginTop: 20,
|
||||
rowClassName: undefined
|
||||
})
|
||||
|
||||
/*
|
||||
@@ -184,12 +188,11 @@
|
||||
const elTable = tableRef.value
|
||||
if (!elTable) return
|
||||
|
||||
// 递归处理树形数据
|
||||
// 支持普通与树形数据行展开
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
const hasChildren = row.children?.length > 0
|
||||
if (hasChildren) {
|
||||
elTable.toggleRowExpansion(row, true)
|
||||
elTable.toggleRowExpansion(row, true)
|
||||
if (row.children?.length > 0) {
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
@@ -203,12 +206,11 @@
|
||||
const elTable = tableRef.value
|
||||
if (!elTable) return
|
||||
|
||||
// 递归处理树形数据
|
||||
// 支持普通与树形数据行收起
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
const hasChildren = row.children?.length > 0
|
||||
if (hasChildren) {
|
||||
elTable.toggleRowExpansion(row, false)
|
||||
elTable.toggleRowExpansion(row, false)
|
||||
if (row.children?.length > 0) {
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="approval-timeline">
|
||||
<div v-for="(rec, idx) in records" :key="idx" class="timeline-item">
|
||||
<div class="left">
|
||||
<div class="avatar-box">
|
||||
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
|
||||
<i v-if="rec.approved" class="status-dot iconfont-sys"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row name-role">
|
||||
<span class="name">{{ rec.name }}</span>
|
||||
<span class="role">({{ rec.role }})</span>
|
||||
</div>
|
||||
<div >
|
||||
审核说明:{{ rec.comment }}
|
||||
</div>
|
||||
<div class="row time">{{ rec.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!records || !records.length" class="empty">暂无审批记录</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
interface ApprovalRecordItem {
|
||||
name?: string
|
||||
role?: string
|
||||
type?: string
|
||||
time?: string
|
||||
approved?: boolean
|
||||
rejected?: boolean
|
||||
pending?: boolean
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ApprovalTimeline',
|
||||
props: {
|
||||
records: {
|
||||
type: Array as () => ApprovalRecordItem[],
|
||||
default: () => []
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 审批记录 时间线 */
|
||||
.approval-timeline {
|
||||
padding: 8px 4px 8px 0;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline-item + .timeline-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.left {
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: #3d7bff;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
font-size: 9px;
|
||||
padding: 3px 1px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.status-dot.pending {
|
||||
background: #3471ff;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
.status-dot.rejected {
|
||||
background: #ff0000;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
/* 连接下一个节点的虚线 */
|
||||
.timeline-item:not(:last-child) .left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 52px; /* avatar 46px + 间距 6px */
|
||||
left: 23px; /* 盒子宽度一半,确保竖线居中 */
|
||||
width: 0;
|
||||
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
|
||||
border-left: 2px dotted #bfbfbf;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row.name-role {
|
||||
color: #222;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.role {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.empty {
|
||||
color: #999;
|
||||
padding: 8px 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="详情"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="payment-detail-dialog" v-loading="loading">
|
||||
<div v-if="paymentData" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">基本信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>创建日期:</label>
|
||||
<span>{{ formatDate(paymentData.created_at) }}</span>
|
||||
</div>
|
||||
<!-- <div class="info-item">
|
||||
<label>付款来源:</label>
|
||||
<span>{{ paymentData.country_name || '—' }}</span>
|
||||
</div> -->
|
||||
<div class="info-item">
|
||||
<label>汇款人:</label>
|
||||
<span>{{ paymentData.remitter || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>账户:</label>
|
||||
<span>{{ paymentData.account_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>币种:</label>
|
||||
<span>{{ paymentData.currency_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>金额:</label>
|
||||
<span class="amount">{{ paymentData.amount || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>外汇到账回单:</label>
|
||||
<span>
|
||||
<el-image
|
||||
v-if="paymentData.fx_receipt_file_full"
|
||||
:src="paymentData.fx_receipt_file_full"
|
||||
:preview-src-list="[paymentData.fx_receipt_file_full]"
|
||||
style="width: 100px; height: 100px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联信息 -->
|
||||
<div class="section" v-if="paymentData.payment_apply_id">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>付款申请ID:</label>
|
||||
<span>{{ paymentData.payment_apply_id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>阿里订单号/PI号:</label>
|
||||
<span>{{ paymentData.payment_apply.stock[0].pi_no }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>业务员:</label>
|
||||
<span>{{ paymentData.payment_apply.sales_user_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>付款公司:</label>
|
||||
<span>{{ paymentData.payment_apply.account_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>付款来源:</label>
|
||||
<span>{{ paymentData.payment_apply.country_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>汇款人:</label>
|
||||
<span>{{ paymentData.payment_apply.remitter || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>账户:</label>
|
||||
<span>{{ paymentData.payment_apply.account_short_desc || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>币种:</label>
|
||||
<span>{{ paymentData.payment_apply.currency_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>汇率:</label>
|
||||
<span>{{ paymentData.payment_apply.exchange_rate || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>总金额:</label>
|
||||
<span>{{ paymentData.payment_apply.amount || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>汇款行:</label>
|
||||
<span>{{ paymentData.payment_apply.swift || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>客户付款水单:</label>
|
||||
<span>
|
||||
<el-image
|
||||
v-if="paymentData.payment_apply.voucher_file_full"
|
||||
:src="paymentData.payment_apply.voucher_file_full"
|
||||
:preview-src-list="[paymentData.payment_apply.voucher_file_full]"
|
||||
style="width: 100px; height: 100px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>入账资料:</label>
|
||||
<div v-if="paymentData.payment_apply.submit_files.length > 0" class="files-list">
|
||||
<div
|
||||
v-for="(file, index) in paymentData.payment_apply.submit_files"
|
||||
:key="index"
|
||||
class="file-item"
|
||||
>
|
||||
<div v-if="isImageFile(file.file_full)" class="image-preview">
|
||||
<img
|
||||
:src="file.file_full"
|
||||
:alt="file.file_name"
|
||||
class="preview-image"
|
||||
@click="previewImage(file)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ file.file_name }}</span>
|
||||
<div class="file-actions">
|
||||
<!-- <el-button type="text" size="small" @click="previewImage(file)">预览</el-button> -->
|
||||
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ file.file_name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收款记录关联 -->
|
||||
<div
|
||||
class="section"
|
||||
v-if="paymentData.receipt_records && paymentData.receipt_records.length"
|
||||
>
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联收款记录</span>
|
||||
</div>
|
||||
<el-table :data="paymentData.receipt_records" style="margin-top: 12px">
|
||||
<el-table-column label="收款记录ID" prop="id" min-width="120" />
|
||||
<el-table-column label="创建日期" prop="created_at" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<div class="section" v-if="paymentData.remark">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">备注信息</span>
|
||||
</div>
|
||||
<div class="remark-content">
|
||||
{{ paymentData.remark }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无数据状态 -->
|
||||
<div v-else-if="!loading" class="no-data">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
|
||||
interface PaymentRecord {
|
||||
id: string | number
|
||||
created_at: string
|
||||
country_name: string
|
||||
remitter: string
|
||||
account_name: string
|
||||
currency_name: string
|
||||
amount: number
|
||||
status: string
|
||||
payment_apply_id?: any
|
||||
receipt_records?: any[]
|
||||
payment_apply?: any
|
||||
remark?: string
|
||||
fx_receipt_file_full?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
paymentId?: string | number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const paymentData = ref<PaymentRecord | null>(null)
|
||||
|
||||
// 获取付款记录详情
|
||||
const getPaymentDetail = async () => {
|
||||
if (!props.paymentId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await financeService.getReceiptDetail({ id: props.paymentId })
|
||||
paymentData.value = res
|
||||
} catch (error) {
|
||||
console.error('获取付款记录详情失败:', error)
|
||||
paymentData.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
paymentData.value = null
|
||||
}
|
||||
|
||||
// 打印功能
|
||||
const handlePrint = () => {
|
||||
window.print()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '—'
|
||||
try {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return `${currency || ''} ${amount?.toLocaleString() || 0}`
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||
const statusMap: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
|
||||
available: 'success',
|
||||
used: 'info',
|
||||
pending: 'warning',
|
||||
cancelled: 'danger',
|
||||
bound: 'success',
|
||||
unbound: 'info'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': '可用',
|
||||
'used': '已使用',
|
||||
'pending': '待处理',
|
||||
'cancelled': '已取消',
|
||||
'bound': '已绑定',
|
||||
'unbound': '未绑定'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: any) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: any) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = async (file: any) => {
|
||||
try {
|
||||
// 对所有文件类型都使用 fetch 下载,确保真正的下载
|
||||
const response = await fetch(file.file_full, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = file.name || file.file_name || 'download'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||
ElMessage.error(`下载失败: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal && props.paymentId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听付款ID变化
|
||||
watch(
|
||||
() => props.paymentId,
|
||||
(newId) => {
|
||||
if (visible.value && newId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-detail-dialog {
|
||||
.detail-content {
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
|
||||
.border {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3d7bff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
//padding: 8px 0;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.amount {
|
||||
font-weight: 600;
|
||||
color: #3d7bff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-content {
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: #999;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 打印样式
|
||||
@media print {
|
||||
.dialog-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-detail-dialog {
|
||||
.section-title {
|
||||
.border {
|
||||
background: #000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div class="file-preview-container">
|
||||
<div class="form-title">
|
||||
<span class="title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="files.length > 0" class="files-list">
|
||||
<div v-for="(file, index) in files" :key="index" class="file-item">
|
||||
<div v-if="isImageFile(file)" class="image-preview">
|
||||
<img
|
||||
:src="file.url"
|
||||
:alt="file.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(file)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<div class="file-actions">
|
||||
<!-- <el-button type="text" size="small" @click="previewImage(file)">预览</el-button> -->
|
||||
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewFile(file)">预览</el-button>
|
||||
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ emptyText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<files-previews v-model="fileshow" :file="filepviews" @close="onClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import FilesPreviews from '@/components/custom/files_previews.vue'
|
||||
interface FileItem {
|
||||
name: string
|
||||
url: string
|
||||
file_name: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilePreview',
|
||||
components: { FilesPreviews },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
files: {
|
||||
type: Array as () => FileItem[],
|
||||
default: () => []
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无文件'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const fileshow = ref(false)
|
||||
const filepviews = ref({} as FileItem)
|
||||
function onClose() {
|
||||
fileshow.value = false
|
||||
console.log('文件预览已关闭')
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: FileItem) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: FileItem) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = async (file: FileItem) => {
|
||||
try {
|
||||
// 对所有文件类型都使用 fetch 下载,确保真正的下载
|
||||
const response = await fetch(file.url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: '*/*'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = file.name || file.file_name || 'download'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||
ElMessage.error(`下载失败: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
const previewFile = (file: FileItem) => {
|
||||
filepviews.value = file
|
||||
fileshow.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
fileshow,
|
||||
filepviews,
|
||||
previewFile,
|
||||
onClose,
|
||||
isImageFile,
|
||||
previewImage,
|
||||
downloadFile
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-preview-container {
|
||||
.form-title {
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.title {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
flex: 0 0 auto;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: 120px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
flex: 1;
|
||||
max-height: 70px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-icon i {
|
||||
font-size: 48px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
padding: 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
title="内勤专用"
|
||||
width="90%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px">
|
||||
<span>显示币种:</span>
|
||||
<el-select v-model="selectedCurrency" placeholder="选择币种" style="width: 200px">
|
||||
<el-option
|
||||
v-for="opt in currencyOptions"
|
||||
:key="opt.currency_code"
|
||||
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
|
||||
:value="opt.currency_code"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="displayData" style="width: 100%">
|
||||
<el-table-column label="编号" width="80" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="model" label="车型" min-width="160" />
|
||||
<el-table-column prop="qty" label="数量" width="80" />
|
||||
<el-table-column prop="clearance_price" label="报关价" min-width="140">
|
||||
<template #default="scope">{{ scope.row.clearance_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fob_transport_fee" label="FOB运费" min-width="140">
|
||||
<template #default="scope">{{ scope.row.fob_transport_fee }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="car_price" label="车价" min-width="140">
|
||||
<template #default="scope">{{ scope.row.car_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ocean_freight" label="海运费" min-width="140">
|
||||
<template #default="scope">{{ scope.row.ocean_freight }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="commission" label="佣金" min-width="140">
|
||||
<template #default="scope">{{ scope.row.commission }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="retention_price" label="挂账" min-width="140">
|
||||
<template #default="scope">{{ scope.row.retention_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="port_misc_fee" label="港杂" min-width="140">
|
||||
<template #default="scope">{{ scope.row.port_misc_fee }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
|
||||
defineOptions({
|
||||
name: 'InternalStaffView'
|
||||
})
|
||||
|
||||
interface CurrencyOption {
|
||||
currency_code: string
|
||||
currency_name: string
|
||||
rate?: number
|
||||
exchange_rate?: number
|
||||
}
|
||||
|
||||
interface VehicleData {
|
||||
model_snapshot?: string
|
||||
model?: string
|
||||
qty?: number
|
||||
clearance_price?: any
|
||||
clearance_price_currency?: string
|
||||
fob_transport_fee?: any
|
||||
fob_fee_currency?: string
|
||||
car_price?: any
|
||||
car_price_currency?: string
|
||||
ocean_freight?: any
|
||||
ocean_freight_currency?: string
|
||||
commission?: any
|
||||
commission_currency?: string
|
||||
retention_price?: any
|
||||
retention_price_currency?: string
|
||||
port_misc_fee?: any
|
||||
port_fee_currency?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
vehicleData: VehicleData[]
|
||||
defaultCurrency?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
const selectedCurrency = ref('')
|
||||
const currencyOptions = ref<CurrencyOption[]>([])
|
||||
|
||||
const loadCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch (e) {
|
||||
console.error('获取币种失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const rateOf = (c?: string) => {
|
||||
const found = (currencyOptions.value || []).find((x: any) => x.currency_code === c)
|
||||
return Number(found?.rate || found?.exchange_rate || 0) || 0
|
||||
}
|
||||
|
||||
const convert = (amount: any, fromCode?: string, toCode?: string): number => {
|
||||
const val = amount === undefined || amount === null || amount === '' ? 0 : Number(amount)
|
||||
if (Number.isNaN(val)) return 0
|
||||
const from = fromCode || 'USD'
|
||||
const to = toCode || 'USD'
|
||||
if (from === to) return val
|
||||
const rf = rateOf(from)
|
||||
const rt = rateOf(to) || 1
|
||||
if (!rf) return 0
|
||||
return (val * rf) / rt
|
||||
}
|
||||
|
||||
const fmt = (n: number, cur: string) => `${n.toFixed(2)} ${cur}`
|
||||
|
||||
const displayData = computed(() => {
|
||||
const code = selectedCurrency.value
|
||||
return (props.vehicleData || []).map((it: VehicleData) => {
|
||||
return {
|
||||
model: it.model_snapshot || it.model || '',
|
||||
qty: it.qty || 1,
|
||||
clearance_price: fmt(
|
||||
convert(
|
||||
it.clearance_price?.amount ?? it.clearance_price,
|
||||
it.clearance_price?.currency || it.clearance_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
fob_transport_fee: fmt(
|
||||
convert(
|
||||
it.fob_transport_fee?.amount ?? it.fob_transport_fee,
|
||||
it.fob_transport_fee?.currency || it.fob_fee_currency || it.ocean_freight_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
car_price: fmt(
|
||||
convert(
|
||||
it.car_price?.amount ?? it.car_price,
|
||||
it.car_price?.currency || it.car_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
ocean_freight: fmt(
|
||||
convert(
|
||||
it.ocean_freight?.amount ?? it.ocean_freight,
|
||||
it.ocean_freight?.currency || it.ocean_freight_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
commission: fmt(
|
||||
convert(
|
||||
it.commission?.amount ?? it.commission,
|
||||
it.commission?.currency || it.commission_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
retention_price: fmt(
|
||||
convert(
|
||||
it.retention_price?.amount ?? it.retention_price,
|
||||
it.retention_price?.currency || it.retention_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
port_misc_fee: fmt(
|
||||
convert(
|
||||
it.port_misc_fee?.amount ?? it.port_misc_fee,
|
||||
it.port_misc_fee?.currency || it.port_fee_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 监听 visible 变化,加载币种选项
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible) {
|
||||
loadCurrencyOptions()
|
||||
// 设置默认币种
|
||||
if (props.defaultCurrency) {
|
||||
selectedCurrency.value = props.defaultCurrency
|
||||
} else {
|
||||
selectedCurrency.value = 'USD'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px">
|
||||
<span>显示币种:</span>
|
||||
<el-select v-model="selectedCurrency" placeholder="选择币种" style="width: 200px">
|
||||
<el-option
|
||||
v-for="opt in currencyOptions"
|
||||
:key="opt.currency_code"
|
||||
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
|
||||
:value="opt.currency_code"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<el-table :data="displayData" style="width: 100%">
|
||||
<el-table-column label="编号" width="80" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="model" label="车型" min-width="160" />
|
||||
<el-table-column prop="qty" label="数量" width="80" />
|
||||
<el-table-column prop="clearance_price" label="报关价" min-width="140">
|
||||
<template #default="scope">{{ scope.row.clearance_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fob_transport_fee" label="FOB运费" min-width="140">
|
||||
<template #default="scope">{{ scope.row.fob_transport_fee }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="car_price" label="车价" min-width="140">
|
||||
<template #default="scope">{{ scope.row.car_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ocean_freight" label="海运费" min-width="140">
|
||||
<template #default="scope">{{ scope.row.ocean_freight }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="commission" label="佣金" min-width="140">
|
||||
<template #default="scope">{{ scope.row.commission }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="retention_price" label="挂账" min-width="140">
|
||||
<template #default="scope">{{ scope.row.retention_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="port_misc_fee" label="港杂" min-width="140">
|
||||
<template #default="scope">{{ scope.row.port_misc_fee }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
|
||||
defineOptions({
|
||||
name: 'InternalStaffViews'
|
||||
})
|
||||
|
||||
interface CurrencyOption {
|
||||
currency_code: string
|
||||
currency_name: string
|
||||
rate?: number
|
||||
exchange_rate?: number
|
||||
}
|
||||
|
||||
interface VehicleData {
|
||||
model_snapshot?: string
|
||||
model?: string
|
||||
qty?: number
|
||||
clearance_price?: any
|
||||
clearance_price_currency?: string
|
||||
fob_transport_fee?: any
|
||||
fob_fee_currency?: string
|
||||
car_price?: any
|
||||
car_price_currency?: string
|
||||
ocean_freight?: any
|
||||
ocean_freight_currency?: string
|
||||
commission?: any
|
||||
commission_currency?: string
|
||||
retention_price?: any
|
||||
retention_price_currency?: string
|
||||
port_misc_fee?: any
|
||||
port_fee_currency?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
vehicleData: VehicleData[]
|
||||
defaultCurrency?: string
|
||||
}>()
|
||||
|
||||
// const emit = defineEmits<{
|
||||
// 'update:visible': [value: boolean]
|
||||
// }>()
|
||||
|
||||
const selectedCurrency = ref('')
|
||||
const currencyOptions = ref<CurrencyOption[]>([])
|
||||
|
||||
const loadCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch (e) {
|
||||
console.error('获取币种失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const rateOf = (c?: string) => {
|
||||
const found = (currencyOptions.value || []).find((x: any) => x.currency_code === c)
|
||||
return Number(found?.rate || found?.exchange_rate || 0) || 0
|
||||
}
|
||||
|
||||
const convert = (amount: any, fromCode?: string, toCode?: string): number => {
|
||||
const val = amount === undefined || amount === null || amount === '' ? 0 : Number(amount)
|
||||
if (Number.isNaN(val)) return 0
|
||||
const from = fromCode || 'USD'
|
||||
const to = toCode || 'USD'
|
||||
if (from === to) return val
|
||||
const rf = rateOf(from)
|
||||
const rt = rateOf(to) || 1
|
||||
if (!rf) return 0
|
||||
return (val * rf) / rt
|
||||
}
|
||||
|
||||
const fmt = (n: number, cur: string) => `${n.toFixed(2)} ${cur}`
|
||||
|
||||
const displayData = computed(() => {
|
||||
const code = selectedCurrency.value
|
||||
return (props.vehicleData || []).map((it: VehicleData) => {
|
||||
return {
|
||||
model: it.model_snapshot || it.model || '',
|
||||
qty: it.qty || 1,
|
||||
clearance_price: fmt(
|
||||
convert(
|
||||
it.clearance_price?.amount ?? it.clearance_price,
|
||||
it.clearance_price?.currency || it.clearance_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
fob_transport_fee: fmt(
|
||||
convert(
|
||||
it.fob_transport_fee?.amount ?? it.fob_transport_fee,
|
||||
it.fob_transport_fee?.currency || it.fob_fee_currency || it.ocean_freight_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
car_price: fmt(
|
||||
convert(
|
||||
it.car_price?.amount ?? it.car_price,
|
||||
it.car_price?.currency || it.car_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
ocean_freight: fmt(
|
||||
convert(
|
||||
it.ocean_freight?.amount ?? it.ocean_freight,
|
||||
it.ocean_freight?.currency || it.ocean_freight_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
commission: fmt(
|
||||
convert(
|
||||
it.commission?.amount ?? it.commission,
|
||||
it.commission?.currency || it.commission_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
retention_price: fmt(
|
||||
convert(
|
||||
it.retention_price?.amount ?? it.retention_price,
|
||||
it.retention_price?.currency || it.retention_price_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
),
|
||||
port_misc_fee: fmt(
|
||||
convert(
|
||||
it.port_misc_fee?.amount ?? it.port_misc_fee,
|
||||
it.port_misc_fee?.currency || it.port_fee_currency,
|
||||
code
|
||||
),
|
||||
code
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
loadCurrencyOptions()
|
||||
// 设置默认币种
|
||||
if (props.defaultCurrency) {
|
||||
selectedCurrency.value = props.defaultCurrency
|
||||
} else {
|
||||
selectedCurrency.value = 'USD'
|
||||
}
|
||||
// 监听 visible 变化,加载币种选项
|
||||
// watch(
|
||||
// () => props.visible,
|
||||
// (newVisible) => {
|
||||
// if (newVisible) {
|
||||
// loadCurrencyOptions()
|
||||
// // 设置默认币种
|
||||
// if (props.defaultCurrency) {
|
||||
// selectedCurrency.value = props.defaultCurrency
|
||||
// } else {
|
||||
// selectedCurrency.value = 'USD'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
</script>
|
||||
@@ -0,0 +1,732 @@
|
||||
<template>
|
||||
<div class="order-detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="basic-info-section">
|
||||
<div class="basic-info-item">
|
||||
<span class="label">备货单编号:</span>
|
||||
<span class="value">{{ orderData.order_no || orderData.orderNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="basic-info-item">
|
||||
<span class="label">业务员:</span>
|
||||
<span class="value">{{
|
||||
orderData.salesperson_snapshot || orderData.salesperson || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div class="customer-info-table">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="info-label"> 阿里订单号/PI号:</span>
|
||||
<span class="info-value">{{ orderData.pi_no || orderData.piNo || '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 日期:</span>
|
||||
<span class="info-value">{{ orderData.created_at || orderData.date || '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 清关目的国:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.clearance_country_snapshot || orderData.clearanceCountry || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户来源:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_source_snapshot || orderData.customerSource || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户名称:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_snapshot || orderData.customerName || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户所在国:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_country_snapshot || orderData.customerCountry || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情表格(ArtTable) -->
|
||||
<div class="table-section">
|
||||
<ArtTable ref="tableRef" :data="vehItems" border :pagination="false">
|
||||
<template #default>
|
||||
<ElTableColumn type="expand" width="30">
|
||||
<template #default="scope">
|
||||
<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>
|
||||
<ElTableColumn label="编号" width="60" align="center">
|
||||
<template #default="scope">{{ scope.$index + 1 }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="车型" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<div>{{ scope.row.model_snapshot || scope.row.model || '—' }}</div>
|
||||
<div v-if="scope.row.mileage > 0">里程:{{ scope.row.mileage }}km</div>
|
||||
<div v-if="scope.row.vin && scope.row.vin != ''">VIN: {{ scope.row.vin }}</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="数量/台" width="90" align="center">
|
||||
<template #default="scope">{{ scope.row.num ?? scope.row.quantity ?? 1 }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="颜色" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<div> 外饰:{{ scope.row.color_exterior_snapshot || '—' }} </div>
|
||||
<div> 内饰:{{ scope.row.color_interior_snapshot || '—' }} </div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="费用" min-width="200" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>官方指导价/二手挂牌价:</span>
|
||||
<span v-if="scope.row.mileage > 0" class="price-num">
|
||||
{{
|
||||
scope.row.listing_price?.currency_info ||
|
||||
scope.row.listing_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="price-num">
|
||||
{{
|
||||
scope.row.official_price?.currency_info ||
|
||||
scope.row.official_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>优惠:</span>
|
||||
<span v-if="scope.row.mileage > 0" class="price-num">
|
||||
{{
|
||||
scope.row.reduction_price?.currency_info ||
|
||||
scope.row.reduction_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="price-num">
|
||||
{{
|
||||
scope.row.discount_price?.currency_info ||
|
||||
scope.row.discount_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>手续费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.handling_fee?.currency_info ||
|
||||
scope.row.handling_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>加价:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.increase_price?.currency_info ||
|
||||
scope.row.increase_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>改装费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.retrofit_fee?.currency_info ||
|
||||
scope.row.retrofit_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>配件费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.accessory_price?.currency_info ||
|
||||
scope.row.accessory_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>FOB运费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.fob_transport_fee?.currency_info ||
|
||||
scope.row.fob_transport_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>港杂费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.port_misc_fee?.currency_info ||
|
||||
scope.row.port_misc_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>海运费:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.ocean_freight?.currency_info ||
|
||||
scope.row.ocean_freight?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>佣金:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.commission?.currency_info ||
|
||||
scope.row.commission?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>挂账:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.retention_price?.currency_info ||
|
||||
scope.row.retention_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>购置税:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.purchase_tax?.currency_info ||
|
||||
scope.row.purchase_tax?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>预估退税额:</span>
|
||||
<span class="price-num">
|
||||
{{ scope.row.tax_refund?.currency_info || scope.row.tax_refund?.amount || '—' }}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>车价:</span>
|
||||
<span class="price-num">
|
||||
{{ scope.row.car_price?.currency_info || scope.row.car_price?.amount || '—' }}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>收款价:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.unit_price?.currency_info ||
|
||||
scope.row.unit_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>总价:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.total_price?.currency_info ||
|
||||
scope.row.total_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row">
|
||||
<span>报关价:</span>
|
||||
<span class="price-num">
|
||||
{{
|
||||
scope.row.clearance_price?.currency_info ||
|
||||
scope.row.clearance_price?.amount ||
|
||||
scope.row.clearance_price ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</ElCol>
|
||||
<ElCol :span="8" class="item-row"></ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="利润" width="150" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.profit?.currency_info ||
|
||||
scope.row.profit?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
<div class="order-summary">
|
||||
<div class="summary-item">
|
||||
<span class="label">合同汇率:</span>
|
||||
<span class="value">{{
|
||||
orderData.contract_exchange_rate || orderData.contractRate || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">合计到岸价:</span>
|
||||
<span class="value"> {{ orderData.cif_total?.currency_info || '—' }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">利润:</span>
|
||||
<span class="value">{{
|
||||
orderData.profit_total?.currency_info || orderData.profit_total?.amount || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 物流信息 -->
|
||||
<div class="customer-info-table">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="info-label"> 清关人:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.declarant_name || orderData.declarant || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td v-if="orderData.is_final == 0">
|
||||
<span class="info-label"> 预计发运日期:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.eta_ship_date || orderData.etaShipDate || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td v-else>
|
||||
<span class="info-label"> 全款收齐日:</span>
|
||||
<span class="info-value">{{ orderData.collect_all_date || '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 出运方式:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.shipping_method_snapshot || orderData.shippingMethod || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 国内收货港:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.domestic_port_snapshot || orderData.domesticPort || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 贸易条款:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.trade_term_snapshot || orderData.tradeTerms || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 清关目的港:</span>
|
||||
<span class="info-value">{{ orderData.clearance_port_snapshot || '—' }}</span>
|
||||
</td>
|
||||
<td v-if="orderData.is_final == 0">
|
||||
<span class="info-label"> 最迟到港时间:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.latest_arrival_date || orderData.latestArrival || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td v-else>
|
||||
<span class="info-label"> 提车日期:</span>
|
||||
<span class="info-value">{{ orderData.pick_up_date || '—' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 计划收款信息 -->
|
||||
<div
|
||||
class="payment-section"
|
||||
v-if="
|
||||
(orderData.account_list && orderData.account_list.length) ||
|
||||
(orderData.payments && orderData.payments.length)
|
||||
"
|
||||
>
|
||||
<div class="table-wrapper">
|
||||
<table class="payment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 140px"></th>
|
||||
<th>序号</th>
|
||||
<th>汇款国</th>
|
||||
<th>日期</th>
|
||||
<th>币种</th>
|
||||
<th>账户</th>
|
||||
<th>汇款人</th>
|
||||
<th>金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in paymentTableData" :key="index">
|
||||
<td v-if="index === 0" :rowspan="paymentTableData.length">计划收款信息</td>
|
||||
<td>{{ row.serial }}</td>
|
||||
<td>{{ row.country }}</td>
|
||||
<td>{{ row.date }}</td>
|
||||
<td>{{ row.currency }}</td>
|
||||
<td>{{ row.account }}</td>
|
||||
<td>{{ row.remitter }}</td>
|
||||
<td>{{ row.amount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, onMounted, nextTick, watch } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'OrderDetailContent',
|
||||
props: {
|
||||
orderData: {
|
||||
type: Object as () => any,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const paymentTableData = computed(() => {
|
||||
const list = (props.orderData as any).account_list
|
||||
? (props.orderData as any).account_list.map((p: any, idx: number) => ({
|
||||
group: '计划收款信息',
|
||||
serial: `收款${idx + 1}`,
|
||||
country: p.remit_country_name_snapshot || '—',
|
||||
date: p.remit_date || '—',
|
||||
currency: p.currency_name_snapshot || '—',
|
||||
account: p.account_name_snapshot || '—',
|
||||
remitter: p.remitter_name || '—',
|
||||
amount: p.amount || '—'
|
||||
}))
|
||||
: (props.orderData as any).payments || []
|
||||
if (!list.length) return []
|
||||
return list
|
||||
})
|
||||
|
||||
const formatPrice = (price?: number): string => {
|
||||
if (price === undefined || price === null) return '0.00'
|
||||
return Number(price).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
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(() => {
|
||||
nextTick(() => {
|
||||
if (vehItems.value && vehItems.value.length) {
|
||||
tableRef.value?.expandAll?.()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认展开所有行(等数据和表格渲染完成后再触发)
|
||||
expandWhenReady()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => vehItems.value?.length,
|
||||
(len) => {
|
||||
if (len && len > 0) expandWhenReady()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
return {
|
||||
paymentTableData,
|
||||
vehItems,
|
||||
formatPrice,
|
||||
tableRef,
|
||||
getItemAttachments
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-detail-content {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.basic-info-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.basic-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.value {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-info-table {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
td {
|
||||
padding: 12px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
font-size: 13px;
|
||||
}
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
.info-label {
|
||||
color: #666;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.order-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
|
||||
th {
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.col-no {
|
||||
width: 60px;
|
||||
}
|
||||
.col-model {
|
||||
max-width: 160px;
|
||||
// text-align: left;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 订单底部汇总样式 */
|
||||
.order-summary {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.order-summary .summary-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
// background: #fff;
|
||||
// border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.order-summary .label {
|
||||
color: #666;
|
||||
}
|
||||
.order-summary .value {
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* ArtTable 表头背景色 */
|
||||
.table-section :deep(.el-table__header th) {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table-section :deep(.el-table__header .cell) {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
.payment-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
.payment-table th {
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 12px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.payment-table td {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 10px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.el-table__expand-cell {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
.el-table__expand-wrapper {
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.item-row-wrap {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.item-row-wrap .item-row {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 5px 0;
|
||||
// min-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
.price-num {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
margin-left: 5px;
|
||||
}
|
||||
/* 打印优化 */
|
||||
@media print {
|
||||
.order-detail-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
.order-table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="付款记录详情"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="payment-detail-dialog" v-loading="loading">
|
||||
<div v-if="paymentData" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">基本信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>创建日期:</label>
|
||||
<span>{{ formatDate(paymentData.created_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>付款来源:</label>
|
||||
<span>{{ paymentData.country_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>汇款人:</label>
|
||||
<span>{{ paymentData.remitter || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>账户:</label>
|
||||
<span>{{ paymentData.account_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>币种:</label>
|
||||
<span>{{ paymentData.currency_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>金额:</label>
|
||||
<span class="amount">{{ paymentData.amount }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>付款水单:</label>
|
||||
<div>
|
||||
<p v-for="(item, index) in paymentData.voucher_file" :key="index">
|
||||
<el-image
|
||||
v-if="
|
||||
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(
|
||||
(item.file_full || '').split('?')[0]
|
||||
)
|
||||
"
|
||||
:src="item.file_full"
|
||||
:preview-src-list="item.file_full ? [item.file_full] : []"
|
||||
style="width: 100px; height: 100px"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="item.file_full"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ item.file_name }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联信息 -->
|
||||
<div class="section" v-if="paymentData.payment_apply_id">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>付款申请ID:</label>
|
||||
<span>{{ paymentData.payment_apply_id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>关联状态:</label>
|
||||
<el-tag :type="getStatusType(paymentData.status)">
|
||||
{{ getStatusText(paymentData.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收款记录关联 -->
|
||||
<div
|
||||
class="section"
|
||||
v-if="paymentData.receipt_records && paymentData.receipt_records.length"
|
||||
>
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联收款记录</span>
|
||||
</div>
|
||||
<el-table :data="paymentData.receipt_records" style="margin-top: 12px">
|
||||
<el-table-column label="收款记录ID" prop="id" min-width="120" />
|
||||
<el-table-column label="创建日期" prop="created_at" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<div class="section" v-if="paymentData.remark">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">备注信息</span>
|
||||
</div>
|
||||
<div class="remark-content">
|
||||
{{ paymentData.remark }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无数据状态 -->
|
||||
<div v-else-if="!loading" class="no-data">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div> -->
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
|
||||
interface PaymentRecord {
|
||||
id: string | number
|
||||
created_at: string
|
||||
country_name: string
|
||||
remitter: string
|
||||
account_name: string
|
||||
currency_name: string
|
||||
amount: number
|
||||
status: string
|
||||
payment_apply_id?: string | number
|
||||
receipt_records?: any[]
|
||||
voucher_file?: any[]
|
||||
submit_files?: string[]
|
||||
remark?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
paymentId?: string | number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const paymentData = ref<PaymentRecord | null>(null)
|
||||
|
||||
// 获取付款记录详情
|
||||
const getPaymentDetail = async () => {
|
||||
if (!props.paymentId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await financeService.getPaymentDetail({ id: props.paymentId })
|
||||
paymentData.value = res
|
||||
} catch (error) {
|
||||
console.error('获取付款记录详情失败:', error)
|
||||
paymentData.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
paymentData.value = null
|
||||
}
|
||||
|
||||
// 打印功能
|
||||
const handlePrint = () => {
|
||||
window.print()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '—'
|
||||
try {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return `${currency || ''} ${amount?.toLocaleString() || 0}`
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
|
||||
const statusMap: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
|
||||
available: 'success',
|
||||
used: 'info',
|
||||
pending: 'warning',
|
||||
cancelled: 'danger',
|
||||
bound: 'success',
|
||||
unbound: 'info'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': '可用',
|
||||
'used': '已使用',
|
||||
'pending': '待处理',
|
||||
'cancelled': '已取消',
|
||||
'bound': '已绑定',
|
||||
'unbound': '未绑定'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal && props.paymentId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听付款ID变化
|
||||
watch(() => props.paymentId, (newId) => {
|
||||
if (visible.value && newId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-detail-dialog {
|
||||
.detail-content {
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
|
||||
.border {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3d7bff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// padding: 8px 0;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.amount {
|
||||
font-weight: 600;
|
||||
color: #3d7bff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-content {
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: #999;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 打印样式
|
||||
@media print {
|
||||
.dialog-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-detail-dialog {
|
||||
.section-title {
|
||||
.border {
|
||||
background: #000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择付款记录"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="payment-record-dialog">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入汇款人、账户或金额"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@keyup.enter="getPaymentRecords"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getPaymentRecords"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="getPaymentRecords">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 付款记录表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="paymentRecords"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
highlight-current-row
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<el-table-column label="创建日期" prop="created_at" min-width="160" />
|
||||
<el-table-column label="付款来源" prop="country_name" min-width="120" />
|
||||
<el-table-column label="汇款人" prop="remitter" min-width="120" />
|
||||
<el-table-column label="账户" prop="account_name" min-width="120" />
|
||||
<el-table-column label="账币" prop="currency_name" min-width="120" />
|
||||
<el-table-column label="金额" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="选择" width="80">
|
||||
<template #default="scope">
|
||||
<el-checkbox
|
||||
v-model="checkboxStates[scope.row.id]"
|
||||
@change="(val) => handleCheckboxChange(scope.row, !!val)"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedRecord" @click="handleConfirm">
|
||||
确认绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface PaymentRecord {
|
||||
id: string | number
|
||||
created_at: string
|
||||
country_name: string
|
||||
remitter: string
|
||||
account_name: string
|
||||
currency_name: string
|
||||
amount: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
receiptId?: string | number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', record: PaymentRecord): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
// const paymentRecords = ref<PaymentRecord[]>([])
|
||||
const paymentRecords = ref<any[]>([])
|
||||
const selectedRecord = ref<PaymentRecord | null>(null)
|
||||
// const selectedRecord = ref<any[]>([])
|
||||
const selectedRecordId = ref<any>(null)
|
||||
const checkboxStates = reactive<Record<string | number, boolean>>({})
|
||||
const tableRef = ref()
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 获取付款记录列表
|
||||
const getPaymentRecords = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: pagination.currentPage,
|
||||
size: pagination.pageSize,
|
||||
keyword: searchKeyword.value,
|
||||
scene: 'correlation',
|
||||
receipt_id: props.receiptId,
|
||||
correlation: 'unbind' // 只显示可用的付款记录
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await financeService.getPaymentList(params)
|
||||
paymentRecords.value = records || []
|
||||
// 初始化 checkbox 状态
|
||||
records?.forEach((record: any) => {
|
||||
checkboxStates[record.id] = false
|
||||
})
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取付款记录失败:', error)
|
||||
ElMessage.error('获取付款记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理选择变化(保留兼容性,单选模式下不使用此方法)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleSelectionChange = (_selection: PaymentRecord[]) => {
|
||||
// 单选模式下不使用此方法
|
||||
}
|
||||
|
||||
// 处理 checkbox 变化(单选逻辑)
|
||||
const handleCheckboxChange = (record: PaymentRecord, checked: boolean) => {
|
||||
if (checked) {
|
||||
// 先清除所有选中状态(单选逻辑)
|
||||
Object.keys(checkboxStates).forEach((id) => {
|
||||
checkboxStates[id] = false
|
||||
})
|
||||
// 选中当前项
|
||||
checkboxStates[record.id] = true
|
||||
selectedRecord.value = record
|
||||
selectedRecordId.value = record.id
|
||||
} else {
|
||||
// 取消选中
|
||||
selectedRecord.value = null
|
||||
selectedRecordId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (record: PaymentRecord) => {
|
||||
// 如果点击的是已选中的项,则取消选中;否则选中该项
|
||||
if (selectedRecordId.value === record.id) {
|
||||
// 取消选中
|
||||
selectedRecord.value = null
|
||||
selectedRecordId.value = null
|
||||
checkboxStates[record.id] = false
|
||||
} else {
|
||||
// 先清除所有选中状态
|
||||
Object.keys(checkboxStates).forEach((id) => {
|
||||
checkboxStates[id] = false
|
||||
})
|
||||
// 选中当前项
|
||||
selectedRecord.value = record
|
||||
selectedRecordId.value = record.id
|
||||
checkboxStates[record.id] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.currentPage = 1
|
||||
getPaymentRecords()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getPaymentRecords()
|
||||
}
|
||||
|
||||
// 确认绑定
|
||||
const handleConfirm = () => {
|
||||
if (!selectedRecord.value) {
|
||||
ElMessage.warning('请选择一条付款记录')
|
||||
return
|
||||
}
|
||||
emit('confirm', selectedRecord.value)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedRecord.value = null
|
||||
selectedRecordId.value = null
|
||||
Object.keys(checkboxStates).forEach((id) => {
|
||||
checkboxStates[id] = false
|
||||
})
|
||||
searchKeyword.value = ''
|
||||
pagination.currentPage = 1
|
||||
}
|
||||
|
||||
// 格式化金额显示
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return `${currency} ${amount?.toLocaleString() || 0}`
|
||||
}
|
||||
|
||||
// 获取状态类型(暂未使用)
|
||||
// const getStatusType = (status: string) => {
|
||||
// const statusMap: Record<string, string> = {
|
||||
// available: 'success',
|
||||
// used: 'info',
|
||||
// pending: 'warning',
|
||||
// cancelled: 'danger'
|
||||
// }
|
||||
// return statusMap[status] || 'info'
|
||||
// }
|
||||
|
||||
// 获取状态文本(暂未使用)
|
||||
// const getStatusText = (status: string) => {
|
||||
// const statusMap: Record<string, string> = {
|
||||
// available: '可用',
|
||||
// used: '已使用',
|
||||
// pending: '待处理',
|
||||
// cancelled: '已取消'
|
||||
// }
|
||||
// return statusMap[status] || status
|
||||
// }
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
getPaymentRecords()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-record-dialog {
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="print-preview-container">
|
||||
<div class="preview-header">
|
||||
<div class="header-actions">
|
||||
<el-button @click="download" v-if="showDownload">下载</el-button>
|
||||
<el-button @click="print" type="primary">打印</el-button>
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div
|
||||
v-if="pdf.images.length"
|
||||
class="images-container"
|
||||
>
|
||||
<img
|
||||
v-for="(img, idx) in pdf.images"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
class="preview-image"
|
||||
/>
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
class="preview-iframe"
|
||||
@load="onIframeLoad"
|
||||
@error="onIframeError"
|
||||
/>
|
||||
<div v-else class="no-content">
|
||||
<p>没有可预览的内容</p>
|
||||
<p>传入的 PDF URL: {{ pdfUrl }}</p>
|
||||
<p>PDF Object URL: {{ pdf.url }}</p>
|
||||
<p>PDF Blob URL: {{ pdf.blobUrl }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch } from 'vue'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PrintPreview',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showDownload: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
pdfUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
// 使用公共的 usePdfPreview
|
||||
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
|
||||
|
||||
// 监听 visible 变化,当显示时加载 PDF
|
||||
watch(() => props.visible, (newVisible) => {
|
||||
console.log('PrintPreview visible 变化:', newVisible)
|
||||
if (newVisible && props.pdfUrl) {
|
||||
// 构造响应对象,模拟 API 返回格式
|
||||
const response = {
|
||||
url: props.pdfUrl,
|
||||
file_url: props.pdfUrl,
|
||||
file_url_full: props.pdfUrl
|
||||
}
|
||||
openWithResponse(response)
|
||||
console.log('PrintPreview 设置 PDF URL:', props.pdfUrl)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 pdfUrl 变化
|
||||
watch(() => props.pdfUrl, (newUrl) => {
|
||||
console.log('PrintPreview pdfUrl 变化:', newUrl)
|
||||
if (newUrl) {
|
||||
// 构造响应对象,模拟 API 返回格式
|
||||
const response = {
|
||||
url: newUrl,
|
||||
file_url: newUrl,
|
||||
file_url_full: newUrl
|
||||
}
|
||||
openWithResponse(response)
|
||||
console.log('PrintPreview 更新 PDF URL:', newUrl)
|
||||
}
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// iframe 加载事件
|
||||
const onIframeLoad = () => {
|
||||
console.log('iframe 加载完成')
|
||||
}
|
||||
|
||||
// iframe 错误事件
|
||||
const onIframeError = (error: any) => {
|
||||
console.error('iframe 加载错误:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
frameRef,
|
||||
pdf,
|
||||
download,
|
||||
print,
|
||||
close,
|
||||
onIframeLoad,
|
||||
onIframeError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.print-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background: #fafafa;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.images-container {
|
||||
overflow: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.no-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 打印样式
|
||||
@media print {
|
||||
.print-preview-container {
|
||||
max-height: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.images-container {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="detail-wrap">
|
||||
<div class="detail-grid">
|
||||
<div class="item">
|
||||
<span class="label">采购单号:</span>
|
||||
<span class="value">{{ detailData.order_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">PI号:</span>
|
||||
<span class="value">{{ detailData.pi_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">提交人:</span>
|
||||
<span class="value">{{ detailData.buyer_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="item full">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ detailData.remark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">合同文件</div>
|
||||
<div
|
||||
v-if="detailData.contract_file_url && detailData.contract_file_url.length"
|
||||
class="files-list"
|
||||
>
|
||||
<div class="file-row" v-for="(f, idx) in detailData.contract_file_url" :key="idx">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ f.name }}</span>
|
||||
<a class="el-link el-link--primary" :href="f.url" target="_blank" rel="noopener">查看</a>
|
||||
<a class="el-link el-link--info" :href="f.url" :download="f.name">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无合同文件</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">备货单组</div>
|
||||
<div v-if="detailData.stock_order && detailData.stock_order.length" class="groups">
|
||||
<div class="group" v-for="(g, gi) in detailData.stock_order" :key="gi">
|
||||
<div class="group-head">
|
||||
<span class="order">{{ g.order_no || '备货单' }}</span>
|
||||
<span class="seller" v-if="g.salesperson_snapshot"
|
||||
>({{ g.salesperson_snapshot }})</span
|
||||
>
|
||||
<span class="date" v-if="g.biz_date">{{ g.biz_date }}</span>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<span class="chip" v-for="(v, vi) in g.stock_order_items || []" :key="vi">
|
||||
{{ v.model_snapshot }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无备货单数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
// 定义组件的数据类型
|
||||
interface DetailData {
|
||||
order_no?: string
|
||||
pi_no?: string
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
contract_file_url?: Array<{ name: string; url: string }>
|
||||
stock_order?: Array<{
|
||||
order_no?: string
|
||||
salesperson_snapshot?: string
|
||||
biz_date?: string
|
||||
stock_order_items?: Array<{ model_snapshot: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
// 使用 Vue 的 props 传递数据
|
||||
export default {
|
||||
name: 'ProcurementDetailView',
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object as PropType<DetailData>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px 16px;
|
||||
}
|
||||
.detail-grid .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-grid .item.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.detail-grid .label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.detail-grid .value {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.detail-block {
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
}
|
||||
.empty-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 38vh;
|
||||
overflow-y: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.group {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
padding: 14px 6px 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
.group-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-content: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #f0f6ff;
|
||||
color: #3a7af3;
|
||||
border: 1px solid #e3edff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="detail-wrap">
|
||||
<div class="detail-grid">
|
||||
<div class="item">
|
||||
<span class="label">采购单号:</span>
|
||||
<span class="value">{{ detailData.order_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">PI号:</span>
|
||||
<span class="value">{{ detailData.pi_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">提交人:</span>
|
||||
<span class="value">{{ detailData.buyer_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="item full">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ detailData.remark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">合同文件</div>
|
||||
<div
|
||||
v-if="detailData.contract_file_url && detailData.contract_file_url.length"
|
||||
class="files-list"
|
||||
>
|
||||
<div class="file-row" v-for="(f, idx) in detailData.contract_file_url" :key="idx">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ f.name }}</span>
|
||||
<a class="el-link el-link--primary" :href="f.url" target="_blank" rel="noopener">查看</a>
|
||||
<a class="el-link el-link--info" :href="f.url" :download="f.name">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无合同文件</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">备货单组</div>
|
||||
<div v-if="detailData.stock_order && detailData.stock_order.length" class="groups">
|
||||
<div class="group" v-for="(g, gi) in detailData.stock_order" :key="gi">
|
||||
<div class="group-head">
|
||||
<span class="order">{{ g.order_no || '备货单' }}</span>
|
||||
<span class="seller" v-if="g.salesperson_snapshot"
|
||||
>({{ g.salesperson_snapshot }})</span
|
||||
>
|
||||
<span class="date" v-if="g.biz_date">{{ g.biz_date }}</span>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<span class="chip" v-for="(v, vi) in g.stock_order_items || []" :key="vi">
|
||||
{{ v.model_snapshot }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无备货单数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
interface DetailData {
|
||||
order_no?: string
|
||||
pi_no?: string
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
contract_file_url?: Array<{ name: string; url: string }>
|
||||
stock_order?: Array<{
|
||||
order_no?: string
|
||||
salesperson_snapshot?: string
|
||||
biz_date?: string
|
||||
stock_order_items?: Array<{ model_snapshot: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
defineProps({
|
||||
detailData: {
|
||||
type: Object as PropType<DetailData>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px 16px;
|
||||
}
|
||||
.detail-grid .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-grid .item.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.detail-grid .label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.detail-grid .value {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.detail-block {
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
}
|
||||
.empty-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 38vh;
|
||||
overflow-y: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.group {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
padding: 14px 6px 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
.group-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-content: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #f0f6ff;
|
||||
color: #3a7af3;
|
||||
border: 1px solid #e3edff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="approval-timeline">
|
||||
<div v-for="(rec, idx) in records" :key="idx" class="timeline-item">
|
||||
<div class="left">
|
||||
<div class="avatar-box">
|
||||
<span class="initial">{{ rec.updated_by?.slice(0, 1) || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row name-role">
|
||||
<span class="name">编辑人:{{ rec.updated_by }}</span>
|
||||
</div>
|
||||
<div class="row time">{{ rec.updated_at }}</div>
|
||||
<div class="card-box">
|
||||
<el-card>
|
||||
<h4>汇款人:{{ rec.remitter }}</h4>
|
||||
<p
|
||||
><span>付款来源:{{ rec.payment_apply_id }}</span>
|
||||
<span>账户:{{ rec.account_short_desc }}</span>
|
||||
</p>
|
||||
<p
|
||||
><span>金额:{{ rec.amount }} </span><span>币种:{{ rec.currency_name }}</span></p
|
||||
>
|
||||
<p
|
||||
><span>折让金额:{{ rec.discount_amount }}</span
|
||||
><span>实际汇率:{{ rec.exchange_rate }}</span>
|
||||
</p>
|
||||
<p>总金额(人民币)实际汇率:{{ rec.total_amount_cny }}</p>
|
||||
<p>收款时间:{{ rec.received_at }}</p>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ApprovalRecordItem {
|
||||
account_id?: string
|
||||
account_name?: string
|
||||
amount?: string
|
||||
arrived_at?: string
|
||||
country_id?: string
|
||||
country_name?: string
|
||||
created_at?: string
|
||||
created_by?: string
|
||||
currency_id?: string
|
||||
currency_name?: string
|
||||
discount_amount?: string
|
||||
exchange_rate?: string
|
||||
fx_receipt_file_full?: string
|
||||
account_short_desc?: string
|
||||
id?: string
|
||||
is_domestic_return?: string
|
||||
payment_apply_id?: string
|
||||
receipt_id?: string
|
||||
receipt_no?: string
|
||||
received_at?: string
|
||||
remitter?: string
|
||||
total_amount_cny?: string
|
||||
updated_at?: string
|
||||
updated_by?: string
|
||||
voucher_no?: string
|
||||
}
|
||||
|
||||
defineProps<{ records: ApprovalRecordItem[] }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 审批记录 时间线 */
|
||||
.approval-timeline {
|
||||
padding: 8px 4px 8px 0;
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline-item + .timeline-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.left {
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: #3d7bff;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
font-size: 9px;
|
||||
padding: 3px 1px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.status-dot.pending {
|
||||
background: #3471ff;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
.status-dot.rejected {
|
||||
background: #ff0000;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
/* 连接下一个节点的虚线 */
|
||||
.timeline-item:not(:last-child) .left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 52px; /* avatar 46px + 间距 6px */
|
||||
left: 23px; /* 盒子宽度一半,确保竖线居中 */
|
||||
width: 0;
|
||||
height: calc(100% + 116px); /* 加上相邻项之间的间距 */
|
||||
border-left: 2px dotted #bfbfbf;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row.name-role {
|
||||
color: #222;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.role {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.empty {
|
||||
color: #999;
|
||||
padding: 8px 0 4px 4px;
|
||||
}
|
||||
.card-box p{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<el-drawer :with-header="false" :model-value="modelValue" :size="isMobile ? '100%' : '480px'" @close="$emit('update:modelValue', false)">
|
||||
<div class="chat-container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="name">{{ title }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-icon class="icon-close" :size="20" @click="$emit('update:modelValue', false)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" ref="messageContainer">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<div :class="['message-item', String(message.user_id) === String(currentUserId) ? 'message-right' : 'message-left']">
|
||||
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
|
||||
<div class="message-content">
|
||||
<div class="message-info">
|
||||
<span class="sender-name">{{ message.user_name }}</span>
|
||||
<span class="message-time">{{ message.updated_at }}</span>
|
||||
</div>
|
||||
<div class="message-text" v-html="message.content"></div>
|
||||
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
|
||||
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
|
||||
<img :src="attachment.storage_path_full" :alt="attachment.original_name" class="attachment-thumb" @click="handleChatImageClick(attachment.storage_path_full)" />
|
||||
</template>
|
||||
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
|
||||
<video :src="attachment.storage_path_full" controls class="attachment-thumb" @click.prevent="handleChatVideoClick(attachment.storage_path_full)"></video>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-attachment">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ attachment.original_name }}</span>
|
||||
<el-button type="primary" size="small" link @click="$emit('download', attachment)">下载</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="chat-input" v-if="canAdd">
|
||||
<div class="chat-attachment-upload">
|
||||
<el-upload v-model:file-list="chatFileList" class="chat-upload" :action="actionImageUplode" :on-success="handleChatUploadSuccess" :headers="uploadHeaders" multiple :limit="5" :on-exceed="handleChatExceed" :show-file-list="false">
|
||||
<el-button type="primary" link>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
添加附件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
|
||||
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||
<img :src="file.file_url || file.response?.data?.file_url" :alt="file.name" class="preview-thumb" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
</template>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<el-button type="danger" size="small" link @click="removeChatFile(idx)">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none" />
|
||||
<div class="chat-input-actions">
|
||||
<el-button type="primary" @click="emitSend">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内置预览 -->
|
||||
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
|
||||
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
||||
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width: 100%; max-height: 70vh; display: block"></video>
|
||||
</ElDialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
messages: any[]
|
||||
currentUserId: string | number
|
||||
canAdd?: boolean
|
||||
actionImageUplode?: string
|
||||
uploadHeaders?: Record<string, any>
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
(e: 'send', payload: { text: string; attachments: any[] }): void
|
||||
(e: 'download', attachment: any): void
|
||||
}>()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
|
||||
const messageText = ref('')
|
||||
const chatFileList = ref<any[]>([])
|
||||
const messageContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.messages,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (messageContainer.value) {
|
||||
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
||||
chatFileList.value = uploadFiles
|
||||
.filter((item) => item.status === 'success')
|
||||
.map((item) => {
|
||||
const data = item.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
name: data.original_name,
|
||||
file_url: data.storage_path_full,
|
||||
status: 'success',
|
||||
response: { data }
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean) as any[]
|
||||
}
|
||||
const handleChatExceed = () => {
|
||||
// no-op; parent can handle toast
|
||||
}
|
||||
const removeChatFile = (index: number) => {
|
||||
chatFileList.value.splice(index, 1)
|
||||
}
|
||||
const emitSend = () => {
|
||||
const text = messageText.value.trim()
|
||||
if (!text && chatFileList.value.length === 0) return
|
||||
const attachments = chatFileList.value.map((file) => {
|
||||
const data = file.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
original_name: data.original_name,
|
||||
storage_path: data.storage_path || data.file_name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}).filter(Boolean) as any[]
|
||||
emit('send', { text, attachments })
|
||||
messageText.value = ''
|
||||
chatFileList.value = []
|
||||
}
|
||||
|
||||
// 预览
|
||||
const chatImageList = ref<string[]>([])
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageViewerIndex = ref(0)
|
||||
function handleChatImageClick(url: string) {
|
||||
chatImageList.value = [url]
|
||||
imageViewerIndex.value = 0
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
const currentVideoUrl = ref('')
|
||||
const videoDialogVisible = ref(false)
|
||||
function handleChatVideoClick(url: string) {
|
||||
currentVideoUrl.value = url
|
||||
videoDialogVisible.value = true
|
||||
}
|
||||
function closeImageViewer() {
|
||||
imageViewerVisible.value = false
|
||||
chatImageList.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.header-left .name { font-size: 16px; font-weight: 500; }
|
||||
.header-right .icon-close { cursor: pointer; }
|
||||
}
|
||||
.chat-container { display: flex; flex-direction: column; height: calc(100% - 70px); }
|
||||
.chat-messages { flex: 1; padding: 30px 16px; overflow-y: auto; border-top: 1px solid var(--el-border-color-lighter); }
|
||||
.message-item { display: flex; gap: 8px; align-items: flex-start; width: 100%; margin-bottom: 30px; }
|
||||
.message-item.message-left { justify-content: flex-start; }
|
||||
.message-item.message-right { flex-direction: row-reverse; }
|
||||
.message-content { display: flex; flex-direction: column; max-width: 70%; }
|
||||
.message-info { display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; }
|
||||
.message-text { font-size: 14px; color: var(--art-gray-900); border-radius: 6px; padding: 10px 14px; line-height: 1.4; white-space: pre-line; }
|
||||
.message-left .message-text { background-color: #f8f5ff; }
|
||||
.message-right .message-text { background-color: #e9f3ff; }
|
||||
.message-attachments { margin-top: 8px; }
|
||||
.message-attachments .attachment-thumb { max-width: 200px; max-height: 150px; border-radius: 6px; cursor: pointer; border: 1px solid var(--el-border-color-lighter); }
|
||||
.chat-input { padding: 16px 16px 0; }
|
||||
.chat-input-actions { display: flex; justify-content: right; margin-top: 12px; }
|
||||
.chat-attachment-upload { margin-bottom: 12px; }
|
||||
.uploaded-attachments { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.attachment-preview-item { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--el-fill-color-light); border-radius: 6px; border: 1px solid var(--el-border-color-lighter); max-width: 200px; }
|
||||
.preview-thumb { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visibleLocal"
|
||||
:title="currentFile?.name || '文件预览'"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="false"
|
||||
width="90%"
|
||||
:fullscreen="isFullscreen"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="file-preview-toolbar">
|
||||
<div class="left">
|
||||
<!-- <el-button-group>
|
||||
<el-button size="small" @click="prevFile" :disabled="!hasPrev">上一页</el-button>
|
||||
<el-button size="small" @click="nextFile" :disabled="!hasNext">下一页</el-button>
|
||||
</el-button-group>
|
||||
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<el-button size="small" @click="zoomOut">-</el-button>
|
||||
<el-button size="small" @click="resetZoom">重置</el-button>
|
||||
<el-button size="small" @click="zoomIn">+</el-button>
|
||||
|
||||
<el-divider direction="vertical" /> -->
|
||||
|
||||
<!-- <el-button size="small" @click="rotateLeft">⟲</el-button> -->
|
||||
<!-- <el-button size="small" @click="rotateRight">⟳</el-button> -->
|
||||
|
||||
<!-- <el-divider direction="vertical" />
|
||||
|
||||
<el-button size="small" @click="toggleFullscreen">{{
|
||||
isFullscreen ? '退出全屏' : '全屏'
|
||||
}}</el-button> -->
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<!-- <el-button size="small" type="primary" :href="currentFile?.url" target="_blank"
|
||||
>在新标签打开</el-button
|
||||
> -->
|
||||
<!-- <el-button size="small" @click="downloadCurrent">下载</el-button> -->
|
||||
<el-button size="small" type="danger" @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-preview-body">
|
||||
<template v-if="!currentFile">
|
||||
<div class="no-file">无要预览的文件</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- IMAGE -->
|
||||
<div v-if="fileType === 'image'" class="viewer-wrap" :key="`image-${currentFile.url}`">
|
||||
<div class="viewer" :style="viewerStyle">
|
||||
<img
|
||||
:src="currentFile.url"
|
||||
:key="currentFile.url"
|
||||
@error="onError"
|
||||
@load="onLoadImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF -->
|
||||
<div v-else-if="fileType === 'pdf'" class="viewer-wrap" :key="`pdf-${currentFile.url}`">
|
||||
<div class="viewer" :style="viewerStyle">
|
||||
<!-- 使用 object / iframe 作为预览,支持大多数现代浏览器 -->
|
||||
<object
|
||||
:key="`pdf-obj-${currentFile.url}`"
|
||||
:data="appendPdfParams(currentFile.url)"
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<iframe
|
||||
:key="`pdf-iframe-${currentFile.url}`"
|
||||
:src="appendPdfParams(currentFile.url)"
|
||||
width="100%"
|
||||
height="100%"
|
||||
></iframe>
|
||||
<div class="fallback"
|
||||
>无法在浏览器内预览 PDF。<a :href="currentFile.url" target="_blank"
|
||||
>点击打开</a
|
||||
></div
|
||||
>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OFFICE (Word, Excel, PPT) 使用微软 Office 在线查看器,仅当文件对外可访问时有效 -->
|
||||
<div
|
||||
v-else-if="fileType === 'office'"
|
||||
class="viewer-wrap"
|
||||
:key="`office-${currentFile.url}`"
|
||||
>
|
||||
<div class="viewer" :style="viewerStyle">
|
||||
<template v-if="isHttpUrl(currentFile.url)">
|
||||
<iframe
|
||||
:key="`office-iframe-${currentFile.url}`"
|
||||
:src="officeViewerUrl(currentFile.url)"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="fallback"
|
||||
>Office 文件必须是可通过公网访问的 URL 才能使用在线预览。<a
|
||||
:href="currentFile.url"
|
||||
target="_blank"
|
||||
>点击打开或下载</a
|
||||
></div
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本 / 代码 -->
|
||||
<div v-else-if="fileType === 'text'" class="viewer-wrap" :key="`text-${currentFile.url}`">
|
||||
<div class="viewer text-viewer" :style="viewerStyle">
|
||||
<pre v-if="textContent !== null">{{ textContent }}</pre>
|
||||
<div v-else class="loading">正在加载文本内容...</div>
|
||||
<div v-if="textError" class="error">加载文本失败:{{ textError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未知 -->
|
||||
<div v-else class="viewer-wrap unknown-viewer" :key="`unknown-${currentFile.url}`">
|
||||
<div class="viewer" :style="viewerStyle">
|
||||
<div class="unknown">
|
||||
<p>无法在线预览此类型文件。</p>
|
||||
<p><a :href="currentFile.url" target="_blank">点击打开或下载文件</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="footer-info">{{ currentFile?.name || '' }}</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="FilesPreviews">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// --- Props ---
|
||||
interface FileItem {
|
||||
file_name: any
|
||||
name?: string
|
||||
url: string
|
||||
mime?: string // 可选,优先使用
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
file?: FileItem | null // 单文件
|
||||
files?: FileItem[] | null // 多文件
|
||||
startIndex?: number
|
||||
enableFullscreen?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// --- Local state ---
|
||||
const visibleLocal = ref<boolean>(!!props.modelValue)
|
||||
const currentIndex = ref<number>(props.startIndex ?? 0)
|
||||
const isFullscreen = ref<boolean>(!!props.enableFullscreen)
|
||||
const zoom = ref<number>(1)
|
||||
const rotate = ref<number>(0) // deg
|
||||
const textContent = ref<string | null>(null)
|
||||
const textError = ref<string | null>(null)
|
||||
|
||||
// --- Computed ---
|
||||
const currentFile = computed((): FileItem | null => {
|
||||
if (props.files && props.files.length > 0) {
|
||||
const idx = Math.min(Math.max(0, currentIndex.value), props.files.length - 1)
|
||||
return props.files[idx]
|
||||
}
|
||||
return props.file ?? null
|
||||
})
|
||||
|
||||
function resetViewState(clearText = true) {
|
||||
zoom.value = 1
|
||||
rotate.value = 0
|
||||
if (clearText) {
|
||||
textContent.value = null
|
||||
textError.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Watchers ---
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => (visibleLocal.value = v)
|
||||
)
|
||||
watch(visibleLocal, (v) => emit('update:modelValue', v))
|
||||
|
||||
watch(
|
||||
() => props.file,
|
||||
() => {
|
||||
currentIndex.value = 0
|
||||
resetViewState()
|
||||
loadTextIfNeeded()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.files,
|
||||
() => {
|
||||
currentIndex.value = props.startIndex ?? 0
|
||||
resetViewState()
|
||||
loadTextIfNeeded()
|
||||
}
|
||||
)
|
||||
|
||||
watch(currentIndex, () => {
|
||||
resetViewState(false)
|
||||
loadTextIfNeeded()
|
||||
})
|
||||
|
||||
// 监听当前文件变化,确保文件切换时正确更新
|
||||
watch(
|
||||
() => currentFile.value?.url,
|
||||
(newUrl, oldUrl) => {
|
||||
if (newUrl !== oldUrl && newUrl) {
|
||||
resetViewState()
|
||||
loadTextIfNeeded()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const hasNext = computed(
|
||||
() => !!(props.files && currentIndex.value < (props.files?.length ?? 0) - 1)
|
||||
)
|
||||
const hasPrev = computed(() => !!(props.files && currentIndex.value > 0))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function prevFile() {
|
||||
if (hasPrev.value) currentIndex.value -= 1
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function nextFile() {
|
||||
if (hasNext.value) currentIndex.value += 1
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visibleLocal.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// --- Viewer helpers ---
|
||||
function getExtension(name = '') {
|
||||
const m = name.split('.')
|
||||
return (m.length > 1 ? m[m.length - 1].toLowerCase() : '').trim()
|
||||
}
|
||||
|
||||
function mimeFromName(name = '') {
|
||||
const ext = getExtension(name)
|
||||
const map: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
gif: 'image/gif',
|
||||
bmp: 'image/bmp',
|
||||
webp: 'image/webp',
|
||||
txt: 'text/plain',
|
||||
md: 'text/markdown',
|
||||
csv: 'text/csv',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
}
|
||||
return map[ext] ?? ''
|
||||
}
|
||||
|
||||
const fileType = computed(() => {
|
||||
const f = currentFile.value
|
||||
if (!f) return 'unknown'
|
||||
let names = f.name
|
||||
if (names == '') {
|
||||
names = f.file_name
|
||||
}
|
||||
const mime = (f.mime || mimeFromName(names) || '').toLowerCase()
|
||||
const ext = getExtension(names)
|
||||
|
||||
if (mime.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext))
|
||||
return 'image'
|
||||
if (mime === 'application/pdf' || ext === 'pdf') return 'pdf'
|
||||
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) return 'office'
|
||||
if (mime.startsWith('text/') || ['txt', 'md', 'csv', 'log', 'json', 'xml'].includes(ext))
|
||||
return 'text'
|
||||
return 'unknown'
|
||||
})
|
||||
|
||||
function isHttpUrl(u?: string) {
|
||||
return !!u && /^https?:\/\//i.test(u)
|
||||
}
|
||||
|
||||
function officeViewerUrl(url: string) {
|
||||
// 使用微软 Office 在线查看器
|
||||
return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`
|
||||
}
|
||||
|
||||
function appendPdfParams(url: string) {
|
||||
// 某些浏览器可以在 pdf 后加参数调整行为,例如 #toolbar=0
|
||||
return url
|
||||
}
|
||||
|
||||
// --- transform style for viewer ---
|
||||
const viewerStyle = computed(() => {
|
||||
return {
|
||||
transform: `scale(${zoom.value}) rotate(${rotate.value}deg)`,
|
||||
transformOrigin: 'center top'
|
||||
} as Record<string, string>
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function zoomIn() {
|
||||
zoom.value = Math.min(zoom.value + 0.25, 4)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function zoomOut() {
|
||||
zoom.value = Math.max(zoom.value - 0.25, 0.25)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function resetZoom() {
|
||||
zoom.value = 1
|
||||
rotate.value = 0
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function rotateLeft() {
|
||||
rotate.value = (rotate.value - 90) % 360
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function rotateRight() {
|
||||
rotate.value = (rotate.value + 90) % 360
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function toggleFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
|
||||
// --- text loading ---
|
||||
async function loadTextIfNeeded() {
|
||||
if (!currentFile.value) return
|
||||
if (fileType.value !== 'text') return
|
||||
|
||||
textContent.value = null
|
||||
textError.value = null
|
||||
|
||||
try {
|
||||
const resp = await fetch(currentFile.value.url)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const txt = await resp.text()
|
||||
textContent.value = txt
|
||||
} catch (err: any) {
|
||||
textError.value = String(err?.message ?? err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- image handlers ---
|
||||
function onError() {
|
||||
ElMessage.warning('图片加载失败')
|
||||
}
|
||||
function onLoadImage() {
|
||||
// 可以在这里获取自然尺寸进行布局优化
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function downloadCurrent() {
|
||||
if (!currentFile.value) return
|
||||
// 打开下载链接(浏览器通常会下载)
|
||||
const a = document.createElement('a')
|
||||
a.href = currentFile.value.url
|
||||
a.download = currentFile.value.name || ''
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
// expose some controls externally if needed
|
||||
onMounted(() => {
|
||||
if (props.modelValue) visibleLocal.value = true
|
||||
if (fileType.value === 'text') loadTextIfNeeded()
|
||||
})
|
||||
// 导出组件
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-preview-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.file-preview-body {
|
||||
height: calc(80vh - 80px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.viewer-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
}
|
||||
.viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.viewer img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.text-viewer pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.unknown-viewer .unknown {
|
||||
text-align: center;
|
||||
}
|
||||
.no-file {
|
||||
color: #999;
|
||||
}
|
||||
.footer-info {
|
||||
color: #666;
|
||||
}
|
||||
.loading {
|
||||
color: #999;
|
||||
}
|
||||
.error {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,320 @@
|
||||
import { computed } from 'vue'
|
||||
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import mittBus from '@/utils/sys/mittBus'
|
||||
|
||||
// 全局初始化状态
|
||||
let globalInitialized = false
|
||||
|
||||
// 重置全局初始化状态(用于退出登录后重新初始化)
|
||||
export const resetGlobalInitialized = () => {
|
||||
globalInitialized = false
|
||||
// console.log('徽标系统全局初始化状态已重置')
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单徽标管理 Composable
|
||||
* 提供菜单徽标的统一管理接口
|
||||
*/
|
||||
export function useMenuBadge() {
|
||||
const menuBadgeStore = useMenuBadgeStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
// 计算属性:获取菜单列表(带徽标状态)
|
||||
const menuListWithBadges = computed(() => {
|
||||
const menuList = menuStore.menuList
|
||||
if (!menuList.length) return menuList
|
||||
|
||||
// 同步徽标状态到菜单列表
|
||||
menuBadgeStore.syncBadgeWithMenuList(menuList)
|
||||
return menuList
|
||||
})
|
||||
|
||||
// 更新菜单徽标
|
||||
const updateMenuBadge = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
|
||||
menuBadgeStore.updateBadgeData(path, count, type)
|
||||
}
|
||||
|
||||
// 批量更新菜单徽标
|
||||
const batchUpdateMenuBadges = (
|
||||
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
|
||||
) => {
|
||||
menuBadgeStore.batchUpdateBadgeData(updates)
|
||||
}
|
||||
|
||||
// 清除菜单徽标
|
||||
const clearMenuBadge = (path: string) => {
|
||||
menuBadgeStore.clearBadge(path)
|
||||
}
|
||||
|
||||
// 清除所有菜单徽标
|
||||
const clearAllMenuBadges = () => {
|
||||
menuBadgeStore.clearAllBadges()
|
||||
}
|
||||
|
||||
// 获取菜单徽标数量
|
||||
const getMenuBadgeCount = (path: string) => {
|
||||
return menuBadgeStore.getBadgeCount(path)
|
||||
}
|
||||
|
||||
// 检查菜单是否显示徽标
|
||||
const isMenuBadgeVisible = (path: string) => {
|
||||
return menuBadgeStore.shouldShowBadge(path)
|
||||
}
|
||||
|
||||
// 获取菜单文本徽标
|
||||
const getMenuTextBadge = (path: string) => {
|
||||
return menuBadgeStore.shouldShowTextBadge(path)
|
||||
}
|
||||
|
||||
// 处理WebSocket徽标更新消息
|
||||
const handleWebSocketBadgeUpdate = (data: any) => {
|
||||
// 防止重复处理
|
||||
if (data._badgeHandled) {
|
||||
return
|
||||
}
|
||||
data._badgeHandled = true
|
||||
|
||||
// 首先检查是否是数组格式的徽标数据
|
||||
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
|
||||
handleMenuBadgeDataArray(data)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据消息类型处理不同的徽标更新
|
||||
if (data.event === 'menu.badge.update') {
|
||||
const { path, count, type } = data.data
|
||||
if (path && typeof count === 'number') {
|
||||
updateMenuBadge(path, count, type || 'text')
|
||||
}
|
||||
} else if (data.event === 'menu.badge.batch_update') {
|
||||
const { updates } = data.data
|
||||
if (Array.isArray(updates)) {
|
||||
batchUpdateMenuBadges(updates)
|
||||
}
|
||||
} else if (data.event === 'menu.badge.clear') {
|
||||
const { path } = data.data
|
||||
if (path) {
|
||||
clearMenuBadge(path)
|
||||
} else {
|
||||
clearAllMenuBadges()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理WebSocket徽标更新消息时出错:', error, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理菜单徽标数据数组格式
|
||||
const handleMenuBadgeDataArray = (data: any) => {
|
||||
// 防止重复处理
|
||||
if (data._arrayHandled) {
|
||||
return
|
||||
}
|
||||
data._arrayHandled = true
|
||||
|
||||
// 支持多种数据格式:
|
||||
// 1. { event: '...', data: [{ url: '...', badge: 3 }] }
|
||||
// 2. [{ url: '...', badge: 3 }] (直接是数组)
|
||||
let badgeArray: any[] = []
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
// 如果 data 本身就是数组
|
||||
badgeArray = data
|
||||
} else if (Array.isArray(data.data)) {
|
||||
// 如果 data.data 是数组
|
||||
badgeArray = data.data
|
||||
} else {
|
||||
console.warn('未找到数组格式的徽标数据:', data)
|
||||
return
|
||||
}
|
||||
|
||||
if (badgeArray.length > 0) {
|
||||
const updates = badgeArray
|
||||
.filter((item: any) => {
|
||||
const isValid = item.url && typeof item.badge === 'number'
|
||||
if (!isValid) {
|
||||
console.warn('跳过无效的徽标数据项:', item)
|
||||
}
|
||||
return isValid
|
||||
})
|
||||
.map((item: any) => ({
|
||||
path: item.url,
|
||||
count: item.badge,
|
||||
type: 'text' as const
|
||||
}))
|
||||
|
||||
if (updates.length > 0) {
|
||||
batchUpdateMenuBadges(updates)
|
||||
} else {
|
||||
console.warn('没有有效的徽标更新项')
|
||||
}
|
||||
} else {
|
||||
console.warn('徽标数据数组为空')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通知相关的徽标更新
|
||||
const handleNotificationBadgeUpdate = (data: any) => {
|
||||
// console.log('收到通知徽标更新消息:', data)
|
||||
|
||||
// 根据通知类型更新对应菜单的徽标
|
||||
if (data.event === 'notify.receive') {
|
||||
// 通知接收,更新通知菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/notification')
|
||||
updateMenuBadge('/notification', currentCount + 1, 'text')
|
||||
} else if (data.event === 'notify.read') {
|
||||
// 通知已读,减少通知菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/notification')
|
||||
updateMenuBadge('/notification', Math.max(0, currentCount - 1), 'text')
|
||||
} else if (data.event === 'notify.clear') {
|
||||
// 清除所有通知,清除通知菜单徽标
|
||||
clearMenuBadge('/notification')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理审批相关的徽标更新
|
||||
const handleApprovalBadgeUpdate = (data: any) => {
|
||||
// 首先检查是否是数组格式的徽标数据
|
||||
if (Array.isArray(data) || Array.isArray(data.data)) {
|
||||
handleMenuBadgeDataArray(data)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.event === 'approval.new') {
|
||||
// 新审批,更新审批菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/admin/approval')
|
||||
updateMenuBadge('/admin/approval', currentCount + 1, 'text')
|
||||
} else if (data.event === 'approval.processed') {
|
||||
// 审批已处理,减少审批菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/admin/approval')
|
||||
updateMenuBadge('/admin/approval', Math.max(0, currentCount - 1), 'text')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理库存相关的徽标更新
|
||||
const handleStockBadgeUpdate = (data: any) => {
|
||||
// console.log('收到库存徽标更新消息:', data)
|
||||
|
||||
if (data.event === 'stock.low') {
|
||||
// 库存不足,更新库存菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/admin/stock')
|
||||
updateMenuBadge('/admin/stock', currentCount + 1, 'dot')
|
||||
} else if (data.event === 'stock.normal') {
|
||||
// 库存正常,减少库存菜单徽标
|
||||
const currentCount = getMenuBadgeCount('/admin/stock')
|
||||
updateMenuBadge('/admin/stock', Math.max(0, currentCount - 1), 'dot')
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的WebSocket消息处理
|
||||
const handleWebSocketMessage = (data: any) => {
|
||||
if (!data) return
|
||||
|
||||
// 如果已经被特定事件处理过(如 menu-badge-update),则跳过
|
||||
// 注意:_badgeProcessed 标记是在 wsClient.ts 中设置的
|
||||
if (data._badgeProcessed) {
|
||||
return
|
||||
}
|
||||
|
||||
// 首先检查是否是数组格式的徽标数据(无论是否有 event 字段)
|
||||
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
|
||||
handleMenuBadgeDataArray(data)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有 event 字段,无法继续处理
|
||||
if (!data.event) return
|
||||
|
||||
// 处理菜单徽标数据数组格式(如 approval.todo.counts)
|
||||
if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) {
|
||||
handleMenuBadgeDataArray(data)
|
||||
} else if (data.event.includes('menu.badge')) {
|
||||
handleWebSocketBadgeUpdate(data)
|
||||
} else if (data.event.includes('notify')) {
|
||||
handleNotificationBadgeUpdate(data)
|
||||
} else if (data.event.includes('approval')) {
|
||||
handleApprovalBadgeUpdate(data)
|
||||
} else if (data.event.includes('stock')) {
|
||||
handleStockBadgeUpdate(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化徽标系统
|
||||
const initBadgeSystem = () => {
|
||||
if (globalInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
globalInitialized = true
|
||||
|
||||
// 标记页面已加载完成
|
||||
menuBadgeStore.setPageReady(true)
|
||||
|
||||
// 先清理可能存在的旧监听器,避免重复注册
|
||||
mittBus.off('ws:message', handleWebSocketMessage)
|
||||
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
|
||||
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
|
||||
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
|
||||
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
|
||||
mittBus.off('menu:updated')
|
||||
|
||||
// 监听WebSocket消息
|
||||
mittBus.on('ws:message', handleWebSocketMessage)
|
||||
|
||||
// 监听特定的徽标更新事件
|
||||
mittBus.on('menu-badge-update', handleWebSocketBadgeUpdate)
|
||||
mittBus.on('approval-badge-update', handleApprovalBadgeUpdate)
|
||||
mittBus.on('stock-badge-update', handleStockBadgeUpdate)
|
||||
mittBus.on('notification-badge-update', handleNotificationBadgeUpdate)
|
||||
|
||||
// 监听菜单数据变化,同步徽标状态
|
||||
mittBus.on('menu:updated', () => {
|
||||
menuBadgeStore.syncBadgeWithMenuList(menuStore.menuList)
|
||||
})
|
||||
}
|
||||
|
||||
// 清理徽标系统
|
||||
const cleanupBadgeSystem = () => {
|
||||
if (!globalInitialized) {
|
||||
// console.log('菜单徽标系统未全局初始化,跳过清理')
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('清理菜单徽标系统')
|
||||
globalInitialized = false
|
||||
|
||||
// 移除事件监听
|
||||
mittBus.off('ws:message', handleWebSocketMessage)
|
||||
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
|
||||
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
|
||||
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
|
||||
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
|
||||
mittBus.off('menu:updated')
|
||||
// 重置页面加载状态
|
||||
menuBadgeStore.setPageReady(false)
|
||||
}
|
||||
|
||||
// 注意:不在组件中自动初始化,避免重复初始化
|
||||
// 初始化应该在App.vue中全局调用一次
|
||||
|
||||
return {
|
||||
// 计算属性
|
||||
menuListWithBadges,
|
||||
|
||||
// 方法
|
||||
updateMenuBadge,
|
||||
batchUpdateMenuBadges,
|
||||
clearMenuBadge,
|
||||
clearAllMenuBadges,
|
||||
getMenuBadgeCount,
|
||||
isMenuBadgeVisible,
|
||||
getMenuTextBadge,
|
||||
|
||||
// 系统方法
|
||||
initBadgeSystem,
|
||||
cleanupBadgeSystem,
|
||||
handleWebSocketMessage
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export interface OrderItem {
|
||||
model?: string
|
||||
quantity?: number
|
||||
exteriorColor?: string
|
||||
interiorColor?: string
|
||||
modification?: string
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
officialPrice?: number
|
||||
discount?: string
|
||||
}
|
||||
|
||||
export interface PaymentItem {
|
||||
country?: string
|
||||
date?: string
|
||||
currency?: string
|
||||
account?: string
|
||||
remitter?: string
|
||||
amount?: number
|
||||
}
|
||||
|
||||
export interface OrderData {
|
||||
contract_file_url: { name: string; url: string }[]
|
||||
orderNo?: string
|
||||
salesperson?: string
|
||||
piNo?: string
|
||||
date?: string
|
||||
clearanceCountry?: string
|
||||
customerSource?: string
|
||||
customerName?: string
|
||||
customerCountry?: string
|
||||
items?: OrderItem[]
|
||||
contractRate?: string
|
||||
totalCif?: number
|
||||
declarant?: string
|
||||
etaShipDate?: string
|
||||
shippingMethod?: string
|
||||
domesticPort?: string
|
||||
tradeTerms?: string
|
||||
latestArrival?: string
|
||||
payments?: PaymentItem[]
|
||||
}
|
||||
|
||||
export function useOrderDetailDialog() {
|
||||
const visible = ref(false)
|
||||
const title = ref('订单详情')
|
||||
const orderData = ref<OrderData>({
|
||||
contract_file_url: [],
|
||||
items: [],
|
||||
payments: []
|
||||
})
|
||||
|
||||
const show = (data: OrderData, dialogTitle?: string) => {
|
||||
orderData.value = data
|
||||
title.value = dialogTitle || '订单详情'
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const print = () => {
|
||||
// 可以在这里添加打印逻辑
|
||||
window.print()
|
||||
}
|
||||
|
||||
return {
|
||||
visible,
|
||||
title,
|
||||
orderData,
|
||||
show,
|
||||
hide,
|
||||
print
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export const usePdfPreview = () => {
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
url: '',
|
||||
name: '',
|
||||
blobUrl: '',
|
||||
images: [] as string[]
|
||||
})
|
||||
const frameRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
// 动态加载 pdf.js(优先本地依赖,失败回退 CDN)
|
||||
const loadPdfJs = async (): Promise<any> => {
|
||||
// 统一使用 CDN,避免本地依赖缺失导致打包时报错
|
||||
const ensure = (src: string) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const s = document.createElement('script')
|
||||
s.src = src
|
||||
s.onload = () => resolve()
|
||||
s.onerror = () => reject(new Error('load script failed'))
|
||||
document.head.appendChild(s)
|
||||
})
|
||||
await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js')
|
||||
await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js')
|
||||
return (window as any).pdfjsLib
|
||||
}
|
||||
|
||||
const open = (url: string, name = '') => {
|
||||
if (!url) return
|
||||
state.url = url
|
||||
state.name = name
|
||||
state.visible = true
|
||||
state.images = []
|
||||
}
|
||||
|
||||
const openWithResponse = (res: any) => {
|
||||
const url = res?.data?.file_url_full || res?.file_url_full
|
||||
const name = res?.data?.file_url || res?.file_url || ''
|
||||
open(url, name)
|
||||
}
|
||||
|
||||
const inferFilenameFromUrl = (url: string): string => {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
const parts = u.pathname.split('/')
|
||||
return parts[parts.length - 1] || 'document.pdf'
|
||||
} catch (e) {
|
||||
const parts = (url || '').split('?')[0].split('/')
|
||||
return parts[parts.length - 1] || 'document.pdf'
|
||||
}
|
||||
}
|
||||
|
||||
const download = async () => {
|
||||
if (!state.url) return
|
||||
const filename = state.name || inferFilenameFromUrl(state.url)
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const blob = await resp.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
window.open(state.url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 预览时也用 blob,提升兼容性
|
||||
const preparePreview = async () => {
|
||||
if (!state.url) return
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const blob = await resp.blob()
|
||||
state.blobUrl && window.URL.revokeObjectURL(state.blobUrl)
|
||||
state.blobUrl = window.URL.createObjectURL(blob)
|
||||
} catch {
|
||||
state.blobUrl = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图片以便无需浏览器 PDF 插件即可展示
|
||||
const prepareImages = async () => {
|
||||
if (!state.url) return
|
||||
state.images = []
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const arrayBuffer = await resp.arrayBuffer()
|
||||
const pdfjsLib = await loadPdfJs()
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
|
||||
const images: string[] = []
|
||||
for (let p = 1; p <= pdf.numPages; p++) {
|
||||
const page = await pdf.getPage(p)
|
||||
const viewport = page.getViewport({ scale: 1.5 })
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) continue
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
await page.render({ canvasContext: context, viewport }).promise
|
||||
images.push(canvas.toDataURL('image/png'))
|
||||
}
|
||||
state.images = images
|
||||
} catch {
|
||||
state.images = []
|
||||
}
|
||||
}
|
||||
|
||||
const print = () => {
|
||||
const frame = frameRef.value
|
||||
if (frame && frame.contentWindow) {
|
||||
frame.contentWindow.focus()
|
||||
frame.contentWindow.print()
|
||||
return
|
||||
}
|
||||
if (state.url) {
|
||||
const win = window.open(state.url, '_blank')
|
||||
if (win) {
|
||||
win.addEventListener('load', () => win.print())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pdf: state,
|
||||
frameRef,
|
||||
open,
|
||||
openWithResponse,
|
||||
download,
|
||||
print,
|
||||
preparePreview,
|
||||
prepareImages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useUserStore } from '@/store/modules/user'
|
||||
export function useUploadHeaders() {
|
||||
const userStore = useUserStore()
|
||||
const uploadHeaders = computed(() => ({
|
||||
'Fdesk-Language': userStore.language,
|
||||
'Fdesk-Corp-Abbr': userStore.companyabbr,
|
||||
'Fdesk-System-Code': 'fdesk',
|
||||
'Fdesk-Login-Type': userStore.loginType,
|
||||
'Fdesk-Language': userStore.language || 'zh',
|
||||
'Fdesk-Corp-Abbr': userStore.companyabbr || 'default',
|
||||
'Fdesk-System-Code': userStore.systemsabbr || 'fdesk',
|
||||
'Fdesk-Login-Type': userStore.loginType || 'upwd',
|
||||
'Fdesk-Auth-Token': 'Bearer ' + userStore.accessToken
|
||||
}))
|
||||
return { uploadHeaders }
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ const appConfig: SystemConfig = {
|
||||
] as const,
|
||||
// 系统其他项默认配置
|
||||
systemSetting: {
|
||||
defaultMenuWidth: 240, // 菜单宽度
|
||||
defaultMenuWidth: 200, // 菜单宽度
|
||||
defaultCustomRadius: '0.75', // 自定义圆角
|
||||
defaultTabStyle: 'tab-default' // 标签样式
|
||||
}
|
||||
|
||||
@@ -59,27 +59,65 @@ async function handleRouteGuard(
|
||||
// 设置系统主题
|
||||
setSystemTheme(to)
|
||||
|
||||
// 处理登录状态
|
||||
if (!(await handleLoginStatus(to, userStore, next))) {
|
||||
return
|
||||
// 特殊处理电子签名页面 - 优先处理,避免登录检查
|
||||
if (to.path === '/auth/signature/mobile') {
|
||||
// 设置认证信息
|
||||
if (to.query.accessToken || to.query.token) {
|
||||
const token = (to.query.accessToken as string) || (to.query.token as string)
|
||||
if (token) {
|
||||
userStore.setToken(token)
|
||||
userStore.setLoginStatus(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置其他必要的参数到userStore
|
||||
if (to.query.companyid && to.query.companyabbr) {
|
||||
userStore.setCompany(to.query.companyid as string, to.query.companyabbr as string)
|
||||
}
|
||||
|
||||
if (to.query.systemsid && to.query.systemsabbr) {
|
||||
userStore.setSystems(to.query.systemsid as string, to.query.systemsabbr as string)
|
||||
}
|
||||
|
||||
if (to.query.loginType) {
|
||||
userStore.setLoginType(to.query.loginType as 'feishu' | 'upwd')
|
||||
}
|
||||
|
||||
if (to.matched.length > 0) {
|
||||
setWorktab(to)
|
||||
setPageTitle(to)
|
||||
next()
|
||||
return
|
||||
} else {
|
||||
// 如果路由没有匹配,直接跳转到404
|
||||
next(RoutesAlias.Exception404)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理动态路由注册
|
||||
if (!isRouteRegistered.value && userStore.isLogin) {
|
||||
// 处理其他页面的登录状态(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile') {
|
||||
if (!(await handleLoginStatus(to, userStore, next))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理动态路由注册(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && !isRouteRegistered.value && userStore.isLogin) {
|
||||
await handleDynamicRoutes(to, router, next)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理已知的匹配路由
|
||||
if (to.matched.length > 0) {
|
||||
// 处理已知的匹配路由(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && to.matched.length > 0) {
|
||||
setWorktab(to)
|
||||
setPageTitle(to)
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试刷新路由重新注册
|
||||
if (userStore.isLogin) {
|
||||
// 尝试刷新路由重新注册(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && userStore.isLogin) {
|
||||
isRouteRegistered.value = false
|
||||
await handleDynamicRoutes(to, router, next)
|
||||
return
|
||||
@@ -97,6 +135,7 @@ async function handleLoginStatus(
|
||||
userStore: ReturnType<typeof useUserStore>,
|
||||
next: NavigationGuardNext
|
||||
): Promise<boolean> {
|
||||
// 检查其他页面的登录状态
|
||||
if (!userStore.isLogin && to.path !== RoutesAlias.Login && !to.meta.noLogin) {
|
||||
userStore.logOut()
|
||||
next({
|
||||
@@ -105,6 +144,7 @@ async function handleLoginStatus(
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -200,22 +240,17 @@ async function registerAndStoreMenu(
|
||||
*/
|
||||
function handleMenuError(error: unknown): void {
|
||||
console.error('菜单处理失败:', error)
|
||||
|
||||
// 清理所有状态
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const worktabStore = useWorktabStore()
|
||||
|
||||
// 清空菜单和工作台
|
||||
menuStore.clearMenuList()
|
||||
worktabStore.clearAll()
|
||||
|
||||
// 重置路由注册状态
|
||||
isRouteRegistered.value = false
|
||||
|
||||
// 登出用户
|
||||
userStore.logOut()
|
||||
|
||||
throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录')
|
||||
}
|
||||
|
||||
@@ -251,20 +286,21 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean {
|
||||
*/
|
||||
export function resetRouterState(router: Router): void {
|
||||
isRouteRegistered.value = false
|
||||
|
||||
|
||||
// 清理所有动态注册的路由(不仅仅是带有dynamic标记的)
|
||||
const routesToRemove: string[] = []
|
||||
router.getRoutes().forEach((route) => {
|
||||
// 排除静态路由和登录页面
|
||||
if (route.name &&
|
||||
route.name !== 'Login' &&
|
||||
route.name !== 'Exception404' &&
|
||||
route.name !== 'Exception500' &&
|
||||
!route.path.startsWith('/exception/')) {
|
||||
if (
|
||||
route.name &&
|
||||
route.name !== 'Login' &&
|
||||
route.name !== 'Exception404' &&
|
||||
route.name !== 'Exception500' &&
|
||||
!route.path.startsWith('/exception/')
|
||||
) {
|
||||
routesToRemove.push(route.name as string)
|
||||
}
|
||||
})
|
||||
|
||||
// 移除动态路由
|
||||
routesToRemove.forEach((routeName) => {
|
||||
try {
|
||||
@@ -273,11 +309,9 @@ export function resetRouterState(router: Router): void {
|
||||
console.warn(`移除路由失败: ${routeName}`, error)
|
||||
}
|
||||
})
|
||||
|
||||
// 清空菜单数据
|
||||
const menuStore = useMenuStore()
|
||||
menuStore.clearMenuList()
|
||||
|
||||
// 清空工作台标签页
|
||||
const worktabStore = useWorktabStore()
|
||||
worktabStore.clearAll()
|
||||
|
||||
@@ -11,6 +11,22 @@ import { AppRouteRecord } from '@/types/router'
|
||||
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
||||
*/
|
||||
export const asyncRoutes: AppRouteRecord[] = [
|
||||
{
|
||||
path: '/dashboard/console/finance',
|
||||
name: 'Consolefinance',
|
||||
component: RoutesAlias.consolefinance,
|
||||
meta: {
|
||||
title: '仪表盘'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dashboard/console/backoffice',
|
||||
name: 'Backoffice',
|
||||
component: RoutesAlias.backoffice,
|
||||
meta: {
|
||||
title: '仪表盘'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/inquiry_system',
|
||||
name: 'Inquiry_system',
|
||||
@@ -65,6 +81,114 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.recycle,
|
||||
meta: {
|
||||
title: '回收站',
|
||||
isHide: true,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 备货单相关路由
|
||||
{
|
||||
path: '/crmpc/stocking/stocking_list',
|
||||
name: 'StockingList',
|
||||
component: 'crmpc/stocking/stocking_list',
|
||||
meta: {
|
||||
title: '备货单列表',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/Add',
|
||||
name: 'StockingAdd',
|
||||
component: 'crmpc/stocking/Add',
|
||||
meta: {
|
||||
title: '创建备货单',
|
||||
isHide: true,
|
||||
isHideTab: false,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/show',
|
||||
name: 'StockingShow',
|
||||
component: 'crmpc/stocking/show',
|
||||
meta: {
|
||||
title: '备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/final_add',
|
||||
name: 'StockingFinalAdd',
|
||||
component: 'crmpc/stocking/final_add',
|
||||
meta: {
|
||||
title: '最终备货单',
|
||||
isHide: true,
|
||||
isHideTab: false,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/final_show',
|
||||
name: 'StockingFinalShow',
|
||||
component: 'crmpc/stocking/final_show',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
//审批详情
|
||||
{
|
||||
path: '/crmpc/stocking/stocking_auth_detil',
|
||||
name: 'StockingAuth',
|
||||
component: 'crmpc/stocking/stocking_auth_detil',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
// 审批流程添加
|
||||
{
|
||||
path: '/admin/approval/approval_add',
|
||||
name: 'approvalAdd',
|
||||
component: 'admin/approval/approval_add',
|
||||
meta: {
|
||||
title: '审批流程添加',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/approval/auth_detil',
|
||||
name: 'approvalDetil',
|
||||
component: 'crmpc/approval/auth_detil',
|
||||
meta: {
|
||||
title: '审核详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
// 管理端备货单相关路由
|
||||
{
|
||||
path: '/admin/stock/stocking_list_admin',
|
||||
name: 'AdminStockingList',
|
||||
component: 'admin/stock/stocking_list_admin',
|
||||
meta: {
|
||||
title: '备货单列表',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/stock/show_admin',
|
||||
name: 'AdminStockingShow',
|
||||
component: 'admin/stock/show_admin',
|
||||
meta: {
|
||||
title: '备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/stock/final_show_admin',
|
||||
name: 'AdminStockingFinalShow',
|
||||
component: 'admin/stock/final_show_admin',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Home from '@views/index/index.vue'
|
||||
export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: HOME_PAGE
|
||||
redirect: localStorage.getItem('home_page') || HOME_PAGE
|
||||
},
|
||||
{
|
||||
path: '/console',
|
||||
@@ -41,6 +41,12 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
component: () => import('@/views/auth/feishu-callback.vue'),
|
||||
meta: { title: '飞书回调', noLogin: true, isHideTab: true }
|
||||
},
|
||||
{
|
||||
path: '/auth/signature/mobile',
|
||||
name: 'SignatureMobile',
|
||||
component: () => import('@views/auth/signature/mobile.vue'),
|
||||
meta: { title: '电子签名', isHideTab: true, noLogin: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: '/exception',
|
||||
component: Home,
|
||||
|
||||
@@ -62,7 +62,15 @@ export enum RoutesAlias {
|
||||
UserCenter = '/system/user-center', // 用户中心
|
||||
inquirySystem = '/admin/inquiry_system', // 询单系统
|
||||
customer = '/admin/customer', // 管理端客户列表
|
||||
customeraudit = '/admin/customer/customeraudit' // 管理端待审客户列表
|
||||
customeraudit = '/admin/customer/customeraudit', // 管理端待审客户列表
|
||||
// 备货单相关路由
|
||||
StockingList = '/crmpc/stocking/stocking_list', // 备货单列表
|
||||
StockingAdd = '/crmpc/stocking/Add', // 创建备货单
|
||||
StockingShow = '/crmpc/stocking/show', // 备货单详情
|
||||
StockingFinalAdd = '/crmpc/stocking/final_add', // 最终备货单添加
|
||||
StockingFinalShow = '/crmpc/stocking/final_show', // 最终备货单详情
|
||||
consolefinance = '/dashboard/console/finance', //财务仪表盘
|
||||
backoffice = '/dashboard/console/backoffice' // 内勤仪表盘
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
@@ -130,15 +130,21 @@ function loadComponent(componentPath: string, routeName: string): () => Promise<
|
||||
}
|
||||
|
||||
// 构建可能的路径
|
||||
const fullPath = `../../views${componentPath}.vue`
|
||||
const fullPathWithIndex = `../../views${componentPath}/index.vue`
|
||||
const fullPathNoSlash = `../../views${componentPath}.vue`
|
||||
const fullPathWithIndexNoSlash = `../../views${componentPath}/index.vue`
|
||||
const fullPathWithSlash = `../../views/${componentPath}.vue`
|
||||
const fullPathWithIndexSlash = `../../views/${componentPath}/index.vue`
|
||||
|
||||
// 先尝试直接路径,再尝试添加/index的路径
|
||||
const module = modules[fullPath] || modules[fullPathWithIndex]
|
||||
// 依次尝试四种可能路径(兼容是否带斜杠的情况)
|
||||
const module =
|
||||
modules[fullPathNoSlash] ||
|
||||
modules[fullPathWithIndexNoSlash] ||
|
||||
modules[fullPathWithSlash] ||
|
||||
modules[fullPathWithIndexSlash]
|
||||
|
||||
if (!module) {
|
||||
console.error(
|
||||
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}`
|
||||
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPathNoSlash}、${fullPathWithIndexNoSlash}、${fullPathWithSlash} 和 ${fullPathWithIndexSlash}`
|
||||
)
|
||||
return () =>
|
||||
Promise.resolve({
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
// 菜单徽标数据类型
|
||||
export interface MenuBadgeData {
|
||||
/** 菜单路径 */
|
||||
path: string
|
||||
/** 徽标数量 */
|
||||
count: number
|
||||
/** 徽标类型:dot(红点) | text(数字) */
|
||||
type: 'dot' | 'text'
|
||||
/** 最后更新时间 */
|
||||
lastUpdated: number
|
||||
}
|
||||
|
||||
// 菜单徽标Store
|
||||
export const useMenuBadgeStore = defineStore('menuBadgeStore', () => {
|
||||
// 徽标数据映射 - 使用普通对象而不是Map,避免持久化问题
|
||||
const badgeData = ref<Record<string, MenuBadgeData>>({})
|
||||
|
||||
// 页面是否已加载完成
|
||||
const isPageReady = ref(false)
|
||||
|
||||
// 待处理的徽标更新队列(页面未加载完成时使用)
|
||||
const pendingUpdates = ref<Array<{ path: string; count: number; type: 'dot' | 'text' }>>([])
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
return userStore.info?.userId?.toString() || 'anonymous'
|
||||
} catch {
|
||||
return 'anonymous'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户特定的存储key
|
||||
const getUserStorageKey = (): string => {
|
||||
const userId = getCurrentUserId()
|
||||
return `menu-badge-${userId}`
|
||||
}
|
||||
|
||||
// 从localStorage加载用户特定的徽标数据
|
||||
const loadUserBadgeData = () => {
|
||||
try {
|
||||
const storageKey = getUserStorageKey()
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
badgeData.value = parsed.badgeData || {}
|
||||
// 注意:不恢复 isPageReady,因为页面状态应该在运行时由代码控制
|
||||
// isPageReady.value 应该在 setPageReady() 中设置,而不是从存储中恢复
|
||||
pendingUpdates.value = parsed.pendingUpdates || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载用户徽标数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户特定的徽标数据到localStorage
|
||||
const saveUserBadgeData = () => {
|
||||
try {
|
||||
const storageKey = getUserStorageKey()
|
||||
const data = {
|
||||
badgeData: badgeData.value,
|
||||
// 注意:不保存 isPageReady,因为页面状态应该在运行时由代码控制
|
||||
// 页面每次加载时都应该重新设置为 true
|
||||
pendingUpdates: pendingUpdates.value
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.warn('保存用户徽标数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理其他用户的徽标数据
|
||||
const cleanupOtherUserData = () => {
|
||||
try {
|
||||
const currentUserId = getCurrentUserId()
|
||||
const keys = Object.keys(localStorage)
|
||||
keys.forEach((key) => {
|
||||
if (key.startsWith('menu-badge-') && !key.endsWith(`-${currentUserId}`)) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('清理其他用户数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性:获取所有徽标数据
|
||||
const allBadgeData = computed(() => {
|
||||
return { ...badgeData.value }
|
||||
})
|
||||
|
||||
// 获取指定菜单的徽标数据
|
||||
const getBadgeData = (path: string): MenuBadgeData | null => {
|
||||
return badgeData.value[path] || null
|
||||
}
|
||||
|
||||
// 获取指定菜单的徽标数量
|
||||
const getBadgeCount = (path: string): number => {
|
||||
const data = badgeData.value[path]
|
||||
return data?.count || 0
|
||||
}
|
||||
|
||||
// 检查菜单是否应该显示徽标
|
||||
const shouldShowBadge = (path: string): boolean => {
|
||||
const data = badgeData.value[path]
|
||||
return Boolean(data && data.count > 0)
|
||||
}
|
||||
|
||||
// 检查菜单是否应该显示文本徽标
|
||||
const shouldShowTextBadge = (path: string): string | null => {
|
||||
const data = badgeData.value[path]
|
||||
if (data && data.count > 0 && data.type === 'text') {
|
||||
return data.count > 99 ? '99+' : data.count.toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 设置页面加载状态
|
||||
const setPageReady = (ready: boolean) => {
|
||||
isPageReady.value = ready
|
||||
|
||||
// 页面加载完成时,加载用户特定的徽标数据
|
||||
if (ready) {
|
||||
loadUserBadgeData()
|
||||
// 确保 isPageReady 保持为 true(防止被其他逻辑覆盖)
|
||||
isPageReady.value = true
|
||||
// 处理待处理的更新
|
||||
if (pendingUpdates.value.length > 0) {
|
||||
pendingUpdates.value.forEach((update) => {
|
||||
updateBadgeData(update.path, update.count, update.type)
|
||||
})
|
||||
pendingUpdates.value = []
|
||||
// 再次确认页面状态
|
||||
isPageReady.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新徽标数据
|
||||
const updateBadgeData = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
|
||||
// 如果页面未加载完成,将更新加入队列
|
||||
if (!isPageReady.value) {
|
||||
// console.log('页面未加载完成,将徽标更新加入队列:', { path, count, type })
|
||||
// 检查是否已存在相同路径的待处理更新,避免重复
|
||||
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === path)
|
||||
if (existingIndex >= 0) {
|
||||
pendingUpdates.value[existingIndex] = { path, count, type }
|
||||
} else {
|
||||
pendingUpdates.value.push({ path, count, type })
|
||||
}
|
||||
console.log('检查菜单是否应该显示徽标:', path, badgeData.value)
|
||||
// 保存到用户特定存储
|
||||
saveUserBadgeData()
|
||||
return
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
// 如果数量为0或负数,移除徽标
|
||||
delete badgeData.value[path]
|
||||
} else {
|
||||
// 更新或创建徽标数据
|
||||
badgeData.value[path] = {
|
||||
path,
|
||||
count,
|
||||
type,
|
||||
lastUpdated: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到用户特定存储
|
||||
saveUserBadgeData()
|
||||
}
|
||||
|
||||
// 批量更新徽标数据
|
||||
const batchUpdateBadgeData = (
|
||||
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
|
||||
) => {
|
||||
if (!isPageReady.value) {
|
||||
updates.forEach((update) => {
|
||||
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === update.path)
|
||||
if (existingIndex >= 0) {
|
||||
pendingUpdates.value[existingIndex] = {
|
||||
path: update.path,
|
||||
count: update.count,
|
||||
type: update.type || 'text'
|
||||
}
|
||||
} else {
|
||||
pendingUpdates.value.push({
|
||||
path: update.path,
|
||||
count: update.count,
|
||||
type: update.type || 'text'
|
||||
})
|
||||
}
|
||||
})
|
||||
// 保存到用户特定存储
|
||||
saveUserBadgeData()
|
||||
return
|
||||
}
|
||||
updates.forEach((update) => {
|
||||
updateBadgeData(update.path, update.count, update.type || 'text')
|
||||
})
|
||||
// 批量更新后保存
|
||||
saveUserBadgeData()
|
||||
}
|
||||
|
||||
// 清除指定菜单的徽标
|
||||
const clearBadge = (path: string) => {
|
||||
updateBadgeData(path, 0)
|
||||
}
|
||||
|
||||
// 清除所有徽标
|
||||
const clearAllBadges = () => {
|
||||
badgeData.value = {}
|
||||
pendingUpdates.value = []
|
||||
// 保存到用户特定存储
|
||||
saveUserBadgeData()
|
||||
}
|
||||
|
||||
// 从菜单列表同步徽标状态
|
||||
const syncBadgeWithMenuList = (menuList: AppRouteRecord[]) => {
|
||||
if (!isPageReady.value) {
|
||||
// console.log('页面未加载完成,跳过菜单徽标同步')
|
||||
return
|
||||
}
|
||||
|
||||
const updateMenuBadge = (menus: AppRouteRecord[]) => {
|
||||
menus.forEach((menu) => {
|
||||
const badgeData = getBadgeData(menu.path)
|
||||
if (badgeData) {
|
||||
// 更新菜单的徽标状态
|
||||
menu.meta.showBadge = badgeData.type === 'dot' && badgeData.count > 0
|
||||
menu.meta.showTextBadge =
|
||||
badgeData.type === 'text' && badgeData.count > 0
|
||||
? badgeData.count > 99
|
||||
? '99+'
|
||||
: badgeData.count.toString()
|
||||
: undefined
|
||||
} else {
|
||||
// 清除菜单的徽标状态
|
||||
menu.meta.showBadge = false
|
||||
menu.meta.showTextBadge = undefined
|
||||
}
|
||||
|
||||
// 递归处理子菜单
|
||||
if (menu.children) {
|
||||
updateMenuBadge(menu.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateMenuBadge(menuList)
|
||||
}
|
||||
|
||||
// 获取待处理更新的数量
|
||||
const getPendingUpdatesCount = () => pendingUpdates.value.length
|
||||
|
||||
// 获取待处理的更新列表
|
||||
const getPendingUpdates = () => [...pendingUpdates.value]
|
||||
|
||||
return {
|
||||
// 状态
|
||||
badgeData,
|
||||
isPageReady,
|
||||
pendingUpdates,
|
||||
|
||||
// 计算属性
|
||||
allBadgeData,
|
||||
|
||||
// 方法
|
||||
getBadgeData,
|
||||
getBadgeCount,
|
||||
shouldShowBadge,
|
||||
shouldShowTextBadge,
|
||||
setPageReady,
|
||||
updateBadgeData,
|
||||
batchUpdateBadgeData,
|
||||
clearBadge,
|
||||
clearAllBadges,
|
||||
syncBadgeWithMenuList,
|
||||
getPendingUpdatesCount,
|
||||
getPendingUpdates,
|
||||
|
||||
// 用户隔离相关方法
|
||||
loadUserBadgeData,
|
||||
saveUserBadgeData,
|
||||
cleanupOtherUserData,
|
||||
getCurrentUserId
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,212 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { LanguageEnum } from '@/enums/appEnum'
|
||||
import { router } from '@/router'
|
||||
import { useSettingStore } from './setting'
|
||||
import { useWorktabStore } from './worktab'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { setPageTitle } from '@/router/utils/utils'
|
||||
import { resetRouterState } from '@/router/guards/beforeEach'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { UserService } from '@/api/usersApi' // 新增导入
|
||||
let refreshTimeout: ReturnType<typeof setTimeout> | null = null // 定时器变量
|
||||
|
||||
// 用户
|
||||
export const useUserStore = defineStore(
|
||||
'userStore',
|
||||
() => {
|
||||
const language = ref(LanguageEnum.ZH)
|
||||
const isLogin = ref(false)
|
||||
const isLock = ref(false)
|
||||
const lockPassword = ref('')
|
||||
const info = ref<Partial<Api.User.UserInfo>>({})
|
||||
const searchHistory = ref<AppRouteRecord[]>([])
|
||||
const accessToken = ref('')
|
||||
const refreshToken = ref('')
|
||||
const companyid = ref('')
|
||||
const companyabbr = ref('')
|
||||
const systemsid = ref('')
|
||||
const systemsabbr = ref('')
|
||||
const home_page = ref('')
|
||||
const loginType = ref<'feishu' | 'upwd'>('upwd')
|
||||
const tokenExpireAt = ref<number>(0)
|
||||
|
||||
const getUserInfo = computed(() => info.value)
|
||||
const getSettingState = computed(() => useSettingStore().$state)
|
||||
const getWorktabState = computed(() => useWorktabStore().$state)
|
||||
|
||||
const setUserInfo = (newInfo: Api.User.UserInfo) => {
|
||||
info.value = newInfo
|
||||
}
|
||||
|
||||
const setLoginStatus = (status: boolean) => {
|
||||
isLogin.value = status
|
||||
}
|
||||
|
||||
const setLanguage = (lang: LanguageEnum) => {
|
||||
setPageTitle(router.currentRoute.value)
|
||||
language.value = lang
|
||||
}
|
||||
const setLoginType = (type: 'feishu' | 'upwd') => {
|
||||
loginType.value = type
|
||||
}
|
||||
const setSearchHistory = (list: AppRouteRecord[]) => {
|
||||
searchHistory.value = list
|
||||
}
|
||||
|
||||
const setLockStatus = (status: boolean) => {
|
||||
isLock.value = status
|
||||
}
|
||||
|
||||
const setLockPassword = (password: string) => {
|
||||
lockPassword.value = password
|
||||
}
|
||||
|
||||
const setToken = (newAccessToken: string, newRefreshToken?: string, expiresIn?: number) => {
|
||||
accessToken.value = newAccessToken
|
||||
if (newRefreshToken) {
|
||||
refreshToken.value = newRefreshToken
|
||||
}
|
||||
if (expiresIn) {
|
||||
const expireAt = Date.now() + expiresIn * 1000
|
||||
tokenExpireAt.value = expireAt
|
||||
localStorage.setItem('token_expire_at', String(expireAt))
|
||||
// 检查是否需要重新设置定时器
|
||||
const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||
if (currentLeftSeconds > 0) {
|
||||
scheduleTokenRefresh(currentLeftSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
const setCompany = (id: string, abbr: string) => {
|
||||
companyid.value = id
|
||||
companyabbr.value = abbr
|
||||
}
|
||||
const setSystems = (id: string, abbr: string) => {
|
||||
systemsid.value = id
|
||||
systemsabbr.value = abbr
|
||||
}
|
||||
// 设置首页
|
||||
const sethome_page = (page: string) => {
|
||||
home_page.value = page
|
||||
}
|
||||
function restoreTokenTimer() {
|
||||
const expireAt = Number(localStorage.getItem('token_expire_at') || '0')
|
||||
if (accessToken.value && expireAt > Date.now()) {
|
||||
const leftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||
scheduleTokenRefresh(leftSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
let isScheduled = false
|
||||
|
||||
function scheduleTokenRefresh(expiresIn: number) {
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout)
|
||||
refreshTimeout = null
|
||||
}
|
||||
|
||||
// 提前1分钟刷新
|
||||
const refreshTime = (expiresIn - 60) * 1000
|
||||
|
||||
if (refreshTime > 0) {
|
||||
if (!isScheduled) {
|
||||
isScheduled = true
|
||||
refreshTimeout = setTimeout(() => {
|
||||
refreshTokenAction().finally(() => {
|
||||
isScheduled = false // 重置标志位
|
||||
})
|
||||
}, refreshTime)
|
||||
} else {
|
||||
console.log('scheduleTokenRefresh is already scheduled, skipping...')
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid refreshTime:', refreshTime)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTokenAction() {
|
||||
try {
|
||||
const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value })
|
||||
// 动态计算剩余时间
|
||||
const currentTime = Date.now()
|
||||
const expireAt = currentTime + data.expires_in * 1000
|
||||
const leftSeconds = Math.floor((expireAt - currentTime) / 1000)
|
||||
setToken(data.access_token, data.refresh_token, leftSeconds)
|
||||
} catch (e) {
|
||||
console.error('Token refresh failed:', e)
|
||||
logOut()
|
||||
}
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
// 清空所有本地缓存
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
systemsabbr.value = ''
|
||||
systemsid.value = ''
|
||||
companyabbr.value = ''
|
||||
companyid.value = ''
|
||||
home_page.value = ''
|
||||
info.value = {}
|
||||
isLogin.value = false
|
||||
isLock.value = false
|
||||
lockPassword.value = ''
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器
|
||||
// 清理工作台标签页
|
||||
useWorktabStore().clearAll()
|
||||
// 清理sessionStorage中的用户信息缓存
|
||||
sessionStorage.removeItem('lastUserInfo')
|
||||
sessionStorage.removeItem('iframeRoutes')
|
||||
// 重置路由状态
|
||||
resetRouterState(router)
|
||||
// 跳转到登录页
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
isLogin,
|
||||
isLock,
|
||||
lockPassword,
|
||||
info,
|
||||
companyid,
|
||||
companyabbr,
|
||||
systemsid,
|
||||
systemsabbr,
|
||||
home_page,
|
||||
searchHistory,
|
||||
loginType,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpireAt,
|
||||
getUserInfo,
|
||||
getSettingState,
|
||||
getWorktabState,
|
||||
setUserInfo,
|
||||
setLoginStatus,
|
||||
setLanguage,
|
||||
setSearchHistory,
|
||||
setLockStatus,
|
||||
setLockPassword,
|
||||
setToken,
|
||||
setCompany,
|
||||
setSystems,
|
||||
setLoginType,
|
||||
sethome_page,
|
||||
logOut,
|
||||
restoreTokenTimer
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'user',
|
||||
storage: localStorage,
|
||||
afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => {
|
||||
ctx.store.restoreTokenTimer()
|
||||
}
|
||||
} as any
|
||||
}
|
||||
)
|
||||
@@ -140,6 +140,20 @@ export const useUserStore = defineStore(
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
// 清理徽标系统
|
||||
import('@/composables/useMenuBadge').then(({ useMenuBadge, resetGlobalInitialized }) => {
|
||||
const { cleanupBadgeSystem, clearAllMenuBadges } = useMenuBadge()
|
||||
clearAllMenuBadges()
|
||||
cleanupBadgeSystem()
|
||||
resetGlobalInitialized()
|
||||
})
|
||||
|
||||
// 清理徽标store中的其他用户数据
|
||||
import('@/store/modules/menuBadge').then(({ useMenuBadgeStore }) => {
|
||||
const menuBadgeStore = useMenuBadgeStore()
|
||||
menuBadgeStore.cleanupOtherUserData()
|
||||
})
|
||||
|
||||
// 清空所有本地缓存
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
|
||||
@@ -9,7 +9,7 @@ declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||
|
||||
@@ -7,6 +7,8 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
'副本': typeof import('./../components/core/layouts/art-page-content/index - 副本.vue')['default']
|
||||
ApprovalTimeline: typeof import('./../components/custom/ApprovalTimeline.vue')['default']
|
||||
ArtBackToTop: typeof import('./../components/core/base/art-back-to-top/index.vue')['default']
|
||||
ArtBarChart: typeof import('./../components/core/charts/ArtBarChart.vue')['default']
|
||||
ArtBarChartCard: typeof import('./../components/core/cards/art-bar-chart-card/index.vue')['default']
|
||||
@@ -72,12 +74,16 @@ declare module 'vue' {
|
||||
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||
copy: typeof import('./../components/custom/ProcurementDetailView copy.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
@@ -93,8 +99,11 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElLoading: typeof import('element-plus/es')['ElLoading']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
@@ -117,16 +126,27 @@ declare module 'vue' {
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
||||
FilePreview: typeof import('./../components/custom/FilePreview.vue')['default']
|
||||
HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default']
|
||||
'Index - 副本': typeof import('./../components/core/layouts/art-page-content/index - 副本.vue')['default']
|
||||
InternalStaffView: typeof import('./../components/custom/InternalStaffView.vue')['default']
|
||||
InternalStaffViews: typeof import('./../components/custom/InternalStaffViews.vue')['default']
|
||||
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
|
||||
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
|
||||
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
|
||||
MultiFileUpload: typeof import('./../components/core/forms/MultiFileUpload.vue')['default']
|
||||
OrderDetailContent: typeof import('./../components/custom/OrderDetailContent.vue')['default']
|
||||
PrintPreview: typeof import('./../components/custom/PrintPreview.vue')['default']
|
||||
ProcurementDetailView: typeof import('./../components/custom/ProcurementDetailView.vue')['default']
|
||||
'ProcurementDetailView copy': typeof import('./../components/custom/ProcurementDetailView copy.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
||||
@@ -135,6 +155,7 @@ declare module 'vue' {
|
||||
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
||||
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
||||
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
||||
WorkChatDrawer: typeof import('./../components/custom/chat/WorkChatDrawer.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -50,7 +50,7 @@ axiosInstance.interceptors.request.use(
|
||||
const { systemsabbr } = useUserStore()
|
||||
request.headers.set('Fdesk-Language', language)
|
||||
request.headers.set('Fdesk-Corp-Abbr', companyabbr)
|
||||
request.headers.set('Fdesk-System-Code', systemsabbr)
|
||||
request.headers.set('Fdesk-System-Code', 'fdesk')
|
||||
request.headers.set('Fdesk-Login-Type', loginType)
|
||||
// 设置 token 和 请求头
|
||||
if (accessToken) {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 菜单徽标管理工具
|
||||
* 用于管理菜单徽标功能
|
||||
*/
|
||||
|
||||
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
|
||||
|
||||
/**
|
||||
* 菜单徽标管理类
|
||||
*/
|
||||
export class MenuBadgeTest {
|
||||
private menuBadgeStore = useMenuBadgeStore()
|
||||
|
||||
/**
|
||||
* 直接更新徽标
|
||||
*/
|
||||
updateBadge(path: string, count: number, type: 'dot' | 'text' = 'text') {
|
||||
// console.log(`直接更新徽标: ${path} = ${count} (${type})`)
|
||||
this.menuBadgeStore.updateBadgeData(path, count, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新徽标
|
||||
*/
|
||||
batchUpdateBadges(updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>) {
|
||||
// console.log('批量更新徽标:', updates)
|
||||
this.menuBadgeStore.batchUpdateBadgeData(updates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除徽标
|
||||
*/
|
||||
clearBadge(path: string) {
|
||||
// console.log(`清除徽标: ${path}`)
|
||||
this.menuBadgeStore.clearBadge(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有徽标
|
||||
*/
|
||||
clearAllBadges() {
|
||||
// console.log('清除所有徽标')
|
||||
this.menuBadgeStore.clearAllBadges()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取徽标状态
|
||||
*/
|
||||
getBadgeStatus(path: string) {
|
||||
const data = this.menuBadgeStore.getBadgeData(path)
|
||||
// console.log(`徽标状态 ${path}:`, data)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有徽标状态
|
||||
*/
|
||||
getAllBadgeStatus() {
|
||||
const allData = this.menuBadgeStore.allBadgeData
|
||||
// console.log('所有徽标状态:', allData)
|
||||
return allData
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待处理更新数量
|
||||
*/
|
||||
getPendingUpdatesCount() {
|
||||
const count = this.menuBadgeStore.getPendingUpdatesCount()
|
||||
// console.log(`待处理更新数量: ${count}`)
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面加载状态
|
||||
*/
|
||||
setPageReady(ready: boolean) {
|
||||
// console.log(`设置页面加载状态: ${ready}`)
|
||||
this.menuBadgeStore.setPageReady(ready)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实际菜单路径
|
||||
*/
|
||||
checkActualMenuPaths() {
|
||||
// console.log('检查实际菜单路径...')
|
||||
|
||||
try {
|
||||
// 获取菜单store
|
||||
import('@/store/modules/menu')
|
||||
.then(({ useMenuStore }) => {
|
||||
const menuStore = useMenuStore()
|
||||
this.printMenuPaths(menuStore.menuList)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('动态导入菜单store失败:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('检查菜单路径失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private printMenuPaths(menuList: any[]) {
|
||||
// console.log('当前菜单列表:', menuList)
|
||||
|
||||
// 递归打印所有菜单路径
|
||||
const printMenuPaths = (menus: any[], level = 0) => {
|
||||
menus.forEach((menu) => {
|
||||
// const indent = ' '.repeat(level)
|
||||
// console.log(`${indent}${menu.path} - ${menu.meta?.title}`)
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
printMenuPaths(menu.children, level + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (menuList && menuList.length > 0) {
|
||||
printMenuPaths(menuList)
|
||||
} else {
|
||||
console.log('菜单列表为空,可能还未加载完成')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行基础功能测试
|
||||
*/
|
||||
runBasicTest() {
|
||||
// console.log('开始运行基础功能测试...')
|
||||
|
||||
// 1. 测试页面未加载完成时的更新
|
||||
// console.log('\n=== 测试页面未加载完成时的更新 ===')
|
||||
this.setPageReady(false)
|
||||
this.updateBadge('/admin/approval', 3, 'text')
|
||||
this.updateBadge('/admin/stock', 1, 'dot')
|
||||
|
||||
// 2. 设置页面加载完成
|
||||
// console.log('\n=== 设置页面加载完成 ===')
|
||||
this.setPageReady(true)
|
||||
|
||||
// 3. 测试批量更新
|
||||
// console.log('\n=== 测试批量更新 ===')
|
||||
this.batchUpdateBadges([
|
||||
{ path: '/admin/approval', count: 3, type: 'text' },
|
||||
{ path: '/admin/stock', count: 1, type: 'dot' }
|
||||
])
|
||||
|
||||
// 4. 显示最终状态
|
||||
// console.log('\n=== 最终状态 ===')
|
||||
this.getAllBadgeStatus()
|
||||
|
||||
// console.log('\n基础功能测试完成!')
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查WebSocket连接状态
|
||||
*/
|
||||
checkWebSocketStatus() {
|
||||
// console.log('检查WebSocket连接状态...')
|
||||
|
||||
// 检查全局WebSocket状态函数
|
||||
if (typeof window !== 'undefined' && (window as any).getWebSocketStatus) {
|
||||
const status = (window as any).getWebSocketStatus()
|
||||
console.log('WebSocket状态:', status)
|
||||
} else {
|
||||
console.log('WebSocket状态函数不可用')
|
||||
}
|
||||
|
||||
// 检查菜单徽标系统初始化状态
|
||||
const pendingCount = this.menuBadgeStore.getPendingUpdatesCount()
|
||||
console.log('待处理的徽标更新数量:', pendingCount)
|
||||
|
||||
if (pendingCount > 0) {
|
||||
console.log('待处理的更新:', this.menuBadgeStore.getPendingUpdates())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局测试实例
|
||||
const menuBadgeTest = new MenuBadgeTest()
|
||||
|
||||
// 将测试工具挂载到全局对象,方便在控制台使用
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-expect-error - 挂载到全局对象用于调试
|
||||
window.menuBadgeTest = menuBadgeTest
|
||||
}
|
||||
|
||||
export default menuBadgeTest
|
||||
@@ -98,10 +98,10 @@ class StorageCompatibilityManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在登录页面
|
||||
* 检查是否在登录页面或电子签名页面
|
||||
*/
|
||||
private isOnLoginPage(): boolean {
|
||||
return location.href.includes(RoutesAlias.Login)
|
||||
return location.href.includes(RoutesAlias.Login) || location.href.includes('/auth/signature/mobile')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import mittBus from './mittBus'
|
||||
import mittBus from './mittBus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { ElNotification, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
type JsonValue = Record<string, any> | any[]
|
||||
|
||||
@@ -18,6 +20,7 @@ let isConnecting = false
|
||||
let lastInitToken: string | null = null
|
||||
let lastInitAt = 0
|
||||
const INIT_DEBOUNCE_MS = 1200
|
||||
let isAlertShowing = false
|
||||
|
||||
// 生成唯一ID
|
||||
const generateTraceId = (): string => {
|
||||
@@ -95,9 +98,144 @@ const handleMessage = (event: MessageEvent) => {
|
||||
try {
|
||||
data = JSON.parse(event.data)
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
// console.warn('WebSocket消息解析失败:', error, '原始数据:', event.data)
|
||||
// 对于无法解析的消息,仍然尝试处理原始数据
|
||||
data = { event: 'unknown', data: event.data }
|
||||
}
|
||||
console.log('WebSocket message:', data)
|
||||
// console.log('WebSocket message:', data)
|
||||
// 判断系统初始化检查
|
||||
if (data.event == 'system.init.check') {
|
||||
const { systemsid } = useUserStore()
|
||||
|
||||
// 检查页面是否已加载完成
|
||||
if (document.readyState !== 'complete') {
|
||||
console.warn('页面未完全加载,延迟处理系统检查消息')
|
||||
// 延迟处理,等待页面加载完成
|
||||
setTimeout(() => {
|
||||
handleMessage(event)
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 router 是否可用
|
||||
let router: any = null
|
||||
let currentPath = ''
|
||||
let isOnMobileSignature = false
|
||||
let isOnUserInfo = false
|
||||
let isOnUserCenter = false
|
||||
|
||||
try {
|
||||
router = useRouter()
|
||||
// 检查 router.currentRoute 是否可用
|
||||
if (router.currentRoute) {
|
||||
currentPath = router.currentRoute.value?.path || ''
|
||||
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
|
||||
isOnUserInfo = currentPath.startsWith('/system/user-center')
|
||||
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
|
||||
} else {
|
||||
// Router currentRoute 未初始化,使用 window.location 作为备选方案
|
||||
currentPath = window.location.pathname || ''
|
||||
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
|
||||
isOnUserInfo = currentPath.startsWith('/system/user-center')
|
||||
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
|
||||
// console.warn('Router currentRoute 未初始化,使用 window.location 作为备选方案')
|
||||
}
|
||||
} catch {
|
||||
// Router 完全未初始化,使用 window.location 作为备选方案
|
||||
currentPath = window.location.pathname || ''
|
||||
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
|
||||
isOnUserInfo = currentPath.startsWith('/system/user-center')
|
||||
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
|
||||
// console.warn('Router 未初始化,使用 window.location 作为备选方案:', error)
|
||||
}
|
||||
|
||||
// 排除移动端电子签名页,不弹出提示框
|
||||
if (isOnMobileSignature || isOnUserInfo || isOnUserCenter) {
|
||||
return
|
||||
}
|
||||
|
||||
// 接收到消息就弹出提示框(排除移动端电子签名页)
|
||||
if (data.data) {
|
||||
const { is_sign } = data.data
|
||||
const msgs = ref('尚未设置【签名】,请前往个人中心完成操作!')
|
||||
|
||||
// 根据检查结果确定提示信息
|
||||
let alertMessage = ''
|
||||
let shouldShowAlert = false
|
||||
|
||||
// 构建状态标识
|
||||
// const statusKey = `${is_report_to}-${is_office_clerk}-${is_sign}`
|
||||
|
||||
// switch (statusKey) {
|
||||
// case 'false-false-false':
|
||||
// alertMessage = msgs.value
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'true-false-true':
|
||||
// alertMessage = '尚未操作【绑定直属上级、签名】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'true-false-false':
|
||||
// alertMessage = '尚未操作【绑定内勤、签名】】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'true-true-false':
|
||||
// alertMessage = '尚未操作【签名】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'false-true-true':
|
||||
// alertMessage = '尚未操作【直属上级】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'false-true-false':
|
||||
// alertMessage = '尚未操作【直属上级,签名】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// case 'false-false-true':
|
||||
// alertMessage = '尚未操作【直属上级,内勤】,请前往个人中心完成操作!'
|
||||
// shouldShowAlert = true
|
||||
// break
|
||||
// default:
|
||||
// // 其他情况不弹出提示
|
||||
// shouldShowAlert = false
|
||||
// break
|
||||
// }
|
||||
if (is_sign == false) {
|
||||
alertMessage = msgs.value
|
||||
shouldShowAlert = true
|
||||
}
|
||||
|
||||
// 只有在需要显示提示且当前没有弹出框的情况下才弹出提示框
|
||||
if (shouldShowAlert && !isAlertShowing) {
|
||||
isAlertShowing = true
|
||||
ElMessageBox.alert(alertMessage, '提醒', {
|
||||
confirmButtonText: '确定',
|
||||
callback: () => {
|
||||
isAlertShowing = false
|
||||
// 如果 router 可用,使用 router 跳转
|
||||
if (router && router.push) {
|
||||
if (systemsid == '1') {
|
||||
router.push('/admin/user/user_info')
|
||||
} else {
|
||||
router.push('/system/user-center')
|
||||
}
|
||||
} else {
|
||||
// Router 不可用,使用 window.location 跳转
|
||||
const targetPath = systemsid == '1' ? '/admin/user/user_info' : '/system/user-center'
|
||||
window.location.href = targetPath
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// 如果弹出框被意外关闭,重置状态
|
||||
isAlertShowing = false
|
||||
})
|
||||
} else if (shouldShowAlert && isAlertShowing) {
|
||||
// 已有弹窗显示,避免重复弹出
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通知消息
|
||||
if (data.event == 'notify.receive') {
|
||||
ElNotification({
|
||||
title: data.data.notice_data.message.title || '通知',
|
||||
@@ -106,22 +244,97 @@ const handleMessage = (event: MessageEvent) => {
|
||||
})
|
||||
mittBus.emit('notification-data-changed', 1)
|
||||
}
|
||||
|
||||
// 处理菜单徽标更新消息
|
||||
if (data.event && data.event.includes('menu.badge')) {
|
||||
// console.log('收到菜单徽标更新消息:', data)
|
||||
// 直接触发徽标更新事件
|
||||
mittBus.emit('menu-badge-update', data)
|
||||
}
|
||||
|
||||
// 处理审批相关消息
|
||||
if (data.event && data.event.includes('approval')) {
|
||||
// 检查是否是数组格式的徽标数据或待办数量消息
|
||||
if (
|
||||
Array.isArray(data.data) ||
|
||||
Array.isArray(data) ||
|
||||
data.event.includes('.todo.counts') ||
|
||||
data.event.includes('.badge.counts')
|
||||
) {
|
||||
// 数组格式的徽标数据或待办数量消息,触发菜单徽标更新事件
|
||||
mittBus.emit('menu-badge-update', data)
|
||||
// 标记已处理,避免在后续通用处理中重复处理
|
||||
data._badgeProcessed = true
|
||||
} else {
|
||||
// 其他审批消息触发审批徽标更新事件
|
||||
mittBus.emit('approval-badge-update', data)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理库存相关消息
|
||||
if (data.event && data.event.includes('stock')) {
|
||||
// console.log('收到库存相关消息:', data)
|
||||
// 触发库存徽标更新事件
|
||||
mittBus.emit('stock-badge-update', data)
|
||||
}
|
||||
|
||||
// 处理通知相关消息
|
||||
if (data.event && data.event.includes('notify')) {
|
||||
// console.log('收到通知相关消息:', data)
|
||||
// 触发通知徽标更新事件
|
||||
mittBus.emit('notification-badge-update', data)
|
||||
}
|
||||
|
||||
// 处理其他可能的徽标相关消息
|
||||
// 检查是否是数组格式的徽标数据(无论是否有 event 字段)
|
||||
// 如果已经被处理过(如审批消息),则跳过
|
||||
if (!data._badgeProcessed) {
|
||||
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
|
||||
mittBus.emit('menu-badge-update', data)
|
||||
} else if (data.event && (data.event.includes('badge') || data.event.includes('count'))) {
|
||||
// 尝试作为通用徽标更新处理
|
||||
mittBus.emit('menu-badge-update', data)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送原始消息到事件总线
|
||||
mittBus.emit('ws:message', data)
|
||||
|
||||
// 检查WebSocket连接状态,如果连接异常则记录
|
||||
if (socket && socket.readyState !== WebSocket.OPEN) {
|
||||
// console.warn('WebSocket连接状态异常,当前状态:', socket.readyState, '消息可能丢失')
|
||||
}
|
||||
}
|
||||
|
||||
// 设置WebSocket事件处理器
|
||||
const setupWebSocketHandlers = (ws: WebSocket) => {
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接已建立')
|
||||
isInitialConnection = false
|
||||
isConnecting = false
|
||||
startHeartbeat()
|
||||
mittBus.emit('ws:open')
|
||||
sendPing()
|
||||
// 连接建立成功后推送一个连接确认消息
|
||||
const connectMessage = {
|
||||
event: 'system.connect',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
message: 'WebSocket连接已建立'
|
||||
},
|
||||
trace_id: generateTraceId()
|
||||
}
|
||||
try {
|
||||
ws.send(JSON.stringify(connectMessage))
|
||||
} catch (error) {
|
||||
console.error('发送连接确认消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = handleMessage
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('WebSocket连接错误:', err)
|
||||
mittBus.emit('ws:error', err)
|
||||
isConnecting = false
|
||||
try {
|
||||
@@ -134,6 +347,7 @@ const setupWebSocketHandlers = (ws: WebSocket) => {
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket连接已关闭')
|
||||
mittBus.emit('ws:close')
|
||||
isConnecting = false
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
@@ -155,7 +369,6 @@ const scheduleReconnect = () => {
|
||||
|
||||
cleanup()
|
||||
const delay = getReconnectDelay()
|
||||
console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
const { accessToken } = useUserStore()
|
||||
if (accessToken && !isConnecting) {
|
||||
@@ -187,32 +400,50 @@ const scheduleReconnect = () => {
|
||||
// 初始化WebSocket连接
|
||||
export const initWebSocket = (token: string) => {
|
||||
try {
|
||||
if (!token) return
|
||||
if (!token) {
|
||||
console.log('WebSocket初始化失败:没有token')
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
// 简单防抖:在短时间内重复触发则跳过
|
||||
if (now - lastInitAt < INIT_DEBOUNCE_MS) {
|
||||
console.log('WebSocket初始化被防抖跳过')
|
||||
return
|
||||
}
|
||||
// 避免并发初始化:已在连接中或已连接且 token 未变化则跳过
|
||||
if (isConnecting) return
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
if (isConnecting) {
|
||||
console.log('WebSocket正在连接中,跳过重复初始化')
|
||||
return
|
||||
}
|
||||
if (
|
||||
socket &&
|
||||
(socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
if (lastInitToken === token) {
|
||||
console.log('WebSocket已连接且token未变化,跳过初始化')
|
||||
return
|
||||
}
|
||||
// token 变更:需要切换连接
|
||||
try { socket.close() } catch {}
|
||||
console.log('WebSocket token变更,关闭旧连接')
|
||||
try {
|
||||
socket.close()
|
||||
} catch {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
socket = null
|
||||
}
|
||||
|
||||
cleanup()
|
||||
|
||||
const url = `${WS_BASE}${token}`
|
||||
console.log('正在初始化WebSocket连接:', url)
|
||||
isConnecting = true
|
||||
lastInitToken = token
|
||||
lastInitAt = now
|
||||
socket = new WebSocket(url)
|
||||
setupWebSocketHandlers(socket)
|
||||
} catch (e) {
|
||||
console.error('WebSocket初始化失败:', e)
|
||||
mittBus.emit('ws:error', e)
|
||||
isConnecting = false
|
||||
scheduleReconnect()
|
||||
@@ -270,4 +501,4 @@ export const getWebSocketStatus = () => ({
|
||||
isInitialConnection,
|
||||
isConnecting,
|
||||
lastInitToken
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,810 @@
|
||||
<template>
|
||||
<div class="borrow-approval-page">
|
||||
<div class="borrow-toolbar boxshadow">
|
||||
<div class="toolbar-right">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="选择状态"
|
||||
clearable
|
||||
style="width: 150px; margin-right: 12px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="handleSearch"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="申请单号" prop="order_no" min-width="160" />
|
||||
<ElTableColumn label="申请人" prop="applicant_name" min-width="120" />
|
||||
<ElTableColumn label="借用物品" prop="item" min-width="200">
|
||||
<template #default="scope">
|
||||
<span>{{ getItemNames(scope.row.item) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
label="借用原因"
|
||||
prop="borrow_reason"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="使用开始日期" prop="use_start_date" min-width="120" />
|
||||
<ElTableColumn label="使用结束日期" prop="use_end_date" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status" min-width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleViewDetail(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
v-if="hasAuth('borrow_approval@show')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="canApprove(scope.row) && hasAuth('borrow_approval@approve')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleApprove(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="借用申请详情"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="detailData.id" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>基本信息</span>
|
||||
</div>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请单号</label>
|
||||
<span>{{ detailData.order_no }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请人</label>
|
||||
<span>{{ detailData.applicant_name }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>使用开始日期</label>
|
||||
<span>{{ detailData.use_start_date }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>使用结束日期</label>
|
||||
<span>{{ detailData.use_end_date }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="24">
|
||||
<div class="info-item">
|
||||
<label>借用原因</label>
|
||||
<span>{{ detailData.borrow_reason }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row" v-if="detailData.remark">
|
||||
<ElCol :span="24">
|
||||
<div class="info-item">
|
||||
<label>备注</label>
|
||||
<span>{{ detailData.remark }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 借用物品 -->
|
||||
<div class="info-section" v-if="detailData.item && detailData.item.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>借用物品</span>
|
||||
</div>
|
||||
<div class="item-list">
|
||||
<div
|
||||
v-for="(item, index) in detailData.item"
|
||||
:key="index"
|
||||
class="item-card"
|
||||
>
|
||||
<div class="item-row">
|
||||
<span class="item-label">物品名称</span>
|
||||
<span class="item-value">{{ item.item_name_snap }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">申请数量</span>
|
||||
<span class="item-value">{{ item.request_qty }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附件 -->
|
||||
<div class="info-section" v-if="detailData.attachment && detailData.attachment.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>附件</span>
|
||||
</div>
|
||||
<div class="image-attachment-list">
|
||||
<div
|
||||
v-for="(file, index) in detailData.attachment"
|
||||
:key="index"
|
||||
class="image-attachment-item"
|
||||
>
|
||||
<el-image
|
||||
:src="file.file_path_pull"
|
||||
:preview-src-list="getImagePreviewUrls(detailData.attachment)"
|
||||
:initial-index="Number(index)"
|
||||
fit="cover"
|
||||
class="attachment-image"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="image-name">{{ file.file_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批记录 -->
|
||||
<div class="info-section" v-if="detailData.approve && detailData.approve.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>审批记录</span>
|
||||
</div>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(record, index) in detailData.approve"
|
||||
:key="index"
|
||||
:timestamp="record.action_at != 0 ? record.action_at : ''"
|
||||
placement="top"
|
||||
>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-header">
|
||||
<span class="approver">{{ record.approver_name }}</span>
|
||||
<ElTag :type="getActionType(record.action)" size="small">
|
||||
{{ getActionLabel(record.action) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div v-if="record.comment" class="timeline-comment">
|
||||
{{ record.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
title="借用申请审批"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<div v-if="approvalDetail.id" class="approval-content">
|
||||
<!-- 申请详情展示 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>申请信息</span>
|
||||
</div>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请单号</label>
|
||||
<span>{{ approvalDetail.order_no }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请人</label>
|
||||
<span>{{ approvalDetail.applicant_name }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>使用开始日期</label>
|
||||
<span>{{ approvalDetail.use_start_date }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>使用结束日期</label>
|
||||
<span>{{ approvalDetail.use_end_date }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="24">
|
||||
<div class="info-item">
|
||||
<label>借用原因</label>
|
||||
<span>{{ approvalDetail.borrow_reason }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 借用物品 -->
|
||||
<div class="info-section" v-if="approvalDetail.item && approvalDetail.item.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>借用物品</span>
|
||||
</div>
|
||||
<div class="item-list">
|
||||
<div v-for="(item, index) in approvalDetail.item" :key="index" class="item-card">
|
||||
<div class="item-row">
|
||||
<span class="item-label">物品名称</span>
|
||||
<span class="item-value">{{ item.item_name_snap }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">申请数量</span>
|
||||
<span class="item-value">{{ item.request_qty }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>审批意见</span>
|
||||
</div>
|
||||
<ElInput
|
||||
v-model="approvalForm.comment"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入审批意见"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 上传附件 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>附件(可选)</span>
|
||||
</div>
|
||||
<el-upload
|
||||
v-model:file-list="approvalFileList"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-remove="handleUploadRemove"
|
||||
:limit="5"
|
||||
multiple
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持图片、PDF等格式,最多上传5个文件</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton
|
||||
type="danger"
|
||||
plain
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleSubmitApprove('reject')"
|
||||
>
|
||||
拒绝
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleSubmitApprove('approve')"
|
||||
>
|
||||
同意
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadUserFile, UploadProps } from 'element-plus'
|
||||
import { invitemService } from '@/api/admin/Itemrequisition'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
|
||||
defineOptions({ name: 'BorrowApprovalPage' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
|
||||
// 上传地址
|
||||
const uploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=borrow_approval`
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '待审核', value: 10 },
|
||||
{ label: '审批中', value: 11 },
|
||||
{ label: '已审批(待归还)', value: 12 },
|
||||
{ label: '拒绝', value: 13 }
|
||||
]
|
||||
|
||||
// 获取状态标签颜色
|
||||
const getStatusType = (status: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
|
||||
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
|
||||
10: 'warning', // 待审核
|
||||
11: 'primary', // 审批中
|
||||
12: 'success', // 已审批(待归还)
|
||||
13: 'danger', // 拒绝
|
||||
14: 'info', // 申请归还
|
||||
15: 'primary', // 归还审核中
|
||||
16: 'success', // 部分归还
|
||||
17: 'success', // 已全部归还
|
||||
18: 'danger' // 已拒绝
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusLabel = (status: number) => {
|
||||
const labelMap: Record<number, string> = {
|
||||
10: '待审核',
|
||||
11: '审批中',
|
||||
12: '已审批(待归还)',
|
||||
13: '拒绝',
|
||||
14: '申请归还',
|
||||
15: '归还审核中',
|
||||
16: '部分归还',
|
||||
17: '已全部归还',
|
||||
18: '已拒绝'
|
||||
}
|
||||
return labelMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 判断是否可以审批
|
||||
const canApprove = (row: any) => {
|
||||
return row.status === 10 || row.status === 11 || row.status === 14 || row.status === 15
|
||||
}
|
||||
|
||||
// 获取审批动作标签颜色
|
||||
const getActionType = (action: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
|
||||
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
|
||||
0: 'info', // 待审核
|
||||
1: 'success', // 同意
|
||||
2: 'danger' // 拒绝
|
||||
}
|
||||
return typeMap[action] || 'info'
|
||||
}
|
||||
|
||||
// 获取审批动作文本
|
||||
const getActionLabel = (action: number) => {
|
||||
const labelMap: Record<number, string> = {
|
||||
0: '待审核',
|
||||
1: '同意',
|
||||
2: '拒绝'
|
||||
}
|
||||
return labelMap[action] || '未知'
|
||||
}
|
||||
|
||||
// 获取物品名称列表
|
||||
const getItemNames = (items: any[]) => {
|
||||
if (!items || items.length === 0) return '-'
|
||||
return items.map((item) => item.item_name_snap).join('、')
|
||||
}
|
||||
|
||||
// 获取图片预览URL列表
|
||||
const getImagePreviewUrls = (attachments: any[]) => {
|
||||
if (!attachments || attachments.length === 0) return []
|
||||
return attachments.map((item) => item.file_path_pull)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 详情弹窗数据
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
|
||||
// 审批弹窗数据
|
||||
const approvalDialogVisible = ref(false)
|
||||
const currentRowId = ref<number | null>(null)
|
||||
const approvalDetail = ref<any>({})
|
||||
const approvalForm = reactive({
|
||||
comment: '',
|
||||
attachment: [] as any[]
|
||||
})
|
||||
const approvalFileList = ref<UploadUserFile[]>([])
|
||||
|
||||
// 上传成功处理
|
||||
const handleUploadSuccess: UploadProps['onSuccess'] = (response) => {
|
||||
if (response.code === 200) {
|
||||
approvalForm.attachment.push({
|
||||
original_name: response.data.original_name || '文件',
|
||||
file_path: response.data.file_path || response.data.file_url,
|
||||
file_url: response.data.file_url
|
||||
})
|
||||
ElMessage.success('文件上传成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件处理
|
||||
const handleUploadRemove: UploadProps['onRemove'] = (file) => {
|
||||
const index = approvalForm.attachment.findIndex(
|
||||
(item) => item.file_url === file.url || item.original_name === file.name
|
||||
)
|
||||
if (index > -1) {
|
||||
approvalForm.attachment.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
// 获取列表数据
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
}
|
||||
if (searchForm.keyword) {
|
||||
params.keyword = searchForm.keyword
|
||||
}
|
||||
if (searchForm.status !== undefined) {
|
||||
params.status = searchForm.status
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await invitemService.getBorrowList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (row: any) => {
|
||||
try {
|
||||
const detail = await invitemService.getBorrowDetail({ id: row.id })
|
||||
detailData.value = detail
|
||||
detailDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开审批弹窗 - 获取完整详情
|
||||
const handleApprove = async (row: any) => {
|
||||
try {
|
||||
const detail = await invitemService.getBorrowDetail({ id: row.id })
|
||||
currentRowId.value = row.id
|
||||
approvalDetail.value = detail
|
||||
approvalForm.comment = ''
|
||||
approvalForm.attachment = []
|
||||
approvalFileList.value = []
|
||||
approvalDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交审批
|
||||
const handleSubmitApprove = async (action: 'approve' | 'reject') => {
|
||||
if (!approvalForm.comment) {
|
||||
ElMessage.warning('请输入审批意见')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { message } = await invitemService.borrowApproval({
|
||||
id: currentRowId.value!,
|
||||
action,
|
||||
comment: approvalForm.comment,
|
||||
attachment: approvalForm.attachment.map((item) => ({
|
||||
original_name: item.original_name,
|
||||
file_path: item.file_path
|
||||
}))
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalForm.comment = ''
|
||||
approvalForm.attachment = []
|
||||
currentRowId.value = null
|
||||
approvalDetail.value = {}
|
||||
approvalFileList.value = []
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.borrow-approval-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.borrow-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 详情和审批弹窗样式
|
||||
.detail-content,
|
||||
.approval-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin: 20px 0;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
// 物品列表
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片附件列表
|
||||
.image-attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.image-attachment-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.attachment-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.image-error {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.image-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 审批记录时间轴
|
||||
.timeline-content {
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.approver {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-comment {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗底部按钮
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
|
||||
.submit-btn {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,929 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ElRow>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton @click="showDialog('add')" v-if="hasAuth('item_list@add')" v-ripple
|
||||
>新增</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="ID" prop="id" width="80" />
|
||||
<ElTableColumn label="公司" prop="company_name" />
|
||||
<ElTableColumn label="名称" prop="item_name" />
|
||||
<ElTableColumn label="物品管理员" prop="manage_user_name" />
|
||||
<ElTableColumn label="备注" prop="remark" />
|
||||
<ElTableColumn label="库存数量" prop="stock_qty" />
|
||||
<ElTableColumn label="扫码领取" prop="allow_qr_claim">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.allow_qr_claim ? 'primary' : 'info'">
|
||||
{{ scope.row.allow_qr_claim ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="领取限制数量" prop="qr_code_num">
|
||||
<template #default="scope">
|
||||
{{ scope.row.qr_code_num === 0 ? '不限制' : scope.row.qr_code_num }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="是否耗材" prop="is_consumable" width="180">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.is_consumable ? 'primary' : 'info'">
|
||||
{{ scope.row.is_consumable ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<ElButton link type="primary" @click="showDialog('edit', scope.row)" v-if="hasAuth('item_list@edit')">
|
||||
编辑
|
||||
</ElButton>
|
||||
<ElDropdown trigger="click">
|
||||
<ElButton link type="primary">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="generateQrCode(scope.row)">
|
||||
<el-icon><Document /></el-icon>
|
||||
{{ scope.row.qr_code_pull ? '查看二维码' : '生成二维码' }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="showProxyClaimDialog(scope.row)">
|
||||
<el-icon><User /></el-icon>
|
||||
管理员代领
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="showStockLogDialog(scope.row)">
|
||||
<el-icon><List /></el-icon>
|
||||
库存记录
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="showQrClaimsDialog(scope.row)">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
领取记录
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem v-if="scope.row.qr_code_pull" @click="deleteQrCode(scope.row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除二维码
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem divided @click="deleteRole(scope.row)" v-if="hasAuth('item_list@del')">
|
||||
<el-icon style="color: #f56c6c"><Delete /></el-icon>
|
||||
<span style="color: #f56c6c">删除物品</span>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" align-center>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
style="padding: 5px 20px"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="公司" prop="company_id">
|
||||
<ElSelect
|
||||
v-model="form.company_id"
|
||||
placeholder="请选择公司"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in companyList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="名称" prop="item_name">
|
||||
<ElInput v-model="form.item_name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="物品管理员" prop="manage_user_id">
|
||||
<ElSelect
|
||||
v-model="form.manage_user_id"
|
||||
placeholder="请选择负责人"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in manageUserList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="规格型号" prop="spec_model">
|
||||
<ElInput v-model="form.spec_model" placeholder="请输入规格型号" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="计量单位" prop="unit">
|
||||
<ElInput v-model="form.unit" placeholder="请输入计量单位" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="是否耗材" prop="is_consumable">
|
||||
<el-radio-group v-model="form.is_consumable">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="form.remark" placeholder="请输入备注" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="库存数量" prop="stock_qty">
|
||||
<ElInputNumber
|
||||
v-model="form.stock_qty"
|
||||
:min="0"
|
||||
placeholder="请输入库存数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="扫码领取" prop="allow_qr_claim">
|
||||
<el-radio-group v-model="form.allow_qr_claim">
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="form.allow_qr_claim === 1" label="单人领取限制" prop="qr_code_num">
|
||||
<ElInputNumber
|
||||
v-model="form.qr_code_num"
|
||||
:min="0"
|
||||
placeholder="0为不限制"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary); font-size: 12px">
|
||||
单人扫码领取数量限制,0为不限制
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 二维码弹窗 -->
|
||||
<ElDialog v-model="qrCodeDialogVisible" title="物品二维码" width="400px" align-center>
|
||||
<div class="qr-code-container">
|
||||
<div v-if="qrCodeData" class="qr-code-wrapper">
|
||||
<img :src="qrCodeData" alt="二维码" class="qr-code-image" />
|
||||
<p class="qr-code-desc">扫描二维码领取物品</p>
|
||||
</div>
|
||||
<div v-else-if="qrCodeLoading" class="qr-code-loading">
|
||||
<el-icon class="is-loading" :size="40">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>生成中...</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="closeQrCodeDialog">{{ qrCodeData ? '关闭' : '取消' }}</ElButton>
|
||||
<ElButton v-if="qrCodeData" type="primary" @click="downloadQrCode">下载二维码</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 管理员代领弹窗 -->
|
||||
<ElDialog v-model="proxyClaimDialogVisible" title="管理员代领" width="500px" align-center>
|
||||
<ElForm ref="proxyClaimFormRef" :model="proxyClaimForm" label-width="100px">
|
||||
<ElFormItem label="选择用户" prop="target_user_ids">
|
||||
<ElSelect
|
||||
v-model="proxyClaimForm.target_user_ids"
|
||||
placeholder="请选择用户"
|
||||
filterable
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption v-for="item in userList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="proxyClaimDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleProxyClaimSubmit">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 库存记录弹窗 -->
|
||||
<ElDialog v-model="stockLogDialogVisible" title="库存变化记录" width="80%" align-center>
|
||||
<!-- 搜索表单 -->
|
||||
<ElForm :model="stockLogSearchForm" inline style="margin-bottom: 16px">
|
||||
<ElFormItem label="变动类型">
|
||||
<ElSelect
|
||||
v-model="stockLogSearchForm.change_type"
|
||||
placeholder="请选择变动类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<ElOption label="入库" :value="1" />
|
||||
<ElOption label="出库(领用)" :value="2" />
|
||||
<ElOption label="出库(损耗)" :value="3" />
|
||||
<ElOption label="入库(归还)" :value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="searchStockLog">查询</ElButton>
|
||||
<ElButton @click="resetStockLogSearch">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<ElTable :data="stockLogList" :loading="stockLogLoading" max-height="400">
|
||||
<ElTableColumn label="物品名称" prop="item_name" />
|
||||
<ElTableColumn label="变化类型" prop="change_type_text" />
|
||||
<ElTableColumn label="变化数量" prop="change_qty">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.change_qty > 0 ? '#67C23A' : '#F56C6C' }">
|
||||
{{ scope.row.change_qty > 0 ? '+' : '' }}{{ scope.row.change_qty }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="变化前" prop="before_qty"/>
|
||||
<ElTableColumn label="变化后" prop="after_qty" />
|
||||
<ElTableColumn label="业务类型" prop="biz_type_text" />
|
||||
<ElTableColumn label="操作人" prop="operator_name" />
|
||||
<ElTableColumn label="备注" prop="remark" min-width="120" />
|
||||
<ElTableColumn label="创建时间" prop="created_at"/>
|
||||
</ElTable>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
|
||||
<ElPagination
|
||||
v-model:current-page="stockLogPagination.currentPage"
|
||||
v-model:page-size="stockLogPagination.pageSize"
|
||||
:total="stockLogPagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleStockLogSizeChange"
|
||||
@current-change="handleStockLogCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="stockLogDialogVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 领取记录弹窗 -->
|
||||
<ElDialog v-model="qrClaimsDialogVisible" title="领取记录" width="80%" align-center>
|
||||
<!-- 搜索表单 -->
|
||||
<ElForm :model="qrClaimsSearchForm" inline style="margin-bottom: 16px">
|
||||
<ElFormItem label="领取方式">
|
||||
<ElSelect
|
||||
v-model="qrClaimsSearchForm.claim_type"
|
||||
placeholder="请选择领取方式"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<ElOption label="扫码领取" :value="2" />
|
||||
<ElOption label="管理员代领" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开始时间">
|
||||
<ElDatePicker
|
||||
v-model="qrClaimsSearchForm.start_time"
|
||||
type="datetime"
|
||||
placeholder="请选择开始时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="结束时间">
|
||||
<ElDatePicker
|
||||
v-model="qrClaimsSearchForm.end_time"
|
||||
type="datetime"
|
||||
placeholder="请选择结束时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="searchQrClaims">查询</ElButton>
|
||||
<ElButton @click="resetQrClaimsSearch">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<ElTable :data="qrClaimsList" :loading="qrClaimsLoading" max-height="400">
|
||||
<ElTableColumn label="物品">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.items && scope.row.items.length > 0">
|
||||
<template v-for="(item, index) in scope.row.items" :key="index">
|
||||
{{ item.item_name_snap }}<span v-if="Number(index) < scope.row.items.length - 1">, </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="数量">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.items && scope.row.items.length > 0">
|
||||
<template v-for="(item, index) in scope.row.items" :key="index">
|
||||
{{ item.approved_qty }}<span v-if="Number(index) < scope.row.items.length - 1">, </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="领取人" prop="applicant_name" />
|
||||
<ElTableColumn label="领取类型" prop="claim_type_text" />
|
||||
<ElTableColumn label="领取时间" prop="created_at" />
|
||||
<ElTableColumn label="代领人" prop="proxy_claim_user_name">
|
||||
<template #default="scope">
|
||||
{{ scope.row.proxy_claim_user_name || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="领取方式" prop="borrow_reason" />
|
||||
<ElTableColumn label="备注" prop="remark" />
|
||||
</ElTable>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
|
||||
<ElPagination
|
||||
v-model:current-page="qrClaimsPagination.currentPage"
|
||||
v-model:page-size="qrClaimsPagination.pageSize"
|
||||
:total="qrClaimsPagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleQrClaimsSizeChange"
|
||||
@current-change="handleQrClaimsCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="qrClaimsDialogVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Loading, ArrowDown, Document, User, List, Tickets, Delete } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { invitemService } from '@/api/admin/Itemrequisition'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'ItemList' })
|
||||
const keyword = ref<string>('')
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const qrCodeDialogVisible = ref(false)
|
||||
const qrCodeData = ref('')
|
||||
const qrCodeLoading = ref(false)
|
||||
const proxyClaimDialogVisible = ref(false)
|
||||
const stockLogDialogVisible = ref(false)
|
||||
const stockLogList = ref<any[]>([])
|
||||
const stockLogLoading = ref(false)
|
||||
const currentStockLogItemId = ref<number | null>(null)
|
||||
const stockLogSearchForm = reactive({
|
||||
change_type: undefined as number | undefined
|
||||
})
|
||||
const stockLogPagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const qrClaimsDialogVisible = ref(false)
|
||||
const qrClaimsList = ref<any[]>([])
|
||||
const qrClaimsLoading = ref(false)
|
||||
const currentQrClaimsItemId = ref<number | null>(null)
|
||||
const qrClaimsSearchForm = reactive({
|
||||
claim_type: undefined as number | undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
})
|
||||
const qrClaimsPagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const proxyClaimFormRef = ref<FormInstance>()
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = reactive<FormRules>({
|
||||
company_id: [{ required: true, message: '请选择公司', trigger: 'change' }],
|
||||
item_name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
manage_user_id: [{ required: true, message: '请选择负责人', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
company_id: '',
|
||||
manage_user_id: '',
|
||||
remark: '',
|
||||
item_name: '',
|
||||
stock_qty: 0,
|
||||
allow_qr_claim: 0,
|
||||
is_consumable: 0,
|
||||
qr_code_num: 0,
|
||||
status: 1,
|
||||
id: ''
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const companyList = ref<any[]>([])
|
||||
const getcompanyList = async () => {
|
||||
const response = await UserService.getCompanyList()
|
||||
const list = (response as any)?.data ?? (response as any)?.records ?? response
|
||||
companyList.value = Array.isArray(list) ? list : []
|
||||
}
|
||||
const manageUserList = ref<any>()
|
||||
// 获取用户列表
|
||||
const getCompanyUserList = async () => {
|
||||
const response = await manageService.presetusers()
|
||||
manageUserList.value = response || []
|
||||
}
|
||||
|
||||
// 管理员代领相关
|
||||
const proxyClaimForm = reactive({
|
||||
item_id: '',
|
||||
target_user_ids: [] as number[]
|
||||
})
|
||||
const userList = ref<any[]>([])
|
||||
// 获取所有用户列表
|
||||
const getAllUserList = async () => {
|
||||
try {
|
||||
const response = await manageService.presetusers()
|
||||
userList.value = response || []
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
getcompanyList()
|
||||
getCompanyUserList()
|
||||
getAllUserList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getWorkList()
|
||||
})
|
||||
// 获取列表数据
|
||||
const getWorkList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
}
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
const { records, current, size, total } = await invitemService.getinvitemlist(params)
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getWorkList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getWorkList()
|
||||
}
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式,填充所有字段
|
||||
Object.assign(form, {
|
||||
company_id: row.company_id,
|
||||
item_name: row.item_name,
|
||||
manage_user_id: row.manage_user_id,
|
||||
remark: row.remark,
|
||||
stock_qty: row.stock_qty ?? 0,
|
||||
is_consumable: row.is_consumable ?? 0,
|
||||
allow_qr_claim: row.allow_qr_claim ?? 0,
|
||||
qr_code_num: row.qr_code_num ?? 0,
|
||||
status: row.status,
|
||||
id: row.id
|
||||
})
|
||||
} else {
|
||||
// 新增模式,重置所有字段
|
||||
Object.assign(form, {
|
||||
company_id: '',
|
||||
item_name: '',
|
||||
manage_user_id: '',
|
||||
remark: '',
|
||||
stock_qty: 0,
|
||||
is_consumable: 0,
|
||||
allow_qr_claim: 0,
|
||||
qr_code_num: 0,
|
||||
status: 1,
|
||||
id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await invitemService.invitemdelete({
|
||||
id: row.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
// 删除二维码
|
||||
const deleteQrCode = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该物品的二维码吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const { message } = await invitemService.deleteQrCode({
|
||||
id: row.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await invitemService.invitemadd({
|
||||
company_id: form.company_id,
|
||||
item_name: form.item_name,
|
||||
manage_user_id: form.manage_user_id,
|
||||
remark: form.remark,
|
||||
stock_qty: form.stock_qty,
|
||||
is_consumable: form.is_consumable,
|
||||
allow_qr_claim: form.allow_qr_claim,
|
||||
qr_code_num: form.qr_code_num || 0,
|
||||
status: form.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await invitemService.invitemupdate({
|
||||
id: form.id,
|
||||
company_id: form.company_id,
|
||||
item_name: form.item_name,
|
||||
manage_user_id: form.manage_user_id,
|
||||
remark: form.remark,
|
||||
stock_qty: form.stock_qty,
|
||||
is_consumable: form.is_consumable,
|
||||
allow_qr_claim: form.allow_qr_claim,
|
||||
qr_code_num: form.qr_code_num || 0,
|
||||
status: form.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成/查看二维码
|
||||
const generateQrCode = async (row: any) => {
|
||||
qrCodeDialogVisible.value = true
|
||||
|
||||
// 如果已有二维码URL,直接使用
|
||||
if (row.qr_code_pull) {
|
||||
qrCodeData.value = row.qr_code_pull
|
||||
qrCodeLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 没有二维码URL,调用接口生成
|
||||
qrCodeLoading.value = true
|
||||
qrCodeData.value = ''
|
||||
|
||||
try {
|
||||
const response = await invitemService.getQrCode({ id: row.id })
|
||||
qrCodeData.value = (response as any)?.data?.qr_code_url || ''
|
||||
ElMessage.success('二维码生成成功')
|
||||
// 刷新列表以更新 qr_code_pull
|
||||
getWorkList()
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
ElMessage.error('生成二维码失败')
|
||||
} finally {
|
||||
qrCodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭二维码弹窗
|
||||
const closeQrCodeDialog = () => {
|
||||
qrCodeDialogVisible.value = false
|
||||
qrCodeData.value = ''
|
||||
qrCodeLoading.value = false
|
||||
}
|
||||
|
||||
// 下载二维码
|
||||
const downloadQrCode = () => {
|
||||
if (!qrCodeData.value) {
|
||||
ElMessage.warning('暂无二维码可下载')
|
||||
return
|
||||
}
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = qrCodeData.value
|
||||
link.download = `item-qr-code-${Date.now()}.png`
|
||||
link.click()
|
||||
ElMessage.success('二维码下载成功')
|
||||
}
|
||||
|
||||
// 显示管理员代领弹窗
|
||||
const showProxyClaimDialog = (row: any) => {
|
||||
proxyClaimDialogVisible.value = true
|
||||
proxyClaimForm.item_id = row.id
|
||||
proxyClaimForm.target_user_ids = []
|
||||
}
|
||||
|
||||
// 提交管理员代领
|
||||
const handleProxyClaimSubmit = async () => {
|
||||
if (!proxyClaimForm.target_user_ids || proxyClaimForm.target_user_ids.length === 0) {
|
||||
ElMessage.warning('请选择用户')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { message } = await invitemService.proxyClaim({
|
||||
item_id: proxyClaimForm.item_id,
|
||||
target_user_id: proxyClaimForm.target_user_ids.join(',')
|
||||
})
|
||||
ElMessage.success(message)
|
||||
proxyClaimDialogVisible.value = false
|
||||
getWorkList()
|
||||
} catch (error) {
|
||||
console.error('管理员代领失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示库存记录弹窗
|
||||
const showStockLogDialog = async (row: any) => {
|
||||
currentStockLogItemId.value = row.id
|
||||
stockLogDialogVisible.value = true
|
||||
// 重置搜索条件和分页
|
||||
stockLogSearchForm.change_type = undefined
|
||||
stockLogPagination.currentPage = 1
|
||||
stockLogPagination.pageSize = 10
|
||||
await getStockLogList()
|
||||
}
|
||||
|
||||
// 获取库存记录列表
|
||||
const getStockLogList = async () => {
|
||||
if (!currentStockLogItemId.value) return
|
||||
|
||||
stockLogLoading.value = true
|
||||
stockLogList.value = []
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
item_id: currentStockLogItemId.value,
|
||||
current: stockLogPagination.currentPage,
|
||||
size: stockLogPagination.pageSize
|
||||
}
|
||||
if (stockLogSearchForm.change_type) {
|
||||
params.change_type = stockLogSearchForm.change_type
|
||||
}
|
||||
|
||||
const response = await invitemService.getStockLogList(params)
|
||||
const records = (response as any)?.data || (response as any)?.records || []
|
||||
stockLogList.value = Array.isArray(records) ? records : []
|
||||
stockLogPagination.total = (response as any)?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取库存记录失败:', error)
|
||||
ElMessage.error('获取库存记录失败')
|
||||
} finally {
|
||||
stockLogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索库存记录
|
||||
const searchStockLog = () => {
|
||||
stockLogPagination.currentPage = 1
|
||||
getStockLogList()
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const resetStockLogSearch = () => {
|
||||
stockLogSearchForm.change_type = undefined
|
||||
stockLogPagination.currentPage = 1
|
||||
getStockLogList()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleStockLogSizeChange = (newPageSize: number) => {
|
||||
stockLogPagination.pageSize = newPageSize
|
||||
getStockLogList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleStockLogCurrentChange = (newCurrentPage: number) => {
|
||||
stockLogPagination.currentPage = newCurrentPage
|
||||
getStockLogList()
|
||||
}
|
||||
|
||||
// 显示扫码记录弹窗
|
||||
const showQrClaimsDialog = async (row: any) => {
|
||||
currentQrClaimsItemId.value = row.id
|
||||
qrClaimsDialogVisible.value = true
|
||||
// 重置搜索条件和分页
|
||||
qrClaimsSearchForm.claim_type = undefined
|
||||
qrClaimsSearchForm.start_time = ''
|
||||
qrClaimsSearchForm.end_time = ''
|
||||
qrClaimsPagination.currentPage = 1
|
||||
qrClaimsPagination.pageSize = 10
|
||||
await getQrClaimsList()
|
||||
}
|
||||
|
||||
// 获取扫码记录列表
|
||||
const getQrClaimsList = async () => {
|
||||
if (!currentQrClaimsItemId.value) return
|
||||
|
||||
qrClaimsLoading.value = true
|
||||
qrClaimsList.value = []
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
item_id: currentQrClaimsItemId.value,
|
||||
current: qrClaimsPagination.currentPage,
|
||||
size: qrClaimsPagination.pageSize
|
||||
}
|
||||
if (qrClaimsSearchForm.claim_type) {
|
||||
params.claim_type = qrClaimsSearchForm.claim_type
|
||||
}
|
||||
if (qrClaimsSearchForm.start_time) {
|
||||
params.start_time = qrClaimsSearchForm.start_time
|
||||
}
|
||||
if (qrClaimsSearchForm.end_time) {
|
||||
params.end_time = qrClaimsSearchForm.end_time
|
||||
}
|
||||
|
||||
const response = await invitemService.getQrClaimsList(params)
|
||||
const records = (response as any)?.data || (response as any)?.records || []
|
||||
qrClaimsList.value = Array.isArray(records) ? records : []
|
||||
qrClaimsPagination.total = (response as any)?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取扫码记录失败:', error)
|
||||
ElMessage.error('获取扫码记录失败')
|
||||
} finally {
|
||||
qrClaimsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索扫码记录
|
||||
const searchQrClaims = () => {
|
||||
qrClaimsPagination.currentPage = 1
|
||||
getQrClaimsList()
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const resetQrClaimsSearch = () => {
|
||||
qrClaimsSearchForm.claim_type = undefined
|
||||
qrClaimsSearchForm.start_time = ''
|
||||
qrClaimsSearchForm.end_time = ''
|
||||
qrClaimsPagination.currentPage = 1
|
||||
getQrClaimsList()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleQrClaimsSizeChange = (newPageSize: number) => {
|
||||
qrClaimsPagination.pageSize = newPageSize
|
||||
getQrClaimsList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleQrClaimsCurrentChange = (newCurrentPage: number) => {
|
||||
qrClaimsPagination.currentPage = newCurrentPage
|
||||
getQrClaimsList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.qr-code-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
.qr-code-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.qr-code-image {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.qr-code-desc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.qr-code-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,742 @@
|
||||
<template>
|
||||
<div class="return-approval-page">
|
||||
<div class="return-toolbar boxshadow">
|
||||
<div class="toolbar-right">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="选择状态"
|
||||
clearable
|
||||
style="width: 150px; margin-right: 12px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="handleSearch"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="归还单号" prop="order_no" min-width="160" />
|
||||
<ElTableColumn label="申请人" prop="applicant_name" min-width="120" />
|
||||
<ElTableColumn label="归还物品" prop="return_item" min-width="200">
|
||||
<template #default="scope">
|
||||
<span>{{ getReturnItemNames(scope.row.return_item) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="归还时间" prop="return_at" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status" min-width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleViewDetail(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
v-if="hasAuth('return_approval@show')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看详情
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="canApprove(scope.row) && hasAuth('return_approval@approve')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleApprove(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="归还申请详情"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="detailData.id" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>基本信息</span>
|
||||
</div>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>归还单号</label>
|
||||
<span>{{ detailData.order_no }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请人</label>
|
||||
<span>{{ detailData.applicant_name }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>归还时间</label>
|
||||
<span>{{ detailData.return_at }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>状态</label>
|
||||
<span>{{ getStatusLabel(detailData.status) }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row" v-if="detailData.apply_remark">
|
||||
<ElCol :span="24">
|
||||
<div class="info-item">
|
||||
<label>申请备注</label>
|
||||
<span>{{ detailData.apply_remark }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row" v-if="detailData.handle_remark">
|
||||
<ElCol :span="24">
|
||||
<div class="info-item">
|
||||
<label>处理备注</label>
|
||||
<span>{{ detailData.handle_remark }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 归还物品 -->
|
||||
<div class="info-section" v-if="detailData.item_return && detailData.item_return.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>归还物品</span>
|
||||
</div>
|
||||
<div class="item-list">
|
||||
<div
|
||||
v-for="(item, index) in detailData.item_return"
|
||||
:key="index"
|
||||
class="item-card"
|
||||
>
|
||||
<div class="item-row">
|
||||
<span class="item-label">物品名称</span>
|
||||
<span class="item-value">{{ item.requisition_item.item_name_snap }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">申请数量</span>
|
||||
<span class="item-value">{{ item.requisition_item.request_qty }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">已归还数量</span>
|
||||
<span class="item-value">{{ item.requisition_item.returned_qty }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">本次归还</span>
|
||||
<span class="item-value">{{ item.return_qty }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附件 -->
|
||||
<div class="info-section" v-if="detailData.return_attachment && detailData.return_attachment.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>附件</span>
|
||||
</div>
|
||||
<div class="image-attachment-list">
|
||||
<div
|
||||
v-for="(file, index) in detailData.return_attachment"
|
||||
:key="index"
|
||||
class="image-attachment-item"
|
||||
>
|
||||
<el-image
|
||||
:src="file.file_path_pull"
|
||||
:preview-src-list="getImagePreviewUrls(detailData.return_attachment)"
|
||||
:initial-index="Number(index)"
|
||||
fit="cover"
|
||||
class="attachment-image"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="image-name">{{ file.file_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审批弹窗 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
title="归还申请审批"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<div v-if="approvalDetail.id" class="approval-content">
|
||||
<!-- 申请详情展示 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>申请信息</span>
|
||||
</div>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>归还单号</label>
|
||||
<span>{{ approvalDetail.order_no }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请人</label>
|
||||
<span>{{ approvalDetail.applicant_name }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="16" class="info-row">
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>归还时间</label>
|
||||
<span>{{ approvalDetail.return_at }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="info-item">
|
||||
<label>申请备注</label>
|
||||
<span>{{ approvalDetail.apply_remark || '-' }}</span>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 归还物品 -->
|
||||
<div class="info-section" v-if="approvalDetail.item_return && approvalDetail.item_return.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>归还物品</span>
|
||||
</div>
|
||||
<div class="item-list">
|
||||
<div
|
||||
v-for="(item, index) in approvalDetail.item_return"
|
||||
:key="index"
|
||||
class="item-card"
|
||||
>
|
||||
<div class="item-row">
|
||||
<span class="item-label">物品名称</span>
|
||||
<span class="item-value">{{ item.requisition_item.item_name_snap }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">申请数量</span>
|
||||
<span class="item-value">{{ item.requisition_item.request_qty }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">已归还数量</span>
|
||||
<span class="item-value">{{ item.requisition_item.returned_qty }}</span>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<span class="item-label">本次归还</span>
|
||||
<span class="item-value">{{ item.return_qty }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>审批意见</span>
|
||||
</div>
|
||||
<ElInput
|
||||
v-model="approvalForm.comment"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入审批意见"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 上传附件 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
<span>附件(可选)</span>
|
||||
</div>
|
||||
<el-upload
|
||||
v-model:file-list="approvalFileList"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-remove="handleUploadRemove"
|
||||
:limit="5"
|
||||
multiple
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持图片、PDF等格式,最多上传5个文件</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton
|
||||
type="danger"
|
||||
plain
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleSubmitApprove('reject')"
|
||||
>
|
||||
拒绝
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleSubmitApprove('approve')"
|
||||
>
|
||||
同意
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UploadUserFile, UploadProps } from 'element-plus'
|
||||
import { invitemService } from '@/api/admin/Itemrequisition'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
|
||||
defineOptions({ name: 'ReturnApprovalPage' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
|
||||
// 上传地址
|
||||
const uploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=return_approval`
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '待审核', value: 0 },
|
||||
{ label: '已通过', value: 1 },
|
||||
{ label: '未通过', value: 2 }
|
||||
]
|
||||
|
||||
// 获取状态标签颜色
|
||||
const getStatusType = (status: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
|
||||
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
|
||||
0: 'warning', // 待审核
|
||||
1: 'success', // 已通过
|
||||
2: 'danger' // 未通过
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusLabel = (status: number) => {
|
||||
const labelMap: Record<number, string> = {
|
||||
0: '待审核',
|
||||
1: '已通过',
|
||||
2: '未通过'
|
||||
}
|
||||
return labelMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 判断是否可以审批
|
||||
const canApprove = (row: any) => {
|
||||
return row.status === 0
|
||||
}
|
||||
|
||||
// 获取归还物品名称列表
|
||||
const getReturnItemNames = (returnItems: any[]) => {
|
||||
if (!returnItems || returnItems.length === 0) return '-'
|
||||
const names = returnItems
|
||||
.map((item) => {
|
||||
if (item.item && item.item.length > 0) {
|
||||
return item.item[0].item_name
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
return names.join('、')
|
||||
}
|
||||
|
||||
// 获取图片预览URL列表
|
||||
const getImagePreviewUrls = (attachments: any[]) => {
|
||||
if (!attachments || attachments.length === 0) return []
|
||||
return attachments.map((item) => item.file_path_pull)
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 详情弹窗数据
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
|
||||
// 审批弹窗数据
|
||||
const approvalDialogVisible = ref(false)
|
||||
const currentRowId = ref<number | null>(null)
|
||||
const approvalDetail = ref<any>({})
|
||||
const approvalForm = reactive({
|
||||
comment: '',
|
||||
attachment: [] as any[]
|
||||
})
|
||||
const approvalFileList = ref<UploadUserFile[]>([])
|
||||
|
||||
// 上传成功处理
|
||||
const handleUploadSuccess: UploadProps['onSuccess'] = (response) => {
|
||||
if (response.code === 200) {
|
||||
approvalForm.attachment.push({
|
||||
original_name: response.data.original_name || '文件',
|
||||
file_path: response.data.file_path || response.data.file_url,
|
||||
file_url: response.data.file_url
|
||||
})
|
||||
ElMessage.success('文件上传成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件处理
|
||||
const handleUploadRemove: UploadProps['onRemove'] = (file) => {
|
||||
const index = approvalForm.attachment.findIndex(
|
||||
(item) => item.file_url === file.url || item.original_name === file.name
|
||||
)
|
||||
if (index > -1) {
|
||||
approvalForm.attachment.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
// 获取列表数据
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
}
|
||||
if (searchForm.keyword) {
|
||||
params.keyword = searchForm.keyword
|
||||
}
|
||||
if (searchForm.status !== undefined) {
|
||||
params.status = searchForm.status
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await invitemService.getReturnList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (row: any) => {
|
||||
try {
|
||||
const detail = await invitemService.getReturnDetail({ id: row.id })
|
||||
detailData.value = detail
|
||||
detailDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开审批弹窗 - 获取完整详情
|
||||
const handleApprove = async (row: any) => {
|
||||
try {
|
||||
const detail = await invitemService.getReturnDetail({ id: row.id })
|
||||
currentRowId.value = row.id
|
||||
approvalDetail.value = detail
|
||||
approvalForm.comment = ''
|
||||
approvalForm.attachment = []
|
||||
approvalFileList.value = []
|
||||
approvalDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交审批
|
||||
const handleSubmitApprove = async (action: 'approve' | 'reject') => {
|
||||
if (!approvalForm.comment) {
|
||||
ElMessage.warning('请输入审批意见')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { message } = await invitemService.returnconfirm({
|
||||
id: currentRowId.value!,
|
||||
action,
|
||||
comment: approvalForm.comment,
|
||||
attachment: approvalForm.attachment.map((item) => ({
|
||||
original_name: item.original_name,
|
||||
file_path: item.file_path
|
||||
}))
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalForm.comment = ''
|
||||
approvalForm.attachment = []
|
||||
currentRowId.value = null
|
||||
approvalDetail.value = {}
|
||||
approvalFileList.value = []
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.return-approval-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.return-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 详情和审批弹窗样式
|
||||
.detail-content,
|
||||
.approval-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin: 20px 0;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
// 物品列表
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片附件列表
|
||||
.image-attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.image-attachment-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.attachment-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.image-error {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.image-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 弹窗底部按钮
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
|
||||
.submit-btn {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="color-page">
|
||||
<div class="color-toolbar boxshadow">
|
||||
<div class="color-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 120px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getColorList"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ElRow>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton @click="addfn()" v-ripple v-if="hasAuth('approval_list@add')" type="primary">
|
||||
新增审核流程
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="名称" prop="name" />
|
||||
<ElTableColumn label="标识" prop="type" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.is_active ? 'success' : 'info'">
|
||||
{{ scope.row.is_active ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="140px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('approval_list@edit')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteColor(scope.row)"
|
||||
v-if="hasAuth('approval_list@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog v-model="dialogVisible" title="新增审批流程" align-center width="600px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="标识" prop="type">
|
||||
<ElInput v-model="form.type" placeholder="请输入标识" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="超时时间" prop="timeout_hours">
|
||||
<ElInput v-model="form.timeout_hours" placeholder="请输入超时时间" >
|
||||
<template #suffix>
|
||||
<span>h</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="超时动作" prop="timeout_action">
|
||||
<el-select v-model="form.timeout_action" placeholder="请选择颜色类型" style="width: 100%">
|
||||
<el-option label="外观颜色" :value="1" />
|
||||
<el-option label="内饰颜色" :value="2" />
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.is_active" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { router } from '@/router'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminApprovalList' })
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const typeFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
name: '',
|
||||
type: '',
|
||||
timeout_hours: '',
|
||||
timeout_action: '',
|
||||
is_active: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getColorList()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([keyword, typeFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getColorList()
|
||||
})
|
||||
// 新增按钮点击
|
||||
const addfn = () => {
|
||||
router.push('/admin/approval/approval_add')
|
||||
}
|
||||
// 获取颜色列表
|
||||
const getColorList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.getstocklist(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取颜色列表失败:', error)
|
||||
ElMessage.error('获取颜色列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const resetSearch = () => {
|
||||
keyword.value = ''
|
||||
typeFilter.value = undefined
|
||||
statusFilter.value = undefined
|
||||
pagination.currentPage = 1
|
||||
getColorList()
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getColorList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getColorList()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
router.push(`/admin/approval/approval_edit?id=${row.id}`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const deleteColor = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该审核流程吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await manageService.stockapprovaldelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getColorList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
// 新增
|
||||
const { message } = await manageService.stockapproval(form)
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getColorList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.color-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.color-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.color-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
|
||||
{{ scope.row.flows_name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
|
||||
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="avatar" :src="scope.row.initiator_avatar" alt="" />
|
||||
<p>{{ scope.row.initiator_name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
|
||||
<ElTableColumn label="提交时间" prop="created_at" min-width="120" />
|
||||
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
v-if="hasAuth('stocking_auth@show')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending' && hasAuth('stocking_auth@approve')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 快速审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 快速审批弹出框 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
:title="orderDialog.title"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<div
|
||||
v-if="orderDialog.type == 'stock_orders' || orderDialog.type == 'stock_orders_nullify'"
|
||||
>
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
</div>
|
||||
<div v-if="orderDialog.type == 'purchase_orders'">
|
||||
<ProcurementDetailView :detailData="orderDialog.orderData" />
|
||||
</div>
|
||||
<!-- 审批备注 -->
|
||||
<div class="info-section">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24">
|
||||
<el-input
|
||||
v-model="approvalNotes"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="备注"
|
||||
class="approval-notes"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleApprove('reject')"
|
||||
>驳回</ElButton
|
||||
>
|
||||
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
|
||||
>同意</ElButton
|
||||
>
|
||||
<ElButton
|
||||
v-if="orderDialog.type == 'purchase_orders'"
|
||||
type="warning"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleTransfer()"
|
||||
>转交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 转交弹出框 -->
|
||||
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
|
||||
<el-form :model="zjForm" label-width="80px">
|
||||
<el-form-item label="转交人" prop="to_uid">
|
||||
<el-select
|
||||
v-model="zjForm.to_uid"
|
||||
filterable
|
||||
placeholder="请选择转交人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<ElFormItem label="转交原因" prop="reason">
|
||||
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
|
||||
</ElFormItem>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="zjDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { router } from '@/router'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
|
||||
defineOptions({ name: 'AdminStockingPage' })
|
||||
const zjDialogVisible = ref(false)
|
||||
const orderDialog = reactive({
|
||||
orderData: {} as OrderData,
|
||||
title: '',
|
||||
type: ''
|
||||
})
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
|
||||
// 根据 flow_type 获取审批单名称的字体颜色
|
||||
const getFlowTypeColor = (flowType: string): string => {
|
||||
switch (flowType) {
|
||||
case 'stock_orders':
|
||||
return '#409EFF' // 蓝色 - 备货单
|
||||
case 'stock_orders_nullify':
|
||||
return '#F56C6C' // 红色 - 备货单作废
|
||||
case 'purchase_orders':
|
||||
return '#67C23A' // 绿色 - 采购单
|
||||
case 'purchase_orders_nullify':
|
||||
return '#E6A23C' // 橙色 - 采购单作废
|
||||
case 'return_orders':
|
||||
return '#909399' // 灰色 - 退货单
|
||||
case 'refund_orders':
|
||||
return '#9C27B0' // 紫色 - 退款单
|
||||
default:
|
||||
return '#303133' // 默认黑色
|
||||
}
|
||||
}
|
||||
const zjForm = reactive({
|
||||
to_uid: '',
|
||||
instance_id: '',
|
||||
step_index: '',
|
||||
reason: ''
|
||||
})
|
||||
// 审批弹出框相关
|
||||
const approvalDialogVisible = ref(false)
|
||||
|
||||
const approvalNotes = ref('')
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'todo',
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.stockapprovalrecord(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
const handleView = (row: any) => {
|
||||
router.push({
|
||||
path: '/admin/approval/stocking_auth_detil',
|
||||
query: {
|
||||
id: row.id,
|
||||
biz_id: row.biz_id,
|
||||
instance_id: row.instance_id,
|
||||
status: row.status,
|
||||
flow_type: row.flow_type,
|
||||
step_index: row.step_index
|
||||
} // 可带客户名等参数
|
||||
})
|
||||
}
|
||||
const instance_id = ref('')
|
||||
function extractFileName(pathOrUrl: string | undefined | null): string {
|
||||
if (!pathOrUrl) return '合同文件'
|
||||
try {
|
||||
const clean = String(pathOrUrl)
|
||||
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
|
||||
return decodeURIComponent(afterSlash)
|
||||
} catch {
|
||||
return '合同文件'
|
||||
}
|
||||
}
|
||||
const activerow = ref()
|
||||
const handleCreateRepair = async (row: any) => {
|
||||
activerow.value = row
|
||||
instance_id.value = row.instance_id
|
||||
const res = await manageService.stockapprovaldetailborder({
|
||||
id: row.id,
|
||||
flow_type: row.flow_type
|
||||
})
|
||||
console.log(res)
|
||||
switch (row.flow_type) {
|
||||
case 'stock_orders': {
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.type = row.flow_type
|
||||
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
|
||||
break
|
||||
}
|
||||
case 'stock_orders_nullify': {
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.type = row.flow_type
|
||||
orderDialog.title = '备货单-作废'
|
||||
break
|
||||
}
|
||||
case 'purchase_orders': {
|
||||
const orderData: OrderData = res
|
||||
const urls = res.contract_file_url
|
||||
const paths = res.contract_file
|
||||
const list: Array<{ name: string; url: string }> = []
|
||||
const nameFromPath = (p: string) => extractFileName(p)
|
||||
urls.forEach((u: string, i: number) => {
|
||||
const p = paths[i] || ''
|
||||
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
|
||||
})
|
||||
orderData.contract_file_url = list
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.type = row.flow_type
|
||||
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
|
||||
break
|
||||
}
|
||||
}
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批操作
|
||||
const handleApprove = async (approvalAction: string) => {
|
||||
try {
|
||||
// 这里调用审批API
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id.value,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalNotes.value = ''
|
||||
instance_id.value = ''
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
ElMessage.error('审批失败')
|
||||
}
|
||||
}
|
||||
// 转交操作
|
||||
const handleTransfer = async () => {
|
||||
zjForm.instance_id = activerow.value.instance_id
|
||||
zjForm.step_index = activerow.value.step_index
|
||||
getuserList()
|
||||
zjDialogVisible.value = true
|
||||
}
|
||||
const handlezjSubmit = async () => {
|
||||
if (!zjForm.to_uid) {
|
||||
ElMessage.warning('请选择转交人')
|
||||
return
|
||||
}
|
||||
if (!zjForm.reason) {
|
||||
ElMessage.warning('请输入转交原因')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { message } = await manageService.stockapprovaltransfer(zjForm)
|
||||
ElMessage.success(message)
|
||||
zjDialogVisible.value = false
|
||||
approvalDialogVisible.value = false
|
||||
approvalNotes.value = ''
|
||||
zjForm.to_uid = ''
|
||||
zjForm.reason = ''
|
||||
instance_id.value = ''
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('转交失败:', error)
|
||||
// ElMessage.error('转交失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 审批弹出框样式
|
||||
.approval-dialog {
|
||||
.approval-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin: 24px 0px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-items {
|
||||
.order-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details {
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-notes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
gap: 12px;
|
||||
|
||||
.reject-btn {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<div class="stocking-show-page">
|
||||
<div class="boxshadow">
|
||||
<div v-if="flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify'">
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
</div>
|
||||
<div v-if="flow_type == 'purchase_orders'">
|
||||
<ProcurementDetailView :detailData="orderDialog.orderData" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传合同 -->
|
||||
<div class="boxshadow">
|
||||
<FilePreview title="合同文件" :files="contractFiles" empty-text="暂无合同文件" />
|
||||
</div>
|
||||
|
||||
<!-- PI文件 -->
|
||||
<div class="boxshadow">
|
||||
<FilePreview title="PI文件" :files="piFiles" empty-text="暂无PI文件" />
|
||||
</div>
|
||||
|
||||
<!-- 水单文件 -->
|
||||
<div class="boxshadow">
|
||||
<FilePreview title="水单文件" :files="waterBillFiles" empty-text="暂无水单文件" />
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">审批记录</span>
|
||||
</div>
|
||||
<div class="approval-timeline">
|
||||
<div v-for="(rec, idx) in approvalRecords" :key="idx" class="timeline-item">
|
||||
<div class="left">
|
||||
<div class="avatar-box">
|
||||
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
|
||||
<i v-if="rec.approved" class="status-dot iconfont-sys"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row name-role">
|
||||
<span class="name">{{ rec.name }}</span>
|
||||
<span class="role">({{ rec.role || rec.type || '审批人' }})</span>
|
||||
</div>
|
||||
<div class="row time">{{ rec.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!approvalRecords.length" class="empty">暂无审批记录</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow" v-if="status == 'pending'">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">审批</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="备注:">
|
||||
<el-input
|
||||
v-model="approvalNotes"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入备注信息"
|
||||
class="approval-notes"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-card" v-if="status == 'pending'">
|
||||
<!-- 底部信息 -->
|
||||
<div class="submit-wrap">
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleSubmit('reject')"
|
||||
>驳回</ElButton
|
||||
>
|
||||
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
|
||||
>同意</ElButton
|
||||
>
|
||||
<ElButton
|
||||
v-if="flow_type == 'purchase_orders'"
|
||||
type="warning"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
@click="handleTransfer()"
|
||||
>转交</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PDF 预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="pdf.visible"
|
||||
title="打印预览"
|
||||
width="80%"
|
||||
top="40px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px">
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<iframe
|
||||
v-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 转交弹出框 -->
|
||||
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
|
||||
<el-form :model="zjForm" label-width="80px">
|
||||
<el-form-item label="转交人" prop="to_uid">
|
||||
<el-select
|
||||
v-model="zjForm.to_uid"
|
||||
filterable
|
||||
placeholder="请选择转交人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<ElFormItem label="转交原因" prop="reason">
|
||||
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
|
||||
</ElFormItem>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="zjDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
|
||||
import FilePreview from '@/components/custom/FilePreview.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
import { router } from '@/router'
|
||||
defineOptions({ name: 'AdminStockingAuthDetil' })
|
||||
const { pdf, frameRef, download } = usePdfPreview()
|
||||
const zjDialogVisible = ref(false)
|
||||
const route = useRoute()
|
||||
const approval_id = route.query.id
|
||||
const stocking_id = route.query.biz_id
|
||||
const instance_id = route.query.instance_id
|
||||
const step_index = route.query.step_index
|
||||
const status = route.query.status
|
||||
const flow_type = route.query.flow_type
|
||||
const approvalNotes = ref('')
|
||||
const orderDialog = reactive({
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
const zjForm = reactive({
|
||||
to_uid: '',
|
||||
instance_id: '',
|
||||
step_index: '',
|
||||
reason: ''
|
||||
})
|
||||
// 转交操作
|
||||
const handleTransfer = async () => {
|
||||
getuserList()
|
||||
zjDialogVisible.value = true
|
||||
}
|
||||
const handlezjSubmit = async () => {
|
||||
if (!zjForm.to_uid) {
|
||||
ElMessage.warning('请选择转交人')
|
||||
return
|
||||
}
|
||||
if (!zjForm.reason) {
|
||||
ElMessage.warning('请输入转交原因')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const zjForms = {
|
||||
to_uid: zjForm.to_uid,
|
||||
instance_id: instance_id,
|
||||
step_index: step_index,
|
||||
reason: zjForm.reason
|
||||
}
|
||||
const { message } = await manageService.stockapprovaltransfer(zjForms)
|
||||
ElMessage.success(message)
|
||||
router.push('/admin/approval/stocking_auth')
|
||||
} catch (error) {
|
||||
console.error('转交失败:', error)
|
||||
// ElMessage.error('转交失败')
|
||||
}
|
||||
}
|
||||
const handleSubmit = async (approvalAction: string) => {
|
||||
try {
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
router.push('/admin/approval/stocking_auth')
|
||||
ElMessage.success(message)
|
||||
} catch {
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
// 审批记录(先填充假数据展示)
|
||||
const approvalRecords = ref<any[]>([])
|
||||
const getDictItemSelectlist = ref<any[]>([])
|
||||
const getDictItemSelect = async () => {
|
||||
const res = await commonService.getDictItemSelect({ dict_id: '18' })
|
||||
getDictItemSelectlist.value = res
|
||||
}
|
||||
|
||||
// 合同文件
|
||||
// 文件数据
|
||||
const contractFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
|
||||
const piFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
|
||||
const waterBillFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
|
||||
|
||||
// 处理文件数据
|
||||
const processFiles = (files: any[], type: string) => {
|
||||
if (!files || !Array.isArray(files)) return []
|
||||
return files.map((file: any) => ({
|
||||
name: file.original_name,
|
||||
url: file.file_full_url || file.file_url,
|
||||
file_name: file.file_url
|
||||
}))
|
||||
}
|
||||
function extractFileName(pathOrUrl: string | undefined | null): string {
|
||||
if (!pathOrUrl) return '合同文件'
|
||||
try {
|
||||
const clean = String(pathOrUrl)
|
||||
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
|
||||
return decodeURIComponent(afterSlash)
|
||||
} catch {
|
||||
return '合同文件'
|
||||
}
|
||||
}
|
||||
//获取详情并映射到回显模型
|
||||
const getdetilfn = async () => {
|
||||
const res = await manageService.stockapprovaldetailborder({
|
||||
id: approval_id,
|
||||
flow_type: flow_type
|
||||
})
|
||||
if (flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify') {
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
} else if (flow_type == 'purchase_orders') {
|
||||
const orderData: OrderData = res
|
||||
const urls = res.contract_file_url
|
||||
const paths = res.contract_file
|
||||
const list: Array<{ name: string; url: string }> = []
|
||||
const nameFromPath = (p: string) => extractFileName(p)
|
||||
urls.forEach((u: string, i: number) => {
|
||||
const p = paths[i] || ''
|
||||
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
|
||||
})
|
||||
orderData.contract_file_url = list
|
||||
orderDialog.orderData = orderData
|
||||
}
|
||||
// 处理合同文件
|
||||
const file_list = res?.file_list
|
||||
if (file_list?.contract) {
|
||||
contractFiles.value = processFiles(file_list.contract, '合同')
|
||||
} else {
|
||||
contractFiles.value = []
|
||||
}
|
||||
|
||||
// 处理PI文件
|
||||
if (file_list?.pi_doc) {
|
||||
piFiles.value = processFiles(file_list.pi_doc, 'PI')
|
||||
} else {
|
||||
piFiles.value = []
|
||||
}
|
||||
|
||||
// 处理水单文件
|
||||
if (file_list?.transaction_slip) {
|
||||
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
|
||||
} else {
|
||||
waterBillFiles.value = []
|
||||
}
|
||||
// 审批记录(兼容多种字段)
|
||||
const resstockapprovallogs = await manageService.stockapprovallogs({ instance_id: instance_id })
|
||||
console.log(resstockapprovallogs)
|
||||
const historyList = resstockapprovallogs
|
||||
if (historyList.length > 0) {
|
||||
approvalRecords.value = historyList.map((h: any) => ({
|
||||
name: h?.initiator_name ? h?.initiator_name : h?.name,
|
||||
role: h?.index == -1 ? '申请人' : '审批人',
|
||||
time: h?.finished_at || '',
|
||||
approved: h?.status === 'approved' || h?.status === 'submitted',
|
||||
rejected: h?.status === 'rejected',
|
||||
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted'
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDictItemSelect()
|
||||
getdetilfn()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-show-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.header-left {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.sub {
|
||||
color: #333333;
|
||||
}
|
||||
.mr40 {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
.form-title {
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-title .title {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.items-wrap {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.item-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
:deep(.el-input.is-disabled .el-input__inner) {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contract-file {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #409eff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
.submit-wrap {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 24px 0 8px;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* 审批记录 时间线 */
|
||||
.approval-timeline {
|
||||
padding: 8px 4px 8px 0;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline-item + .timeline-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.left {
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: #3d7bff;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
// width: 14px;
|
||||
// height: 14px;
|
||||
font-size: 9px;
|
||||
padding: 3px 1px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.status-dot.pending {
|
||||
background: #3471ff;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
.status-dot.rejected {
|
||||
background: #ff0000;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
/* 连接下一个节点的虚线 */
|
||||
.timeline-item:not(:last-child) .left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 52px; /* avatar 46px + 间距 6px */
|
||||
left: 23px; /* 盒子宽度一半,确保竖线居中 */
|
||||
width: 0;
|
||||
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
|
||||
border-left: 2px dotted #bfbfbf;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row.name-role {
|
||||
color: #222;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.role {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.empty {
|
||||
color: #999;
|
||||
padding: 8px 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<!-- <div class="toolbar-left">
|
||||
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
|
||||
</div> -->
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
|
||||
{{ scope.row.flows_name }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
|
||||
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
|
||||
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="avatar" :src="scope.row.initiator_avatar" alt="" />
|
||||
<p>{{ scope.row.initiator_name }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
|
||||
<ElTableColumn label="审批时间" prop="approved_at" min-width="120" />
|
||||
<ElTableColumn label="提交时间" prop="created_at" min-width="120" />
|
||||
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
v-if="hasAuth('stocking_auth_history@show')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending' && hasAuth('stocking_auth_history@approve')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 快速审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 快速审批弹出框 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
title="合塑实业-汽车事业部-备货单(客户下定)"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
<!-- 审批备注 -->
|
||||
<div class="info-section">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24">
|
||||
<el-input
|
||||
v-model="approvalNotes"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="备注"
|
||||
class="approval-notes"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
|
||||
>驳回</ElButton
|
||||
>
|
||||
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
|
||||
>同意</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { router } from '@/router'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { CrmService } from '@/api/crmApi'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
defineOptions({ name: 'AdminStockingPage' })
|
||||
const orderDialog = reactive({
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// 审批弹出框相关
|
||||
const approvalDialogVisible = ref(false)
|
||||
const stockingInfo = ref<any>({})
|
||||
|
||||
// 回显视图模型,字段与模板一致
|
||||
const header = reactive({ order_no: '', salesman: '' })
|
||||
const base = reactive({
|
||||
pn_no: '',
|
||||
date: '',
|
||||
close_date: '',
|
||||
channel: '',
|
||||
customer_name: '',
|
||||
client_country: '',
|
||||
cif_total: '',
|
||||
cif_total_currency: '',
|
||||
contract_rate: ''
|
||||
})
|
||||
const items = ref<any[]>([])
|
||||
|
||||
const approvalNotes = ref('')
|
||||
const approvalAction = ref('')
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
// 根据 flow_type 获取审批单名称的字体颜色
|
||||
const getFlowTypeColor = (flowType: string): string => {
|
||||
switch (flowType) {
|
||||
case 'stock_orders':
|
||||
return '#409EFF' // 蓝色 - 备货单
|
||||
case 'stock_orders_nullify':
|
||||
return '#F56C6C' // 红色 - 备货单作废
|
||||
case 'purchase_orders':
|
||||
return '#67C23A' // 绿色 - 采购单
|
||||
case 'purchase_orders_nullify':
|
||||
return '#E6A23C' // 橙色 - 采购单作废
|
||||
case 'return_orders':
|
||||
return '#909399' // 灰色 - 退货单
|
||||
case 'refund_orders':
|
||||
return '#9C27B0' // 紫色 - 退款单
|
||||
default:
|
||||
return '#303133' // 默认黑色
|
||||
}
|
||||
}
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'history',
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.stockapprovalrecord(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
const handleView = (row: any) => {
|
||||
router.push({
|
||||
path: '/admin/approval/stocking_auth_detil',
|
||||
query: {
|
||||
id: row.id,
|
||||
biz_id: row.biz_id,
|
||||
instance_id: row.instance_id,
|
||||
status: row.status,
|
||||
flow_type: row.flow_type,
|
||||
step_index: row.step_index
|
||||
} // 可带客户名等参数
|
||||
})
|
||||
}
|
||||
const instance_id = ref('')
|
||||
const handleCreateRepair = async (row: any) => {
|
||||
instance_id.value = row.instance_id
|
||||
const res = await manageService.checkStock({ id: row.biz_id })
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批操作
|
||||
const handleApprove = async (approvalAction: string) => {
|
||||
try {
|
||||
// 这里调用审批API
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id.value,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalNotes.value = ''
|
||||
instance_id.value = ''
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
ElMessage.error('审批失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 审批弹出框样式
|
||||
.approval-dialog {
|
||||
.approval-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-items {
|
||||
.order-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details {
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-notes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
gap: 12px;
|
||||
|
||||
.reject-btn {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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">
|
||||
<!-- 客户标记无效 原因与附件回显 -->
|
||||
@@ -167,6 +168,7 @@
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
defineOptions({ name: 'AdminCustomerAudit' })
|
||||
const { hasAuth } = useAuth()
|
||||
const keyword = ref<string>('')
|
||||
const loading = ref(false)
|
||||
@@ -326,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">
|
||||
@@ -257,6 +258,7 @@
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import emptyimg from '@/assets/img/crm/empty.png'
|
||||
defineOptions({ name: 'AdminHighseas' })
|
||||
const TIMELINE_PLACEMENT = 'top'
|
||||
const activecolor = '#3471ff'
|
||||
const { hasAuth } = useAuth()
|
||||
@@ -347,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
|
||||
|
||||
@@ -19,6 +19,17 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户渠道" prop="channel_id">
|
||||
<el-select v-model="selectchannel_id" filterable placeholder="请选择客户渠道">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
|
||||
<i class="iconfont-sys"></i> 导出
|
||||
@@ -227,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">
|
||||
@@ -341,10 +353,12 @@
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import emptyimg from '@/assets/img/crm/empty.png'
|
||||
defineOptions({ name: 'AdminCustomer' })
|
||||
const TIMELINE_PLACEMENT = 'top'
|
||||
const activecolor = '#3471ff'
|
||||
const { hasAuth } = useAuth()
|
||||
const selectValue = ref<string[]>([])
|
||||
const selectchannel_id = ref<string>('')
|
||||
const showMore = ref(false)
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
@@ -372,6 +386,12 @@
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const channelOptions = ref<any>()
|
||||
//渠道
|
||||
const getgetChannel = async () => {
|
||||
const res = await CrmService.getChannel()
|
||||
channelOptions.value = res || []
|
||||
}
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
@@ -401,12 +421,13 @@
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
onMounted(() => {
|
||||
getgetChannel()
|
||||
getuserList()
|
||||
gettableList()
|
||||
tagGrouplistfn()
|
||||
})
|
||||
// tabs 切换联动
|
||||
watch([selectValue, keyword], () => {
|
||||
watch([selectValue, keyword, selectchannel_id], () => {
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
})
|
||||
@@ -424,6 +445,9 @@
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
if (selectchannel_id.value) {
|
||||
params.channel_id = selectchannel_id.value
|
||||
}
|
||||
// 标签参数,所有选中的标签id组成数组
|
||||
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
|
||||
if (selectedTagIds.length) {
|
||||
@@ -463,7 +487,9 @@
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
user_id: selectValue.value.join(','),
|
||||
tag_id: Object.values(tagActiveMap).filter(Boolean)
|
||||
tag_id: Object.values(tagActiveMap).filter(Boolean),
|
||||
keyword: keyword.value,
|
||||
channel_id: selectchannel_id.value
|
||||
}
|
||||
// manageService.crmCustomerExport(params)
|
||||
try {
|
||||
@@ -471,7 +497,7 @@
|
||||
// const res = await manageService.crmCustomerExport(params)
|
||||
const urls =
|
||||
import.meta.env.VITE_API_URL +
|
||||
`/admin/crm/customer/export?current=${params.currentPage}&size=${params.pageSize}&user_id=${params.user_id}&tag_id=${params.tag_id}`
|
||||
`/admin/crm/customer/export?current=${params.current}&size=${params.size}&user_id=${params.user_id}&tag_id=${params.tag_id}&channel_id=${params.channel_id}&keyword=${params.keyword}`
|
||||
// 2. 创建blob对象
|
||||
// const blob = new Blob([res], { type: 'application/vnd.ms-excel' }) // 或根据后端实际类型调整
|
||||
// 3. 创建下载链接
|
||||
@@ -503,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
|
||||
|
||||
@@ -57,8 +57,13 @@
|
||||
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||
<el-table-column prop="del_source" label="来源"></el-table-column>
|
||||
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
|
||||
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
|
||||
<el-table-column
|
||||
prop="status_desc"
|
||||
label="删除原因"
|
||||
show-overflow-tooltip
|
||||
></el-table-column>
|
||||
<!-- <el-table-column prop="follow_last" label="上次跟进"></el-table-column> -->
|
||||
<!-- <el-table-column prop="follow_next" label="下次跟进"></el-table-column> -->
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="编号"
|
||||
@@ -139,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
|
||||
@@ -154,6 +160,7 @@
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
defineOptions({ name: 'AdminRecycle' })
|
||||
const { hasAuth } = useAuth()
|
||||
const selectValue = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -231,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
|
||||
@@ -256,20 +272,34 @@
|
||||
})
|
||||
}
|
||||
const delectfn = (row: any) => {
|
||||
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||
ElMessageBox.prompt('确定要删除此客户吗?请填写入删除原因!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
inputPattern: /^(?!\s*$).+/,
|
||||
inputErrorMessage: '请填写入删除原因'
|
||||
}).then(async ({ value }) => {
|
||||
const res = await manageService.crmCustomerDelete({
|
||||
customer_id: row.id,
|
||||
scene: 'force',
|
||||
status_desc: value
|
||||
})
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
// ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||
// confirmButtonText: '确定',
|
||||
// cancelButtonText: '取消',
|
||||
// type: 'warning'
|
||||
// })
|
||||
// .then(async () => {
|
||||
// const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
|
||||
// ElMessage.success(res?.message || '提交成功!')
|
||||
// gettableList()
|
||||
// // 你可以在这里调用接口或做后续处理
|
||||
// })
|
||||
// .catch(() => {
|
||||
// // 用户点击了取消
|
||||
// })
|
||||
}
|
||||
const closeImageViewer = () => {
|
||||
imageViewerVisible.value = false
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="account-page">
|
||||
<div class="account-toolbar boxshadow">
|
||||
<div class="account-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 180px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getAccountList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getAccountList" v-ripple>搜索</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ElRow>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('account@add')" type="primary"
|
||||
>新增账户</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="账户简称" prop="short_desc" min-width="160" />
|
||||
<ElTableColumn label="账户名称" prop="account_name" min-width="160" />
|
||||
<ElTableColumn label="账户号码" prop="account_number" min-width="160" />
|
||||
<ElTableColumn label="银行名称" prop="bank_name" min-width="140" />
|
||||
<!-- <ElTableColumn label="开户支行" prop="bank_branch" min-width="160" /> -->
|
||||
<ElTableColumn label="公司名称" prop="corp_name" min-width="160" />
|
||||
<ElTableColumn label="货币" prop="currency_id" min-width="120">
|
||||
<template #default="scope">
|
||||
<span>{{ getCurrencyLabel(scope.row.currency_id) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="排序" prop="sort" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
|
||||
scope.row.status ? '启用' : '停用'
|
||||
}}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" min-width="160px" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('account@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteAccount(scope.row)"
|
||||
v-if="hasAuth('account@destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增账户' : '编辑账户'"
|
||||
align-center
|
||||
width="700px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
|
||||
<ElFormItem label="账户简称" prop="short_desc">
|
||||
<ElInput v-model="form.short_desc" placeholder="请输入账户简称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户名称" prop="account_name">
|
||||
<ElInput v-model="form.account_name" placeholder="请输入账户名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户号码" prop="account_number">
|
||||
<ElInput v-model="form.account_number" placeholder="请输入账户号码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="银行名称" prop="bank_name">
|
||||
<ElInput v-model="form.bank_name" placeholder="请输入银行名称" />
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="开户支行" prop="bank_branch">
|
||||
<ElInput v-model="form.bank_branch" placeholder="请输入开户支行" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="公司名称" prop="corp_id">
|
||||
<el-select v-model="form.corp_id" placeholder="请选择公司" style="width: 100%">
|
||||
<el-option
|
||||
v-for="account in companyList"
|
||||
:key="account.id"
|
||||
:label="account.name"
|
||||
:value="account.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户货币类型" prop="currency_id">
|
||||
<el-select v-model="form.currency_id" placeholder="请选择货币" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in currencyOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="300"
|
||||
show-word-limit
|
||||
placeholder="请输入账户描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { financeService } from '@/api/financeApi'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'FinanceAccount' })
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const currencyFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
// 下拉:货币
|
||||
const currencyOptions = ref<any[]>([])
|
||||
const loadCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch (e) {
|
||||
currencyOptions.value = []
|
||||
}
|
||||
}
|
||||
const companyList = ref<any>()
|
||||
const getcompanyList = async () => {
|
||||
const response = await UserService.getCompanyList()
|
||||
companyList.value = response || []
|
||||
}
|
||||
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
currency_id: undefined,
|
||||
short_desc: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
bank_name: '',
|
||||
bank_branch: '',
|
||||
description: '',
|
||||
account_number: '',
|
||||
account_name: '',
|
||||
corp_id: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
short_desc: [{ required: true, message: '请输入账户简称', trigger: 'blur' }],
|
||||
account_name: [{ required: true, message: '请输入账户名称', trigger: 'blur' }],
|
||||
account_number: [{ required: true, message: '请输入账户号码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getcompanyList()
|
||||
loadCurrencyOptions()
|
||||
getAccountList()
|
||||
})
|
||||
watch([keyword, currencyFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getAccountList()
|
||||
})
|
||||
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
const { records, current, size, total } = await financeService.getAccountList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取账户列表失败:', error)
|
||||
ElMessage.error('获取账户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getAccountList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
short_desc: row.short_desc,
|
||||
currency_id: row.currency_id,
|
||||
status: row.status,
|
||||
sort: row.sort ?? 0,
|
||||
bank_name: row.bank_name,
|
||||
bank_branch: row.bank_branch,
|
||||
description: row.description,
|
||||
account_number: row.account_number,
|
||||
account_name: row.account_name,
|
||||
corp_id: row.corp_id
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
short_desc: '',
|
||||
currency_id: undefined,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
bank_name: '',
|
||||
bank_branch: '',
|
||||
description: '',
|
||||
account_number: '',
|
||||
account_name: '',
|
||||
corp_id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该账户吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await financeService.accountDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await financeService.accountAdd({
|
||||
currency_id: form.currency_id,
|
||||
short_desc: form.short_desc,
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
bank_name: form.bank_name,
|
||||
bank_branch: form.bank_branch,
|
||||
description: form.description,
|
||||
account_number: form.account_number,
|
||||
account_name: form.account_name,
|
||||
corp_id: form.corp_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await financeService.accountEdit({
|
||||
id: form.id,
|
||||
currency_id: form.currency_id,
|
||||
short_desc: form.short_desc,
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
bank_name: form.bank_name,
|
||||
bank_branch: form.bank_branch,
|
||||
description: form.description,
|
||||
account_number: form.account_number,
|
||||
account_name: form.account_name,
|
||||
corp_id: form.corp_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getCurrencyLabel = (id: number | undefined) => {
|
||||
if (!id) return '-'
|
||||
const found = currencyOptions.value.find((c: any) => c.id === id)
|
||||
return found ? `${found.currency_name} (${found.currency_code})` : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.account-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.account-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.account-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-left"></div>
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="未认领" name="not"></el-tab-pane>
|
||||
<el-tab-pane label="已认领" name="already"></el-tab-pane>
|
||||
<el-tab-pane label="超时未认领" name="timeout"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="创建日期" prop="created_at" min-width="160" />
|
||||
<!-- <ElTableColumn label="付款来源" prop="country_name" min-width="120" /> -->
|
||||
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
|
||||
<ElTableColumn label="账户" prop="account_name" min-width="120" />
|
||||
<ElTableColumn label="账币" prop="currency_name" min-width="120" />
|
||||
<ElTableColumn label="金额" prop="amount" min-width="120" />
|
||||
<ElTableColumn label="计时" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="!scope.row.payment_apply_id"
|
||||
:class="getCountdownClass(scope.row.claim_timeout_time)"
|
||||
>
|
||||
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
|
||||
<i class="iconfont-sys"></i>
|
||||
</span>
|
||||
{{ getCountdown(scope.row.claim_timeout_time) }}
|
||||
</span>
|
||||
<span v-else class="countdown-success">已认领</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
v-if="hasAuth('claim@show')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="!scope.row.payment_apply_id && hasAuth('claim@claim')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleClaim(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 认领
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 付款记录选择对话框 -->
|
||||
<PaymentRecordDialog
|
||||
v-model="paymentDialogVisible"
|
||||
:receipt-id="currentReceiptId"
|
||||
@confirm="handlePaymentRecordConfirm"
|
||||
/>
|
||||
<!-- 付款记录详情查看对话框 -->
|
||||
<ClaimRecordDetailDialog v-model="detailDialogVisible" :payment-id="currentPaymentId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import PaymentRecordDialog from '@/components/custom/PaymentRecordDialog.vue'
|
||||
import ClaimRecordDetailDialog from '@/components/custom/ClaimRecordDetailDialog.vue'
|
||||
import dayjs from 'dayjs'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'Adminclaim' })
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
const activeTab = ref('not')
|
||||
const detailDialogVisible = ref(false)
|
||||
// 付款记录对话框相关
|
||||
const paymentDialogVisible = ref(false)
|
||||
const currentReceiptId = ref<string | number>('')
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
watch([activeTab], () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
})
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
payment_apply: activeTab.value,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await financeService.getReceiptList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
const currentPaymentId = ref<string | number>('')
|
||||
const handleView = async (params: any) => {
|
||||
detailDialogVisible.value = true
|
||||
currentPaymentId.value = params.id
|
||||
await financeService.getReceiptDetail(params)
|
||||
}
|
||||
// 处理认领操作
|
||||
const handleClaim = (row: any) => {
|
||||
currentReceiptId.value = row.id
|
||||
paymentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理付款记录确认绑定
|
||||
const handlePaymentRecordConfirm = async (paymentRecord: any) => {
|
||||
try {
|
||||
const params = {
|
||||
id: currentReceiptId.value,
|
||||
payment_apply_id: paymentRecord.id
|
||||
}
|
||||
|
||||
await financeService.bindPaymentRecord(params)
|
||||
ElMessage.success('认领成功')
|
||||
getList() // 刷新列表
|
||||
} catch (error) {
|
||||
console.error('认领失败:', error)
|
||||
ElMessage.error('认领失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {}
|
||||
const timer = ref(0)
|
||||
const refreshKey = ref(0)
|
||||
onMounted(() => {
|
||||
getList()
|
||||
timer.value = window.setInterval(() => {
|
||||
refreshKey.value++
|
||||
}, 1000)
|
||||
})
|
||||
function getCountdown(targetDate: string) {
|
||||
// 让 refreshKey 成为依赖,确保每秒刷新
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
refreshKey.value
|
||||
const now = dayjs()
|
||||
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||
if (!target.isValid()) return '时间格式错误'
|
||||
const diff = target.diff(now)
|
||||
if (diff <= 0) return '已超时'
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
|
||||
const minutes = Math.floor((diff / (1000 * 60)) % 60)
|
||||
const seconds = Math.floor((diff / 1000) % 60)
|
||||
return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
|
||||
}
|
||||
|
||||
function getCountdownClass(targetDate: string) {
|
||||
const now = dayjs()
|
||||
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||
if (!target.isValid() || target.diff(now) <= 0) {
|
||||
return 'countdown-danger'
|
||||
}
|
||||
return 'countdown-success'
|
||||
}
|
||||
const tableRowClassName = ({ row }: { row: any }) => {
|
||||
const flag = Number(row?.claim_timeout)
|
||||
return flag === 1 ? 'row-claim-timeout' : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.row-claim-timeout) {
|
||||
background-color: #ffc9c9 !important;
|
||||
}
|
||||
:deep(.el-tabs__nav-wrap:after){
|
||||
background: #fff;
|
||||
}
|
||||
.countdown-danger {
|
||||
color: #ff0000 !important;
|
||||
}
|
||||
|
||||
.countdown-success {
|
||||
color: #00a43f !important;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
// 创建收款记录 - 弹窗样式对齐设计图
|
||||
.receipt-dialog {
|
||||
.receipt-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: flex-end;
|
||||
// border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatarimg {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.receipt-dialog .el-dialog__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ElRow>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('currency@add')"
|
||||
>新增</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="货币编码" prop="currency_code" min-width="140" />
|
||||
<ElTableColumn label="货币名称" prop="currency_name" min-width="140" />
|
||||
<ElTableColumn label="符号" prop="currency_symbol" width="100" />
|
||||
<ElTableColumn label="当前汇率" prop="exchange_rate" min-width="120" />
|
||||
<ElTableColumn label="允许上浮动%" prop="float_percent_up" min-width="120" />
|
||||
<ElTableColumn label="允许下浮动%" prop="float_percent_down" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" width="240px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDetail(scope.row)" v-if="hasAuth('currency@show')">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('currency@update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="hasAuth('currency@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增货币' : '编辑货币'"
|
||||
align-center
|
||||
width="640px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
|
||||
<ElFormItem label="货币编码" prop="currency_code">
|
||||
<ElInput v-model="form.currency_code" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="货币名称" prop="currency_name">
|
||||
<ElInput v-model="form.currency_name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="货币符号" prop="currency_symbol">
|
||||
<ElInput v-model="form.currency_symbol" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="当前汇率" prop="exchange_rate">
|
||||
<ElInputNumber
|
||||
v-model="form.exchange_rate"
|
||||
:min="0"
|
||||
:step="0.0001"
|
||||
:precision="6"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="允许上浮动%" prop="float_percent_up">
|
||||
<ElInputNumber
|
||||
v-model="form.float_percent_up"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="允许下浮动%" prop="float_percent_down">
|
||||
<ElInputNumber
|
||||
v-model="form.float_percent_down"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog v-model="detailVisible" title="货币详情" align-center width="640px">
|
||||
<div class="detail-dialog">
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div class="label">货币编码</div>
|
||||
<div class="value">{{ detailData.currency_code || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">货币名称</div>
|
||||
<div class="value">{{ detailData.currency_name || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">货币符号</div>
|
||||
<div class="value">{{ detailData.currency_symbol || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">当前汇率</div>
|
||||
<div class="value">{{ detailData.exchange_rate ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">允许上浮动%</div>
|
||||
<div class="value">{{ detailData.float_percent_up ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">允许下浮动%</div>
|
||||
<div class="value">{{ detailData.float_percent_down ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">状态</div>
|
||||
<div class="value">{{ detailData.status ? '启用' : '停用' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">创建时间</div>
|
||||
<div class="value">{{ detailData.created_at || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">更新时间</div>
|
||||
<div class="value">{{ detailData.updated_at || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="detailVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { financeService } from '@/api/financeApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'TypeCurrency' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const rules = reactive<FormRules>({
|
||||
currency_code: [{ required: true, message: '请输入货币编码', trigger: 'blur' }],
|
||||
currency_name: [{ required: true, message: '请输入货币名称', trigger: 'blur' }],
|
||||
exchange_rate: [{ required: true, message: '请输入当前汇率', trigger: 'change' }]
|
||||
})
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
currency_code: '',
|
||||
currency_name: '',
|
||||
currency_symbol: '',
|
||||
exchange_rate: 0,
|
||||
float_percent_up: 0,
|
||||
float_percent_down: 0,
|
||||
status: 1
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
onMounted(() => {
|
||||
getCurrencyList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getCurrencyList()
|
||||
})
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCurrencyList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCurrencyList()
|
||||
}
|
||||
// 获取货币列表
|
||||
const getCurrencyList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const { records, current, size, total } = await financeService.getCurrencyList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
})
|
||||
tableData.value = records.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const dialogType = ref('add')
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
const showDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
currency_code: row.currency_code,
|
||||
currency_name: row.currency_name,
|
||||
currency_symbol: row.currency_symbol,
|
||||
exchange_rate: row.exchange_rate ?? 0,
|
||||
float_percent_up: row.float_percent_up ?? 0,
|
||||
float_percent_down: row.float_percent_down ?? 0,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
currency_code: '',
|
||||
currency_name: '',
|
||||
currency_symbol: '',
|
||||
exchange_rate: 0,
|
||||
float_percent_up: 0,
|
||||
float_percent_down: 0,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleDelete = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await financeService.currencyDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getCurrencyList()
|
||||
})
|
||||
}
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (dialogType.value === 'add') {
|
||||
const { id, ...payload } = form
|
||||
const { message } = await financeService.currencyAdd(payload)
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await financeService.currencyEdit(form)
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getCurrencyList()
|
||||
})
|
||||
}
|
||||
|
||||
const showDetail = async (row: any) => {
|
||||
detailVisible.value = true
|
||||
const res = await financeService.currencyDetail({ id: row.id })
|
||||
detailData.value = res || {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
.detail-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.section {
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,838 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-left">
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-ripple
|
||||
@click="handleCreate"
|
||||
v-if="hasAuth('receipt_records@add')"
|
||||
>
|
||||
创建收款记录
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:key="tableData.length"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="创建日期" prop="created_at" min-width="140" />
|
||||
<!-- <ElTableColumn label="付款来源" prop="country_name" min-width="160" /> -->
|
||||
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
|
||||
<ElTableColumn label="账户" prop="account_short_desc" min-width="120" />
|
||||
<ElTableColumn label="币种" prop="currency_name" min-width="120" />
|
||||
<ElTableColumn label="金额" prop="amount" min-width="120" />
|
||||
<ElTableColumn label="折让金额(含手续费)" prop="discount_amount" min-width="120" />
|
||||
<ElTableColumn label="认领状态" prop="payment_apply_id" min-width="120">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="scope.row.payment_apply_id == 0 || scope.row.payment_apply_id == null"
|
||||
class="text-danger"
|
||||
>
|
||||
未认领
|
||||
</span>
|
||||
<span v-else class="text-success">已认领</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="入账" prop="id" min-width="120">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.received_at">{{ scope.row.received_at }}</span>
|
||||
<el-button
|
||||
v-else-if="hasAuth('receipt_records@confirmentry')"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handlebtn(scope.row)"
|
||||
>
|
||||
确认入账
|
||||
</el-button>
|
||||
<span v-else></span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@show')"
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@update')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleEdit(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 修改
|
||||
</el-link>
|
||||
<!-- <el-link
|
||||
v-if="hasAuth('receipt_records@destroy')"
|
||||
type="danger"
|
||||
underline="never"
|
||||
@click="handleDelete()"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 作废
|
||||
</el-link> -->
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@history')"
|
||||
type="warning"
|
||||
underline="never"
|
||||
@click="handleHistory(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 历史记录
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑收款记录 - 通用弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑收款记录' : '创建收款记录'"
|
||||
width="900px"
|
||||
class="receipt-dialog"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="form"
|
||||
:rules="createRules"
|
||||
label-width="180px"
|
||||
class="receipt-form"
|
||||
>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<!-- <ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="付款来源">
|
||||
<el-select v-model="form.country_id" placeholder="请选择付款国家">
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addrs"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol> -->
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇款人">
|
||||
<el-input v-model="form.remitter" placeholder="请填写汇款人" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="收款账户">
|
||||
<el-select v-model="form.account_id" placeholder="请选择收款账户">
|
||||
<el-option
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.short_desc"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency_id" placeholder="请选择币种">
|
||||
<el-option
|
||||
v-for="item in currencyOptions"
|
||||
:key="item.id"
|
||||
:label="item.currency_name"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="金额">
|
||||
<el-input v-model="form.amount" placeholder="请填写金额" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="到账时间">
|
||||
<el-date-picker
|
||||
v-model="form.arrived_at"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
type="date"
|
||||
placeholder="请选择到账时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<!-- <ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="折让金额">
|
||||
<el-input v-model="form.discount_amount" placeholder="请填写折让金额" />
|
||||
</el-form-item>
|
||||
</ElCol> -->
|
||||
<ElCol :sm="24" :md="12" :lg="12" v-if="isEdit">
|
||||
<el-form-item label="收汇凭证号">
|
||||
<el-input v-model="form.voucher_no" placeholder="请填写收汇凭证号" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<!-- <ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="实际汇率(出纳)">
|
||||
<el-input v-model="form.exchange_rate" placeholder="请填写实际汇率" />
|
||||
</el-form-item>
|
||||
</ElCol> -->
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="总金额(人民币)实际汇率">
|
||||
<el-input
|
||||
v-model="form.total_amount_cny"
|
||||
placeholder="请填写总金额(人民币)实际汇率"
|
||||
/>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="是否转回内地">
|
||||
<el-radio-group v-model="form.is_domestic_return">
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="24" :lg="24">
|
||||
<el-form-item label="外汇到账回单(出纳)">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess1"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
>
|
||||
<img :src="imageUrl1" alt="" class="avatarimg" />
|
||||
<input type="hidden" v-model="form.fx_receipt_file" />
|
||||
</el-upload>
|
||||
<div
|
||||
v-if="Array.isArray(form.fx_receipt_file) && form.fx_receipt_file.length"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(u, i) in form.fx_receipt_file"
|
||||
:key="i"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
|
||||
>
|
||||
<img
|
||||
v-if="
|
||||
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(
|
||||
(u.file_full || '').split('?')[0]
|
||||
)
|
||||
"
|
||||
:src="u.file_full"
|
||||
style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="u.file_full"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ u.file_name }}</a
|
||||
>
|
||||
<el-button link type="danger" @click="removeVoucher(i)" style="width: 80px"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="dialogVisibleshow"
|
||||
title="查看收款记录"
|
||||
width="900px"
|
||||
class="receipt-dialog"
|
||||
>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>创建日期:</label>
|
||||
<span>{{ formshow.created_at }}</span>
|
||||
</div>
|
||||
<!-- <div class="info-item">
|
||||
<label>付款来源:</label>
|
||||
<span>{{ formshow.country_name || '—' }}</span>
|
||||
</div> -->
|
||||
<div class="info-item">
|
||||
<label>汇款人:</label>
|
||||
<span>{{ formshow.remitter || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>账户:</label>
|
||||
<span>{{ formshow.account_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>币种:</label>
|
||||
<span>{{ formshow.currency_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>金额:</label>
|
||||
<span class="amount">{{ formshow.amount }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>到账时间:</label>
|
||||
<span>{{ formshow.arrived_at || '—' }}</span>
|
||||
</div>
|
||||
<!-- <div class="info-item">
|
||||
<label>折让金额:</label>
|
||||
<span>{{ formshow.discount_amount || '—' }}</span>
|
||||
</div> -->
|
||||
<div class="info-item">
|
||||
<label>收汇凭证号:</label>
|
||||
<span>{{ formshow.voucher_no || '—' }}</span>
|
||||
</div>
|
||||
<!-- <div class="info-item">
|
||||
<label>实际汇率(出纳):</label>
|
||||
<span>{{ formshow.exchange_rate || '—' }}</span>
|
||||
</div> -->
|
||||
<div class="info-item">
|
||||
<label>总金额(人民币)实际汇率:</label>
|
||||
<span>{{ formshow.total_amount_cny || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>是否转回内地:</label>
|
||||
<span>{{ formshow.is_domestic_return == 1 ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>外汇到账回单:</label>
|
||||
<div>
|
||||
<p v-for="(item, index) in formshow.fx_receipt_file" :key="index">
|
||||
<el-image
|
||||
v-if="
|
||||
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((item.file_full || '').split('?')[0])
|
||||
"
|
||||
:src="item.file_full"
|
||||
:preview-src-list="item.file_full ? [item.file_full] : []"
|
||||
style="width: 100px; height: 100px"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="item.file_full"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ item.file_name }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 历史记录 -->
|
||||
<el-dialog
|
||||
v-model="timelineDialogVisible"
|
||||
title="历史记录"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<Timeline :records="approvalRecords" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import imageUrl1 from '@/assets/img/crm/upimg.png'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
const Timeline = defineAsyncComponent(() => import('@/components/custom/Timeline.vue'))
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=receipt_records`
|
||||
defineOptions({ name: 'AdminReceiptRecords' })
|
||||
const approvalRecords = ref<any[]>([])
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const dialogVisibleshow = ref(false)
|
||||
const timelineDialogVisible = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
//国家
|
||||
const countryOptions = ref<any[]>([])
|
||||
const fetchCountryOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCountrySelect()
|
||||
res.forEach((item: any) => {
|
||||
item.cn_addrs = item.cn_addr + '(' + item.en_addr + ')'
|
||||
})
|
||||
countryOptions.value = res || []
|
||||
} catch {
|
||||
countryOptions.value = []
|
||||
}
|
||||
}
|
||||
//账户
|
||||
const accountOptions = ref<any[]>([])
|
||||
const fetchAccountOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getAccountSelect()
|
||||
accountOptions.value = res || []
|
||||
} catch {
|
||||
accountOptions.value = []
|
||||
}
|
||||
}
|
||||
//币种
|
||||
const currencyOptions = ref<any[]>([])
|
||||
const fetchCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch {
|
||||
currencyOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 审批弹出框相关
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await financeService.getReceiptList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const tableRowClassName = ({ row }: { row: any }) => {
|
||||
const flag = Number(row?.claim_timeout)
|
||||
return flag === 1 ? 'row-claim-timeout' : ''
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
|
||||
// 新建/编辑弹窗通用状态
|
||||
const dialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentId = ref<string | number | null>(null)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
interface FileInfo {
|
||||
file_name?: string
|
||||
file_full?: string
|
||||
file_src?: string
|
||||
}
|
||||
const form = ref({
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
amount: '',
|
||||
arrived_at: new Date().toISOString().split('T')[0],
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: [] as FileInfo[],
|
||||
fx_receipt_file_list: [] as object[]
|
||||
})
|
||||
const createRules: FormRules = {
|
||||
remitter: [{ required: true, message: '请输入汇款人', trigger: 'blur' }],
|
||||
account_id: [{ required: true, message: '请选择账户', trigger: 'change' }],
|
||||
currency_id: [{ required: true, message: '请选择币种', trigger: 'change' }]
|
||||
}
|
||||
const formshow = ref({
|
||||
created_at: '',
|
||||
country_name: '',
|
||||
remitter: '',
|
||||
account_name: '',
|
||||
currency_name: '',
|
||||
amount: '',
|
||||
arrived_at: '',
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: [] as FileInfo[],
|
||||
fx_receipt_file_full: '',
|
||||
fx_receipt_file_list: [] as object[]
|
||||
})
|
||||
const resetCreateForm = () => {
|
||||
Object.assign(form.value, {
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
amount: '',
|
||||
arrived_at: new Date().toISOString().split('T')[0],
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: [] as FileInfo[],
|
||||
fx_receipt_file_list: []
|
||||
})
|
||||
currentId.value = null
|
||||
createFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
resetCreateForm()
|
||||
isEdit.value = false
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
createFormRef.value?.validate?.(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
createLoading.value = true
|
||||
// TODO: 根据新增/编辑调用对应接口
|
||||
if (isEdit.value) {
|
||||
const formData = { ...form.value }
|
||||
// if ('fx_receipt_file_list' in formData) {
|
||||
// delete formData.fx_receipt_file_list
|
||||
// }
|
||||
const forms = { ...formData, id: currentId.value }
|
||||
const { message } = await financeService.updateReceipt({
|
||||
remitter: forms.remitter,
|
||||
account_id: forms.account_id,
|
||||
currency_id: forms.currency_id,
|
||||
amount: forms.amount,
|
||||
arrived_at: forms.arrived_at,
|
||||
voucher_no: forms.voucher_no,
|
||||
total_amount_cny: forms.total_amount_cny,
|
||||
is_domestic_return: forms.is_domestic_return,
|
||||
fx_receipt_file: forms.fx_receipt_file,
|
||||
id: forms.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const formData = { ...form.value }
|
||||
const { message } = await financeService.addReceipt({
|
||||
remitter: formData.remitter,
|
||||
account_id: formData.account_id,
|
||||
currency_id: formData.currency_id,
|
||||
amount: formData.amount,
|
||||
arrived_at: formData.arrived_at,
|
||||
voucher_no: formData.voucher_no,
|
||||
total_amount_cny: formData.total_amount_cny,
|
||||
is_domestic_return: formData.is_domestic_return,
|
||||
fx_receipt_file: formData.fx_receipt_file
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleView = (row?: any) => {
|
||||
// const fx_receipt_file_full = []
|
||||
// if (row.fx_receipt_file_full) {
|
||||
// fx_receipt_file_full.push(row.fx_receipt_file_full)
|
||||
// }
|
||||
formshow.value = row
|
||||
// formshow.value.fx_receipt_file_list = fx_receipt_file_full
|
||||
dialogVisibleshow.value = true
|
||||
}
|
||||
const handleEdit = (row?: any) => {
|
||||
resetCreateForm()
|
||||
isEdit.value = true
|
||||
currentId.value = row?.id ?? null
|
||||
if (row && typeof row === 'object') {
|
||||
// form.value.country_id = row.country_id ?? form.value.country_id
|
||||
form.value.remitter = row.remitter ?? form.value.remitter
|
||||
form.value.account_id = row.account_id ?? form.value.account_id
|
||||
form.value.currency_id = row.currency_id ?? form.value.currency_id
|
||||
form.value.amount = row.amount ?? form.value.amount
|
||||
form.value.arrived_at = row.arrived_at ?? form.value.arrived_at
|
||||
// form.value.discount_amount = row.discount_amount ?? form.value.discount_amount
|
||||
form.value.voucher_no = row.voucher_no ?? form.value.voucher_no
|
||||
form.value.total_amount_cny = row.total_amount_cny ?? form.value.total_amount_cny
|
||||
// form.value.exchange_rate = row.exchange_rate ?? form.value.exchange_rate
|
||||
form.value.is_domestic_return = row.is_domestic_return ?? form.value.is_domestic_return
|
||||
form.value.fx_receipt_file = row.fx_receipt_file ?? form.value.fx_receipt_file
|
||||
form.value.fx_receipt_file_list = row.fx_receipt_file ?? form.value.fx_receipt_file
|
||||
|
||||
// const fx_receipt_file_full = []
|
||||
// if (row.fx_receipt_file_full) {
|
||||
// fx_receipt_file_full.push(row.fx_receipt_file_full)
|
||||
// }
|
||||
// form.value.fx_receipt_file_list = Array.isArray(fx_receipt_file_full)
|
||||
// ? fx_receipt_file_full
|
||||
// : form.value.fx_receipt_file_list
|
||||
}
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handlebtn = (row?: any) => {
|
||||
ElMessageBox.prompt('你是否确认入账?确认入账请填写入账实时汇率!', '入账', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^(?!\s*$).+/,
|
||||
inputErrorMessage: '请填写入账实时汇率'
|
||||
}).then(({ value }) => {
|
||||
console.log(value)
|
||||
financeService.confirmReceipt({ id: row.id, exchange_rate: value }).then((res) => {
|
||||
ElMessage.success(res.message)
|
||||
getList()
|
||||
})
|
||||
})
|
||||
}
|
||||
// const handleDelete = () => {
|
||||
// ElMessageBox.confirm('确定删除吗?', '提示', {
|
||||
// confirmButtonText: '确定',
|
||||
// cancelButtonText: '取消',
|
||||
// type: 'warning'
|
||||
// }).then(() => {
|
||||
// financeService.deleteReceipt({ id: currentId.value }).then((res) => {
|
||||
// ElMessage.success(res.message)
|
||||
// getList()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
const handleHistory = (row: any) => {
|
||||
financeService.getReceiptHistory({ receipt_id: row.id }).then((res) => {
|
||||
timelineDialogVisible.value = true
|
||||
approvalRecords.value = res.records || []
|
||||
// ElMessage.success(res.message)
|
||||
// getList()
|
||||
})
|
||||
|
||||
// history.push('/admin/finance/receipt_records_history')
|
||||
}
|
||||
// function handleUploadSuccess1(response: any) {
|
||||
// const fileName = response?.data?.file_name || ''
|
||||
// const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
|
||||
// form.value.fx_receipt_file = [form.value.fx_receipt_file, fileName].filter(Boolean).join(',')
|
||||
// const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
// ? form.value.fx_receipt_file_list
|
||||
// : []
|
||||
// list.push(fileUrl)
|
||||
// form.value.fx_receipt_file_list = [...list]
|
||||
// ElMessage.success(response.message)
|
||||
// }
|
||||
function handleUploadSuccess1(response: any) {
|
||||
const fileName = response?.data?.file_name || ''
|
||||
const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
|
||||
const names = Array.isArray(form.value.fx_receipt_file) ? [...form.value.fx_receipt_file] : []
|
||||
const files = ref({
|
||||
file_src: '',
|
||||
file_name: '',
|
||||
file_full: ''
|
||||
})
|
||||
files.value.file_src = fileName
|
||||
files.value.file_name = response?.data?.original_name || ''
|
||||
files.value.file_full = response?.data?.file_url || ''
|
||||
if (fileName) names.push(files.value)
|
||||
form.value.fx_receipt_file = names
|
||||
const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
? form.value.fx_receipt_file_list
|
||||
: []
|
||||
list.push(fileUrl)
|
||||
form.value.fx_receipt_file_list = [...list]
|
||||
ElMessage.success(response.message)
|
||||
}
|
||||
// 预览项移除(同步预览数组与提交字段)
|
||||
function removeVoucher(index: number) {
|
||||
// const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
// ? [...form.value.fx_receipt_file_list]
|
||||
// : []
|
||||
// if (index < 0 || index >= list.length) return
|
||||
// list.splice(index, 1)
|
||||
// form.value.fx_receipt_file_list = list
|
||||
// const names = (form.value.fx_receipt_file || '').split(',').filter(Boolean)
|
||||
// if (index < names.length) names.splice(index, 1)
|
||||
// form.value.fx_receipt_file = names.join(',')
|
||||
|
||||
const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
? [...form.value.fx_receipt_file_list]
|
||||
: []
|
||||
if (index < 0 || index >= list.length) return
|
||||
list.splice(index, 1)
|
||||
form.value.fx_receipt_file_list = list
|
||||
const names = Array.isArray(form.value.fx_receipt_file) ? [...form.value.fx_receipt_file] : []
|
||||
if (index < names.length) names.splice(index, 1)
|
||||
form.value.fx_receipt_file = names
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.row-claim-timeout) {
|
||||
background-color: #ffc9c9 !important;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
// 创建收款记录 - 弹窗样式对齐设计图
|
||||
.receipt-dialog {
|
||||
.receipt-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: flex-end;
|
||||
// border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatarimg {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// padding: 8px 0;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.amount {
|
||||
font-weight: 600;
|
||||
color: #3d7bff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.receipt-dialog .el-dialog__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -68,7 +68,7 @@
|
||||
</el-form>
|
||||
</section>
|
||||
<div class="table">
|
||||
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry#store')"><i class="iconfont-sys"></i> 录单</el-button>
|
||||
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry@store')"><i class="iconfont-sys"></i> 录单</el-button>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
@@ -121,7 +121,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="recipient" label="跟单人">
|
||||
<template #default="scope">
|
||||
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry#reallocation')" @click="onFenpei(scope.row,'edit')">
|
||||
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry@reallocation')" @click="onFenpei(scope.row,'edit')">
|
||||
{{scope.row.recipientInfo? scope.row.recipientInfo.name : ''}}
|
||||
<i class="iconfont-sys"></i>
|
||||
</span>
|
||||
@@ -147,31 +147,31 @@
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="260px">
|
||||
<template #default="scope">
|
||||
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry#allocation')">
|
||||
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry@allocation')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
分配
|
||||
</div>
|
||||
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry#show')">
|
||||
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry@show')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
详情
|
||||
</div>
|
||||
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry#release')">
|
||||
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry@release')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
发布
|
||||
</div>
|
||||
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry#update')">
|
||||
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry@update')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
编辑
|
||||
</div>
|
||||
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry#destroy')">
|
||||
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry@destroy')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
@@ -349,6 +349,7 @@
|
||||
import { CrmService } from '@/api/crmApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
defineOptions({ name: 'InquirySystem' })
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const dialogType = ref('add')
|
||||
|
||||