This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
|
||||
- 更新订单管理中的“创建订单”弹窗支付凭证上传区:
|
||||
- 支持拖拽图片上传
|
||||
- 保留现有单文件限制、图片类型校验、5MB 大小校验、异步上传和上传中禁提交流程
|
||||
- 保留现有单文件限制、图片类型校验、异步上传和上传中禁提交流程
|
||||
- 不再做前端文件大小限制
|
||||
- 更新资产信息中的“创建充值订单”弹窗支付凭证上传区:
|
||||
- 支持拖拽图片上传
|
||||
- 保留现有单文件限制、图片类型校验、5MB 大小校验、异步上传和上传中禁提交流程
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Requirement: Order Voucher Drag Upload
|
||||
|
||||
The system SHALL support drag-and-drop image selection for payment vouchers in the admin order creation dialog.
|
||||
The system SHALL support drag-and-drop selection of any browser-recognized image format for payment vouchers in the admin order creation dialog.
|
||||
|
||||
#### Scenario: Drag image into order voucher upload area
|
||||
|
||||
@@ -17,4 +17,11 @@ The system SHALL support drag-and-drop image selection for payment vouchers in t
|
||||
- **GIVEN** 用户正在后台“订单管理”中创建线下支付订单
|
||||
- **WHEN** 用户点击支付凭证上传区选择一张符合要求的图片
|
||||
- **THEN** 系统 MUST 继续支持原有点击选择行为
|
||||
- **AND** 系统 MUST NOT 因新增拖拽能力破坏原有单文件限制、类型校验和大小校验
|
||||
- **AND** 系统 MUST NOT 因新增拖拽能力破坏原有单文件限制和类型校验
|
||||
|
||||
#### Scenario: Upload any image format order payment voucher without frontend size limit
|
||||
|
||||
- **GIVEN** 用户正在后台“订单管理”中创建线下支付订单
|
||||
- **WHEN** 用户选择或拖入任意浏览器识别为图片类型的支付凭证
|
||||
- **THEN** 系统 MUST NOT reject the file based on frontend file size checks
|
||||
- **AND** 系统 MUST accept the file when its MIME type starts with `image/`
|
||||
|
||||
21
openspec/changes/update-refund-order-voucher/proposal.md
Normal file
21
openspec/changes/update-refund-order-voucher/proposal.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Change: Update Refund Order Voucher Requirements
|
||||
|
||||
## Why
|
||||
|
||||
退款管理的创建订单流程需要记录退款凭证图片,便于财务审核和后续追溯。用户也需要在退款订单列表和详情中像订单列表的支付凭证一样直接查看退款凭证。
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add required `refund_voucher_key` to refund order creation payload, sourced from `/storage/upload-url` image upload result.
|
||||
- Add a "查看退款凭证" action in refund order list operations when the order has `refund_voucher_key`.
|
||||
- Display refund voucher directly in refund order details when `refund_voucher_key` exists.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `order-management`
|
||||
- Affected code:
|
||||
- Refund management create order dialog/form and API payload typing
|
||||
- Refund order list action column
|
||||
- Refund order detail view
|
||||
- Shared voucher preview/upload utilities if reused from order payment voucher flow
|
||||
- Breaking changes: Existing refund order creation now requires `refund_voucher_key`.
|
||||
@@ -0,0 +1,45 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Refund Order Voucher Upload
|
||||
|
||||
The system SHALL require a refund voucher file key when creating a refund order in refund management.
|
||||
|
||||
#### Scenario: Create refund order with uploaded voucher
|
||||
|
||||
- **GIVEN** 用户正在退款管理中创建退款订单
|
||||
- **WHEN** 用户通过 `/storage/upload-url` 上传退款凭证图片并获得 `file_key`
|
||||
- **AND** 用户提交创建退款订单表单
|
||||
- **THEN** 系统 MUST 将该 `file_key` 作为 `refund_voucher_key` 提交
|
||||
- **AND** `refund_voucher_key` MUST be a non-empty string
|
||||
|
||||
#### Scenario: Block refund order creation without voucher
|
||||
|
||||
- **GIVEN** 用户正在退款管理中创建退款订单
|
||||
- **WHEN** 用户未上传退款凭证图片或未获得 `file_key`
|
||||
- **AND** 用户尝试提交创建退款订单表单
|
||||
- **THEN** 系统 MUST prevent submission
|
||||
- **AND** 系统 MUST 提示用户上传退款凭证
|
||||
|
||||
### Requirement: Refund Voucher Viewing
|
||||
|
||||
The system SHALL allow users to view refund vouchers from refund order list operations and refund order details when `refund_voucher_key` exists.
|
||||
|
||||
#### Scenario: View refund voucher from refund order list action
|
||||
|
||||
- **GIVEN** 用户正在查看退款订单列表
|
||||
- **WHEN** 某条退款订单存在 `refund_voucher_key`
|
||||
- **THEN** 系统 MUST 在操作列提供查看退款凭证入口
|
||||
- **AND** 该入口的查看体验 MUST be consistent with the existing order list payment voucher preview behavior
|
||||
|
||||
#### Scenario: Hide refund voucher action when no voucher exists
|
||||
|
||||
- **GIVEN** 用户正在查看退款订单列表
|
||||
- **WHEN** 某条退款订单不存在 `refund_voucher_key`
|
||||
- **THEN** 系统 MUST NOT display an enabled refund voucher preview action for that order
|
||||
|
||||
#### Scenario: Display refund voucher in refund order detail
|
||||
|
||||
- **GIVEN** 用户打开退款订单详情
|
||||
- **WHEN** 详情数据包含 `refund_voucher_key`
|
||||
- **THEN** 系统 MUST directly display the refund voucher image or preview entry in the detail content
|
||||
- **AND** 用户 MUST NOT need to return to the list to view the voucher
|
||||
9
openspec/changes/update-refund-order-voucher/tasks.md
Normal file
9
openspec/changes/update-refund-order-voucher/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Update refund order creation request type to include required `refund_voucher_key: string`.
|
||||
- [x] 1.2 Add refund voucher image upload to the refund order creation form using `/storage/upload-url`, storing the returned file key.
|
||||
- [x] 1.3 Validate that refund voucher is uploaded before allowing refund order creation.
|
||||
- [x] 1.4 Submit `refund_voucher_key` in the create refund order payload.
|
||||
- [x] 1.5 Add a list operation to view the refund voucher when `refund_voucher_key` exists, matching the existing order list payment voucher behavior.
|
||||
- [x] 1.6 Show the refund voucher directly in refund order details when `refund_voucher_key` exists.
|
||||
- [x] 1.7 Verify create, list preview, and detail display flows manually or with existing tests.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Change: Update Refund Voucher Multi Image Behavior
|
||||
|
||||
## Why
|
||||
|
||||
退款管理需要更清晰地展示退款凭证,并支持一次退款记录关联多张凭证图片,便于财务核验。创建退款申请选择订单时也应只允许选择已支付订单,避免对未支付订单发起退款。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 退款详情中直接展示退款凭证图片,点击图片后可放大预览。
|
||||
- 创建退款申请的订单号搜索/选择请求必须增加 `payment_status: 2` 条件。
|
||||
- 退款管理凭证上传支持多张图片,并将所有上传后的文件 key 以英文逗号拼接后放入 `refund_voucher_key` 参数。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `order-management`
|
||||
- Affected code:
|
||||
- Refund detail voucher display and image preview behavior
|
||||
- Create refund request order selector query params
|
||||
- Refund management voucher upload component and submit payload
|
||||
- Breaking changes: Refund voucher payload field for this flow remains `refund_voucher_key` and supports one or more uploaded image keys separated by English commas.
|
||||
@@ -0,0 +1,109 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Refund Detail Inline Voucher Preview
|
||||
|
||||
The system SHALL display refund voucher images directly in the refund detail page when voucher file keys exist, and each displayed image SHALL support click-to-enlarge preview. Refund voucher viewing from the refund list SHALL be controlled by the permission code `refund:view_voucher`.
|
||||
|
||||
#### Scenario: Display and enlarge refund voucher in detail
|
||||
|
||||
- **GIVEN** 用户打开退款详情页
|
||||
- **AND** 退款详情数据包含一个或多个退款凭证文件 key
|
||||
- **WHEN** 页面渲染退款凭证区域
|
||||
- **THEN** 系统 MUST 直接展示对应的凭证图片
|
||||
- **AND** 用户点击任一凭证图片后,系统 MUST 放大预览该图片
|
||||
|
||||
#### Scenario: Hide voucher images when no voucher exists
|
||||
|
||||
- **GIVEN** 用户打开退款详情页
|
||||
- **WHEN** 退款详情数据不包含退款凭证文件 key
|
||||
- **THEN** 系统 MUST NOT 展示空的凭证图片占位
|
||||
- **AND** 系统 MAY display `-` or equivalent empty-state text for the voucher field
|
||||
|
||||
#### Scenario: Show refund voucher list action with permission
|
||||
|
||||
- **GIVEN** 用户正在查看退款管理列表
|
||||
- **AND** 某条退款记录存在 `refund_voucher_key`
|
||||
- **AND** 当前用户拥有 `refund:view_voucher` 权限编码
|
||||
- **WHEN** 系统渲染该行操作列
|
||||
- **THEN** 系统 MUST show the `查看退款凭证` action
|
||||
|
||||
#### Scenario: Hide refund voucher list action without permission
|
||||
|
||||
- **GIVEN** 用户正在查看退款管理列表
|
||||
- **AND** 某条退款记录存在 `refund_voucher_key`
|
||||
- **AND** 当前用户不拥有 `refund:view_voucher` 权限编码
|
||||
- **WHEN** 系统渲染该行操作列
|
||||
- **THEN** 系统 MUST NOT show the `查看退款凭证` action
|
||||
|
||||
### Requirement: Refund Application Paid Order Filtering
|
||||
|
||||
The system SHALL restrict the order number search/select request in create refund application to paid orders by including `payment_status: 2` in the query parameters.
|
||||
|
||||
#### Scenario: Search selectable orders for refund application
|
||||
|
||||
- **GIVEN** 用户正在创建退款申请
|
||||
- **WHEN** 用户通过订单号搜索/选择订单
|
||||
- **THEN** 系统 MUST call the order search/list API with `payment_status: 2`
|
||||
- **AND** 未支付、已取消或其他非已支付状态订单 MUST NOT be returned as selectable refund orders by the frontend query
|
||||
|
||||
### Requirement: Refund Application Zero Amount
|
||||
|
||||
The system SHALL allow the requested refund amount in create refund application to be `0`, while still preventing negative amounts and amounts greater than the actual received amount.
|
||||
|
||||
#### Scenario: Submit zero requested refund amount
|
||||
|
||||
- **GIVEN** 用户正在创建退款申请
|
||||
- **AND** 用户已选择可退款订单
|
||||
- **WHEN** 用户输入申请退款金额 `0`
|
||||
- **THEN** 系统 MUST allow the amount validation to pass
|
||||
- **AND** 系统 MUST submit `requested_refund_amount` as `0`
|
||||
|
||||
#### Scenario: Block negative requested refund amount
|
||||
|
||||
- **GIVEN** 用户正在创建退款申请
|
||||
- **WHEN** 用户输入小于 `0` 的申请退款金额
|
||||
- **THEN** 系统 MUST prevent submission
|
||||
- **AND** 系统 MUST 提示申请退款金额不能小于 `0`
|
||||
|
||||
#### Scenario: Block requested refund amount above actual received amount
|
||||
|
||||
- **GIVEN** 用户正在创建退款申请
|
||||
- **AND** 已选订单存在实收金额
|
||||
- **WHEN** 用户输入的申请退款金额大于实收金额
|
||||
- **THEN** 系统 MUST prevent submission
|
||||
- **AND** 系统 MUST 提示申请退款金额不能大于实收金额
|
||||
|
||||
### Requirement: Refund Management Multi Image Voucher Upload
|
||||
|
||||
The system SHALL allow multiple refund voucher images in any browser-recognized image format to be uploaded in refund management and SHALL submit all uploaded file keys in the `refund_voucher_key` parameter as a comma-separated string.
|
||||
|
||||
#### Scenario: Submit multiple uploaded voucher images
|
||||
|
||||
- **GIVEN** 用户正在退款管理中创建或提交退款相关记录
|
||||
- **AND** 用户上传了多张退款凭证图片
|
||||
- **WHEN** 每张图片上传成功并返回文件 key
|
||||
- **AND** 用户提交表单
|
||||
- **THEN** 系统 MUST join the uploaded file keys with English commas
|
||||
- **AND** 系统 MUST put the joined string into `refund_voucher_key`
|
||||
- **AND** `refund_voucher_key` MUST look like `attachments/a.jpg,attachments/b.jpg` when multiple files exist
|
||||
|
||||
#### Scenario: Upload any image format refund voucher without frontend size limit
|
||||
|
||||
- **GIVEN** 用户正在创建退款申请
|
||||
- **WHEN** 用户选择或拖入任意浏览器识别为图片类型的退款凭证
|
||||
- **THEN** 系统 MUST NOT reject the file based on frontend file size checks
|
||||
- **AND** 系统 MUST accept the file when its MIME type starts with `image/`
|
||||
|
||||
#### Scenario: Submit single uploaded voucher image
|
||||
|
||||
- **GIVEN** 用户正在退款管理中创建或提交退款相关记录
|
||||
- **AND** 用户只上传了一张退款凭证图片
|
||||
- **WHEN** 用户提交表单
|
||||
- **THEN** 系统 MUST put that single file key into `refund_voucher_key` without a trailing comma
|
||||
|
||||
#### Scenario: Block submission without required voucher image
|
||||
|
||||
- **GIVEN** 退款管理表单要求上传退款凭证
|
||||
- **WHEN** 用户未上传任何凭证图片并尝试提交
|
||||
- **THEN** 系统 MUST prevent submission
|
||||
- **AND** 系统 MUST 提示用户上传退款凭证
|
||||
10
openspec/changes/update-refund-voucher-multi-image/tasks.md
Normal file
10
openspec/changes/update-refund-voucher-multi-image/tasks.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Update refund detail to render voucher images inline when voucher keys exist.
|
||||
- [x] 1.2 Add click-to-preview/enlarge behavior for refund detail voucher images.
|
||||
- [x] 1.3 Add `payment_status: 2` to the order number search/select request used by create refund application.
|
||||
- [x] 1.4 Update refund management voucher upload to allow multiple image uploads.
|
||||
- [x] 1.5 Submit uploaded voucher file keys joined by English commas in `refund_voucher_key`.
|
||||
- [x] 1.6 Validate that at least one voucher image is uploaded before submission when voucher is required.
|
||||
- [x] 1.7 Verify refund detail display, order selector filtering, and multi-image payload behavior.
|
||||
- [x] 1.8 Allow requested refund amount `0` while blocking negative and over-actual amounts.
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="30%" @closed="handleDialogClosed">
|
||||
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
@@ -26,7 +26,7 @@
|
||||
<ElFormItem label="申请退款金额" prop="requested_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="formData.requested_refund_amount"
|
||||
:min="0.01"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
@@ -46,12 +46,36 @@
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
class="refund-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
multiple
|
||||
>
|
||||
<ElIcon class="refund-voucher-upload__icon"><UploadFilled /></ElIcon>
|
||||
<div class="refund-voucher-upload__text">将退款凭证拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持所有图片格式,可上传多张</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认创建
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,10 +83,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { RefundService, OrderService, StorageService } from '@/api/modules'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
|
||||
@@ -84,26 +109,32 @@
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const submitLoading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const voucherUploadingCount = ref(0)
|
||||
const orderSearchLoading = ref(false)
|
||||
const orderSearchOptions = ref<Order[]>([])
|
||||
let activeVoucherUploadBatch = 0
|
||||
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount: number
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string
|
||||
}>({
|
||||
order_id: null,
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: 0,
|
||||
refund_reason: ''
|
||||
refund_reason: '',
|
||||
refund_voucher_key: undefined
|
||||
})
|
||||
|
||||
const validateRefundAmount = (rule: any, value: number, callback: any) => {
|
||||
const requestedRefundAmount = yuanToFen(value) ?? 0
|
||||
if (value === undefined || value === null || value <= 0) {
|
||||
callback(new Error('申请退款金额必须大于0'))
|
||||
if (value === undefined || value === null || value < 0) {
|
||||
callback(new Error('申请退款金额不能小于0'))
|
||||
} else if (requestedRefundAmount > formData.actual_received_amount) {
|
||||
callback(new Error('申请退款金额不能大于实收金额'))
|
||||
} else {
|
||||
@@ -116,7 +147,8 @@
|
||||
requested_refund_amount: [
|
||||
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
||||
{ validator: validateRefundAmount, trigger: 'blur' }
|
||||
]
|
||||
],
|
||||
refund_voucher_key: [{ required: true, message: '请上传退款凭证', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
||||
@@ -132,7 +164,8 @@
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 20
|
||||
page_size: 20,
|
||||
payment_status: 2
|
||||
}
|
||||
if (query) {
|
||||
params.order_no = query
|
||||
@@ -170,15 +203,104 @@
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
activeVoucherUploadBatch += 1
|
||||
voucherUploadingCount.value = 0
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.refund_voucher_key = undefined
|
||||
voucherFileKeyMap.clear()
|
||||
removedVoucherUploadUids.clear()
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const removedVoucherUploadUids = new Set<number>()
|
||||
|
||||
const refreshPaymentVoucherKey = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
formData.refund_voucher_key = keys.length ? keys.join(',') : undefined
|
||||
}
|
||||
|
||||
const removeUploadFile = (uploadFile: UploadFile) => {
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const uploadBatch = activeVoucherUploadBatch
|
||||
removedVoucherUploadUids.delete(uploadFile.uid)
|
||||
voucherUploadingCount.value += 1
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
|
||||
try {
|
||||
ElMessage.info('正在上传退款凭证...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (
|
||||
uploadBatch !== activeVoucherUploadBatch ||
|
||||
removedVoucherUploadUids.has(uploadFile.uid)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
refreshPaymentVoucherKey()
|
||||
ElMessage.success('上传成功')
|
||||
await nextTick(() => {
|
||||
formRef.value?.validateField('refund_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeVoucherUploadBatch) return
|
||||
console.error('上传退款凭证失败:', error)
|
||||
ElMessage.error(error.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeVoucherUploadBatch) {
|
||||
voucherUploadingCount.value = Math.max(0, voucherUploadingCount.value - 1)
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveVoucher = (uploadFile: UploadFile) => {
|
||||
removedVoucherUploadUids.add(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('退款凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
@@ -188,6 +310,7 @@
|
||||
order_id: formData.order_id!,
|
||||
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
||||
actual_received_amount: formData.actual_received_amount,
|
||||
refund_voucher_key: formData.refund_voucher_key!,
|
||||
refund_reason: formData.refund_reason || undefined
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
@@ -213,3 +336,35 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ElImageViewer
|
||||
v-if="visible"
|
||||
:url-list="[url]"
|
||||
:url-list="urls"
|
||||
:initial-index="0"
|
||||
hide-on-click-modal
|
||||
@close="handleClose"
|
||||
@@ -24,24 +24,32 @@
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const url = ref('')
|
||||
const urls = ref<string[]>([])
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
url.value = ''
|
||||
urls.value = []
|
||||
visible.value = false
|
||||
const fileKeys = val
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: [val]
|
||||
file_keys: fileKeys
|
||||
})
|
||||
if (res.code === 0 && res.data?.urls?.[val]) {
|
||||
url.value = res.data.urls[val]
|
||||
visible.value = true
|
||||
if (res.code === 0) {
|
||||
urls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
||||
visible.value = urls.value.length > 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取支付凭证失败:', error)
|
||||
}
|
||||
} else {
|
||||
visible.value = false
|
||||
urls.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -49,7 +57,7 @@
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
url.value = ''
|
||||
urls.value = []
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface Refund {
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
status: RefundStatus
|
||||
refund_reason: string
|
||||
refund_voucher_key?: string // 退款凭证 file_key,多个以英文逗号分隔
|
||||
reject_reason: string
|
||||
remark: string
|
||||
asset_reset: boolean
|
||||
@@ -62,6 +63,7 @@ export interface CreateRefundRequest {
|
||||
order_id: number
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
refund_voucher_key: string // 退款凭证对象存储 file_key,多个以英文逗号分隔
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElImageViewer
|
||||
v-if="voucherPreviewVisible"
|
||||
:url-list="refundVoucherUrls"
|
||||
:initial-index="voucherPreviewIndex"
|
||||
hide-on-click-modal
|
||||
@close="voucherPreviewVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
v-model="approveDialogVisible"
|
||||
@@ -162,13 +170,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, h } from 'vue'
|
||||
import { ref, onMounted, computed, h, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||
import { ElCard, ElImageViewer, ElIcon, ElMessage, ElTag } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import { RefundService, StorageService } from '@/api/modules'
|
||||
import type {
|
||||
Refund,
|
||||
RefundStatus,
|
||||
@@ -177,7 +185,6 @@
|
||||
ResubmitRefundRequest
|
||||
} from '@/types/api'
|
||||
import { formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'RefundDetail' })
|
||||
|
||||
@@ -186,6 +193,9 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
const refundVoucherUrls = ref<string[]>([])
|
||||
const voucherPreviewVisible = ref(false)
|
||||
const voucherPreviewIndex = ref(0)
|
||||
|
||||
const pageTitle = computed(() => `退款详情`)
|
||||
|
||||
@@ -250,6 +260,14 @@
|
||||
return statusMap[status] || '-'
|
||||
}
|
||||
|
||||
const getVoucherKeys = (data: Refund): string[] => {
|
||||
const voucherKey = data.refund_voucher_key || ''
|
||||
return voucherKey
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
{
|
||||
title: '退款信息',
|
||||
@@ -287,6 +305,24 @@
|
||||
render: (data) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status))
|
||||
},
|
||||
{
|
||||
label: '退款凭证',
|
||||
fullWidth: true,
|
||||
render: () =>
|
||||
refundVoucherUrls.value.length
|
||||
? h(
|
||||
'div',
|
||||
{ class: 'refund-voucher-list' },
|
||||
refundVoucherUrls.value.map((url, index) =>
|
||||
h('img', {
|
||||
class: 'refund-voucher-image',
|
||||
src: url,
|
||||
onClick: () => handlePreviewRefundVoucher(index)
|
||||
})
|
||||
)
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '资产重置',
|
||||
render: (data) =>
|
||||
@@ -354,6 +390,7 @@
|
||||
const res = await RefundService.getRefundById(id)
|
||||
if (res.code === 0) {
|
||||
refund.value = res.data
|
||||
await loadRefundVoucherUrls(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -367,6 +404,26 @@
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadRefundVoucherUrls = async (data: Refund) => {
|
||||
const fileKeys = getVoucherKeys(data)
|
||||
refundVoucherUrls.value = []
|
||||
if (!fileKeys.length) return
|
||||
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({ file_keys: fileKeys })
|
||||
if (res.code === 0) {
|
||||
refundVoucherUrls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取退款凭证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviewRefundVoucher = (index: number) => {
|
||||
voucherPreviewIndex.value = index
|
||||
voucherPreviewVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
@@ -395,7 +452,8 @@
|
||||
const handleApproveRefund = async () => {
|
||||
if (!approveFormRef.value || !refund.value) return
|
||||
|
||||
await approveFormRef.value.validate(async (valid) => {
|
||||
const refundId = refund.value.id
|
||||
await approveFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
approveLoading.value = true
|
||||
try {
|
||||
@@ -404,10 +462,10 @@
|
||||
remark: approveForm.remark || undefined
|
||||
}
|
||||
|
||||
await RefundService.approveRefund(refund.value.id, data)
|
||||
await RefundService.approveRefund(refundId, data)
|
||||
ElMessage.success('审批通过成功')
|
||||
approveDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
await fetchRefundDetail(refundId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -420,14 +478,15 @@
|
||||
const handleRejectRefund = async () => {
|
||||
if (!rejectFormRef.value || !refund.value) return
|
||||
|
||||
await rejectFormRef.value.validate(async (valid) => {
|
||||
const refundId = refund.value.id
|
||||
await rejectFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
rejectLoading.value = true
|
||||
try {
|
||||
await RefundService.rejectRefund(refund.value.id, rejectForm)
|
||||
await RefundService.rejectRefund(refundId, rejectForm)
|
||||
ElMessage.success('审批拒绝成功')
|
||||
rejectDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
await fetchRefundDetail(refundId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -440,7 +499,8 @@
|
||||
const handleResubmitRefund = async () => {
|
||||
if (!resubmitFormRef.value || !refund.value) return
|
||||
|
||||
await resubmitFormRef.value.validate(async (valid) => {
|
||||
const refundId = refund.value.id
|
||||
await resubmitFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
resubmitLoading.value = true
|
||||
try {
|
||||
@@ -450,10 +510,10 @@
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refund.value.id, data)
|
||||
await RefundService.resubmitRefund(refundId, data)
|
||||
ElMessage.success('重新提交成功')
|
||||
resubmitDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
await fetchRefundDetail(refundId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -495,5 +555,21 @@
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.refund-voucher-list) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.refund-voucher-image) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
cursor: zoom-in;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="220"
|
||||
:inlineActionsCount="3"
|
||||
:inlineActionsCount="2"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -48,6 +48,9 @@
|
||||
<!-- 创建退款申请对话框 -->
|
||||
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
|
||||
|
||||
<!-- 退款凭证预览 -->
|
||||
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
v-model="approveDialogVisible"
|
||||
@@ -248,6 +251,7 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
@@ -269,6 +273,7 @@
|
||||
const returnDialogVisible = ref(false)
|
||||
const resubmitDialogVisible = ref(false)
|
||||
const currentRefund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKey = ref('')
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: RefundQueryParams = {
|
||||
@@ -364,7 +369,6 @@
|
||||
{ label: '审批时间', prop: 'processed_at' }
|
||||
]
|
||||
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const approveFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const returnFormRef = ref<FormInstance>()
|
||||
@@ -856,6 +860,7 @@
|
||||
// 获取操作按钮
|
||||
const getActions = (row: Refund) => {
|
||||
const actions: any[] = []
|
||||
const voucherKey = row.refund_voucher_key
|
||||
|
||||
// 待审批状态可以打回重填、审批通过或审批拒绝
|
||||
if (row.status === 1) {
|
||||
@@ -894,8 +899,22 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (voucherKey && hasAuth('refund:view_voucher')) {
|
||||
actions.push({
|
||||
label: '查看退款凭证',
|
||||
handler: () => handleViewRefundVoucher(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
const handleViewRefundVoucher = (row: Refund) => {
|
||||
const voucherKey = row.refund_voucher_key
|
||||
if (!voucherKey) return
|
||||
refundVoucherFileKey.value = voucherKey
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
将支付凭证拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 jpg、png 格式,文件大小不超过 5MB</div>
|
||||
<div class="el-upload__tip">支持所有图片格式</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
@@ -534,14 +534,6 @@
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!')
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++activeVoucherUploadId
|
||||
voucherUploading.value = true
|
||||
createForm.payment_voucher_key = undefined
|
||||
|
||||
Reference in New Issue
Block a user