feat: 退款企微审批详情与业务处理状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m2s

This commit is contained in:
luo
2026-07-24 16:30:58 +08:00
parent 9a84cd0d31
commit 4c6d871c35
10 changed files with 552 additions and 620 deletions

View File

@@ -0,0 +1,64 @@
## Context
退款申请由后台提交业务资料,审批过程在企微完成,后台只负责展示企微审批信息以及订单退款的业务处理结果。当前页面仍按本地审批模型展示通过、拒绝和退回操作,需要调整为只读状态展示并保留受限的重新申请能力。
## Goals / Non-Goals
- Goals: 让退款创建、列表和详情与企微审批及业务处理状态保持一致。
- Goals: 展示可追溯的审批单号、参与人、意见、附件和时间线。
- Goals: 明确区分审批状态与业务处理状态,并覆盖处理失败和自动重试提示。
- Goals: 允许符合条件的退款重新申请,同时锁定订单、资产和提交人快照。
- Non-Goals: 不在前端实现企微审批动作、审批回调或审批引擎。
- Non-Goals: 不在后台执行非代理钱包的人工退款。
- Non-Goals: 不根据本地旧状态推断企微审批状态或处理状态。
## Decisions
- Decision: 详情接口返回的 `approval` 对象作为企微审批信息的唯一数据源。
- Rationale: 审批动作在企微完成,前端不应通过本地状态或额外请求重建审批过程。
- Decision: `processing_status` 与审批状态独立展示,并使用后端返回的状态名称、失败摘要和业务处理结果。
- Rationale: “审批通过”不等于退款处理完成,两个状态必须避免混用。
- Decision: 创建和重新申请的附件统一使用 `{ file_key, file_name, file_size }[]`
- Rationale: 页面既要提交对象存储文件标识,也要在审批详情中保留可读的文件元数据。
- Decision: 重新申请资格由后端最终校验,前端按已驳回、已撤销或已删除状态控制入口并处理接口拒绝。
- Rationale: 状态可能被并发更新,前端展示控制不能替代服务端业务校验。
- Decision: 移除本地通过、驳回、退回和人工退款确认入口及调用。
- Rationale: 这些操作与企微审批职责冲突,继续保留会造成重复审批或错误退款。
## Data Contract
- Create request: refund amount, reason, remark and `attachments`.
- Detail response: refund business data, `approval` object and `processing_status`.
- Approval object: `source`, `sp_no`, `status`, `status_name`, `template_version`, `applicant`, `approvers`, `comments`, `attachments`, `timeline`, `business_process_result`.
- Resubmit request: editable refund amount, attachments and reason; immutable order, asset and submitter snapshots.
- Resubmit eligible statuses: rejected, revoked or deleted, using the backend status contract.
## Risks / Trade-offs
- Risk: 企微审批状态名称或时间线字段可能为空或增加新状态.
- Mitigation: 保留后端原始字段,缺失时显示稳定占位,不在前端硬编码未知状态映射。
- Risk: 审批通过后业务处理可能失败或进入重试.
- Mitigation: 独立展示处理状态、失败摘要,并按后端结果显示系统重试或联系管理员提示。
- Risk: 后端仍允许旧本地审批接口调用.
- Mitigation: 前端移除入口和调用,同时由后端按新审批模式拒绝不适用的本地操作。
## Migration Plan
1. 确认退款创建、详情和重新申请接口的字段名称及状态枚举。
2. 扩展退款类型和对象存储附件元数据类型。
3. 更新创建退款表单和附件提交结构。
4. 重构退款详情为业务信息、企微审批信息和业务处理结果三个区域。
5. 移除本地审批及人工退款操作,保留并限制重新申请入口。
6. 验证审批中、通过处理中、处理成功、驳回、撤销和通过后撤销异常状态。
## Open Questions
- `processing_status` 的完整枚举、展示名称和“系统重试中/联系管理员”的触发字段需要后端确认。
- “已删除”是退款记录状态、审批状态还是业务处理状态,需要接口契约明确。
- `approval.attachments` 是否与退款创建附件共用文件对象结构,需要联调确认。

View File

@@ -0,0 +1,41 @@
# Change: 退款企微审批详情与业务处理状态
## Why
退款申请创建时需要一次性提交完整业务资料,后续审批由企微完成,后台系统不再承担本地通过、驳回、退回或人工退款确认。当前退款页面仍展示本地审批操作,且详情无法完整呈现企微审批过程和退款业务处理结果,容易造成审批状态与实际处理状态混淆。
## What Changes
- 退款创建表单支持退款金额、退款原因、备注和附件,并以对象存储附件对象数组提交。
- 创建后申请退款金额不可在本地修改,审批金额以企微审批结果为准。
- 退款详情拆分为退款业务信息、企微审批信息和业务处理结果三个区域。
- 企微审批区域展示 `sp_no`、审批状态、申请人、审批人、意见、附件和审批时间线。
- 业务处理区域展示 `processing_status`、处理失败摘要和“系统重试中/联系管理员”提示。
- 退款列表和详情仅展示审批及处理状态,不提供本地通过、驳回、退回或人工退款确认按钮。
- 重新申请仅允许已驳回、已撤销或已删除的退款记录,并允许修改金额、凭证和原因;订单、资产和提交人快照不可修改。
- 继续使用 `POST /api/admin/refunds``GET /api/admin/refunds/{id}``POST /api/admin/refunds/{id}/resubmit`,同步扩展请求和响应契约。
## Impact
- Affected specs:
- `refund-management`
- Affected code:
- `src/types/api/refund.ts`
- `src/api/modules/refund.ts`
- `src/views/finance/refund/index.vue`
- `src/views/finance/refund/detail.vue`
- 退款创建与附件展示组件
- API contracts:
- `POST /api/admin/refunds`
- `GET /api/admin/refunds/{id}`
- `POST /api/admin/refunds/{id}/resubmit`
- Dependencies:
- 后端返回完整 `approval` 对象和 `processing_status`
- 对象存储上传接口可返回 `file_key`,前端维护文件名和文件大小。
- 企微审批系统负责审批动作和审批结果同步。
- Out of scope:
- 前端发起、撤回或执行企微审批动作。
- 前端实现财务线下退款。
- 前端根据状态码自行推导审批结果或业务处理结果。
- Breaking changes:
- 退款审批相关本地操作按钮和对应前端调用不再适用于企微审批退款流程。

View File

@@ -0,0 +1,89 @@
## ADDED Requirements
### Requirement: Refund Creation Submits WeCom Approval Materials
The refund creation flow SHALL submit the refund business materials required for an external WeCom approval.
#### Scenario: Create refund with business materials
- **GIVEN** 用户打开退款创建表单
- **WHEN** 用户填写退款金额、退款原因、备注并完成附件上传
- **THEN** 系统 MUST submit the refund amount, reason, remark and attachment metadata to `POST /api/admin/refunds`
- **AND** 每个附件 MUST use the structure `{ file_key, file_name, file_size }`
#### Scenario: Submitted amount is immutable locally
- **GIVEN** 退款申请已创建并进入企微审批
- **WHEN** 用户查看退款记录
- **THEN** 页面 MUST NOT provide a local control to modify the submitted refund amount
- **AND** any changed amount MUST be submitted through the defined resubmission flow only
### Requirement: Refund Detail Displays WeCom Approval
The refund detail page SHALL display the backend-provided WeCom approval object as a read-only approval record.
#### Scenario: Display approval details
- **GIVEN** `GET /api/admin/refunds/{id}` returns an `approval` object
- **WHEN** 用户打开退款详情
- **THEN** 页面 MUST display the approval source, `sp_no`, status, status name, applicant, approvers, comments, attachments and timeline
- **AND** 页面 MUST preserve the backend approval values without locally changing or deriving them
#### Scenario: No local approval controls
- **GIVEN** 用户查看退款列表或退款详情
- **WHEN** 页面展示企微审批信息
- **THEN** 页面 MUST NOT display local approve, reject, return or manual refund confirmation buttons
- **AND** 页面 MUST NOT call local approval action endpoints for this flow
### Requirement: Refund Processing Result Is Independent From Approval
The refund detail page SHALL display business processing status separately from WeCom approval status.
#### Scenario: Display processing result
- **GIVEN** 退款详情返回 `processing_status` and its result fields
- **WHEN** 用户查看退款业务处理区域
- **THEN** 页面 MUST display the processing status, failure summary and business process result
- **AND** 页面 MUST display a system-retrying or contact-administrator message when indicated by the backend
#### Scenario: Display required lifecycle states
- **GIVEN** 后端返回审批中、通过处理中、处理成功、驳回、撤销或通过后撤销异常状态
- **WHEN** 页面渲染退款列表或详情
- **THEN** 页面 MUST show an explicit status for the returned state
- **AND** 页面 MUST NOT infer processing completion from approval status alone
### Requirement: Refund Resubmission Has Restricted Editable Fields
The refund resubmission flow SHALL allow resubmission only for rejected, revoked or deleted records and SHALL preserve immutable business snapshots.
#### Scenario: Resubmit an eligible refund
- **GIVEN** 退款状态为已驳回、已撤销或已删除
- **WHEN** 用户发起重新申请
- **THEN** 页面 MUST allow editing the refund amount, attachments and refund reason
- **AND** 系统 MUST submit the request to `POST /api/admin/refunds/{id}/resubmit`
#### Scenario: Reject resubmission for an ineligible refund
- **GIVEN** 退款记录不属于已驳回、已撤销或已删除状态
- **WHEN** 用户查看退款详情
- **THEN** 页面 MUST NOT enable the resubmission action
- **AND** 页面 MUST handle a backend rejection without creating a new approval request
#### Scenario: Preserve refund snapshots on resubmission
- **GIVEN** 用户填写重新申请表单
- **WHEN** 用户修改可编辑字段
- **THEN** 页面 MUST NOT provide controls to change the order snapshot, asset snapshot or submitter snapshot
### Requirement: Refund Pages Use Backend Approval And Processing Data
The refund list and detail pages SHALL use backend response data as the source of truth for approval and processing display.
#### Scenario: Refresh refund status
- **WHEN** 用户刷新退款列表或重新加载退款详情
- **THEN** 页面 MUST use the latest refund, approval and processing data returned by the backend
- **AND** 页面 MUST NOT reconstruct a status from stale local form data or legacy status mappings

View File

@@ -0,0 +1,38 @@
## 1. Contract And Types
- [x] 1.1 按产品文档落地退款创建接口的金额、原因、备注和附件字段名称。
- [x] 1.2 按产品文档扩展退款详情响应中的 `approval``processing_status`、失败摘要和业务处理结果字段。
- [x] 1.3 按产品文档限制重新申请的可编辑字段、允许状态和不可变快照字段。
- [x] 1.4 扩展退款请求、响应和审批对象 TypeScript 类型,支持附件对象数组。
- [x] 1.5 更新退款 API service 类型及注释,移除不再适用的本地审批调用契约。
## 2. Refund Creation
- [x] 2.1 更新退款创建表单,提交退款金额、原因、备注和附件。
- [x] 2.2 复用现有对象存储上传流程,并将上传结果规范化为 `{ file_key, file_name, file_size }[]`
- [x] 2.3 创建成功后明确提示已提交企微审批,创建后的申请金额不可在本地修改。
- [x] 2.4 验证附件上传中、上传失败、空附件和接口失败时不能提交错误数据。
## 3. Refund List And Detail
- [x] 3.1 退款详情拆分为退款业务信息、企微审批信息和业务处理结果三个区域。
- [x] 3.2 展示 `sp_no`、审批状态、申请人、审批人、意见、附件和审批时间线。
- [x] 3.3 展示 `processing_status`、失败摘要,以及系统重试中/联系管理员提示。
- [x] 3.4 退款列表和详情不显示本地通过、驳回、退回或人工退款确认按钮。
- [x] 3.5 保留既有退款业务信息和附件查看能力,不根据前端状态推导审批或处理结果。
## 4. Resubmission
- [x] 4.1 仅在已驳回、已撤销或已删除状态显示重新申请入口。
- [x] 4.2 重新申请允许修改退款金额、凭证和退款原因。
- [x] 4.3 重新申请禁止修改订单、资产和提交人快照。
- [x] 4.4 提交 `/api/admin/refunds/{id}/resubmit` 后刷新详情并展示新的企微审批状态。
- [x] 4.5 处理后端状态变化或无权限时的重新申请失败提示。
## 5. Verification
- [x] 5.1 验证审批中、通过处理中、处理成功、驳回、撤销和通过后撤销异常展示明确。
- [x] 5.2 验证审批信息与业务处理状态分区展示且互不推导。
- [x] 5.3 验证本地审批及人工退款操作入口和 API 调用均已移除。
- [x] 5.4 验证刷新、列表进入详情和重新申请后的数据来自后端最新响应。
- [x] 5.5 运行类型检查、lint、相关测试和构建。

View File

@@ -8,10 +8,7 @@ import type {
RefundQueryParams, RefundQueryParams,
RefundListResponse, RefundListResponse,
CreateRefundRequest, CreateRefundRequest,
ApproveRefundRequest,
RejectRefundRequest,
ResubmitRefundRequest, ResubmitRefundRequest,
ReturnRefundRequest,
BaseResponse BaseResponse
} from '@/types/api' } from '@/types/api'
@@ -40,24 +37,6 @@ export class RefundService extends BaseService {
return this.post<BaseResponse<Refund>>('/api/admin/refunds', data) return this.post<BaseResponse<Refund>>('/api/admin/refunds', data)
} }
/**
* 审批通过退款申请
* @param id 退款申请ID
* @param data 审批通过请求参数
*/
static approveRefund(id: number, data: ApproveRefundRequest): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/approve`, data)
}
/**
* 审批拒绝退款申请
* @param id 退款申请ID
* @param data 审批拒绝请求参数
*/
static rejectRefund(id: number, data: RejectRefundRequest): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/reject`, data)
}
/** /**
* 重新提交退款申请 * 重新提交退款申请
* @param id 退款申请ID * @param id 退款申请ID
@@ -66,13 +45,4 @@ export class RefundService extends BaseService {
static resubmitRefund(id: number, data: ResubmitRefundRequest): Promise<BaseResponse<void>> { static resubmitRefund(id: number, data: ResubmitRefundRequest): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/resubmit`, data) return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/resubmit`, data)
} }
/**
* 驳回申请
* @param id 退款申请ID
* @param data 退回请求参数
*/
static returnRefund(id: number, data: ReturnRefundRequest): Promise<BaseResponse<void>> {
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/return`, data)
}
} }

View File

@@ -46,6 +46,16 @@
placeholder="请输入退款原因" placeholder="请输入退款原因"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="备注">
<ElInput
v-model="formData.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="请输入备注"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key"> <ElFormItem label="退款凭证" prop="refund_voucher_key">
<VoucherUpload <VoucherUpload
ref="uploadRef" ref="uploadRef"
@@ -53,6 +63,7 @@
voucher-name="退款凭证" voucher-name="退款凭证"
@uploading-change="voucherUploading = $event" @uploading-change="voucherUploading = $event"
@change="formRef?.validateField('refund_voucher_key')" @change="formRef?.validateField('refund_voucher_key')"
@files-change="formData.attachments = $event"
/> />
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@@ -77,7 +88,7 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { RefundService, OrderService } from '@/api/modules' import { RefundService, OrderService } from '@/api/modules'
import type { CreateRefundRequest, Order } from '@/types/api' import type { CreateRefundRequest, Order, RefundAttachment } from '@/types/api'
import { formatMoney, yuanToFen } from '@/utils/business/format' import { formatMoney, yuanToFen } from '@/utils/business/format'
import { getErrorMessage } from '@/utils/business' import { getErrorMessage } from '@/utils/business'
import VoucherUpload from '@/components/business/VoucherUpload.vue' import VoucherUpload from '@/components/business/VoucherUpload.vue'
@@ -111,13 +122,17 @@
requested_refund_amount?: number requested_refund_amount?: number
actual_received_amount: number actual_received_amount: number
refund_reason: string refund_reason: string
remark: string
refund_voucher_key: string[] refund_voucher_key: string[]
attachments: RefundAttachment[]
}>({ }>({
order_id: null, order_id: null,
requested_refund_amount: undefined, requested_refund_amount: undefined,
actual_received_amount: 0, actual_received_amount: 0,
refund_reason: '', refund_reason: '',
refund_voucher_key: [] remark: '',
refund_voucher_key: [],
attachments: []
}) })
const validateRefundAmount = (rule: any, value: number, callback: any) => { const validateRefundAmount = (rule: any, value: number, callback: any) => {
@@ -201,7 +216,9 @@
formData.requested_refund_amount = undefined formData.requested_refund_amount = undefined
formData.actual_received_amount = 0 formData.actual_received_amount = 0
formData.refund_reason = '' formData.refund_reason = ''
formData.remark = ''
formData.refund_voucher_key = [] formData.refund_voucher_key = []
formData.attachments = []
orderSearchOptions.value = [] orderSearchOptions.value = []
uploadRef.value?.clearFiles(false) uploadRef.value?.clearFiles(false)
} }
@@ -221,8 +238,9 @@
order_id: formData.order_id!, order_id: formData.order_id!,
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0, requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
actual_received_amount: formData.actual_received_amount, actual_received_amount: formData.actual_received_amount,
refund_voucher_key: formData.refund_voucher_key, attachments: formData.attachments,
refund_reason: formData.refund_reason || undefined refund_reason: formData.refund_reason || undefined,
remark: formData.remark || undefined
} }
await RefundService.createRefund(data) await RefundService.createRefund(data)
ElMessage.success('退款申请创建成功') ElMessage.success('退款申请创建成功')

View File

@@ -57,6 +57,7 @@
import { Close, Document, UploadFilled } from '@element-plus/icons-vue' import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus' import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
import { StorageService } from '@/api/modules' import { StorageService } from '@/api/modules'
import type { RefundAttachment } from '@/types/api/refund'
interface Props { interface Props {
modelValue?: string[] | string modelValue?: string[] | string
@@ -77,12 +78,14 @@
'update:modelValue': [value: string[]] 'update:modelValue': [value: string[]]
'uploading-change': [value: boolean] 'uploading-change': [value: boolean]
change: [value: string[]] change: [value: string[]]
'files-change': [value: RefundAttachment[]]
}>() }>()
const rootRef = ref<HTMLElement>() const rootRef = ref<HTMLElement>()
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
const uploadingCount = ref(0) const uploadingCount = ref(0)
const voucherFileKeyMap = new Map<number, string>() const voucherFileKeyMap = new Map<number, string>()
const voucherFileMetadataMap = new Map<number, RefundAttachment>()
const removedUploadUids = new Set<number>() const removedUploadUids = new Set<number>()
const fileObjectUrlMap = new Map<number, string>() const fileObjectUrlMap = new Map<number, string>()
const selectedUploadUids = new Set<number>() const selectedUploadUids = new Set<number>()
@@ -100,6 +103,10 @@
emit('change', keys) emit('change', keys)
} }
const emitFileMetadata = () => {
emit('files-change', Array.from(voucherFileMetadataMap.values()))
}
const setUploadingCount = (count: number) => { const setUploadingCount = (count: number) => {
uploadingCount.value = Math.max(0, count) uploadingCount.value = Math.max(0, count)
emit('uploading-change', uploadingCount.value > 0) emit('uploading-change', uploadingCount.value > 0)
@@ -109,12 +116,14 @@
activeUploadBatch += 1 activeUploadBatch += 1
setUploadingCount(0) setUploadingCount(0)
voucherFileKeyMap.clear() voucherFileKeyMap.clear()
voucherFileMetadataMap.clear()
removedUploadUids.clear() removedUploadUids.clear()
selectedUploadUids.clear() selectedUploadUids.clear()
revokeAllFileObjectUrls() revokeAllFileObjectUrls()
uploadRef.value?.clearFiles() uploadRef.value?.clearFiles()
if (emitValue) { if (emitValue) {
emitVoucherKeys() emitVoucherKeys()
emitFileMetadata()
} }
} }
@@ -228,7 +237,13 @@
} }
voucherFileKeyMap.set(uploadFile.uid, file_key) voucherFileKeyMap.set(uploadFile.uid, file_key)
voucherFileMetadataMap.set(uploadFile.uid, {
file_key,
file_name: file.name,
file_size: file.size
})
emitVoucherKeys() emitVoucherKeys()
emitFileMetadata()
ElMessage.success('上传成功') ElMessage.success('上传成功')
} catch (error: any) { } catch (error: any) {
if (uploadBatch !== activeUploadBatch) return if (uploadBatch !== activeUploadBatch) return
@@ -236,7 +251,9 @@
console.error(`上传${props.voucherName}失败:`, error) console.error(`上传${props.voucherName}失败:`, error)
ElMessage.error(error?.message || '上传失败,请重试') ElMessage.error(error?.message || '上传失败,请重试')
voucherFileKeyMap.delete(uploadFile.uid) voucherFileKeyMap.delete(uploadFile.uid)
voucherFileMetadataMap.delete(uploadFile.uid)
emitVoucherKeys() emitVoucherKeys()
emitFileMetadata()
removeUploadFile(uploadFile) removeUploadFile(uploadFile)
} finally { } finally {
if (uploadBatch === activeUploadBatch) { if (uploadBatch === activeUploadBatch) {
@@ -250,7 +267,9 @@
selectedUploadUids.delete(uploadFile.uid) selectedUploadUids.delete(uploadFile.uid)
revokeFileObjectUrl(uploadFile.uid) revokeFileObjectUrl(uploadFile.uid)
voucherFileKeyMap.delete(uploadFile.uid) voucherFileKeyMap.delete(uploadFile.uid)
voucherFileMetadataMap.delete(uploadFile.uid)
emitVoucherKeys() emitVoucherKeys()
emitFileMetadata()
} }
const focusUploadArea = () => { const focusUploadArea = () => {

View File

@@ -10,6 +10,38 @@ export enum RefundStatus {
RETURNED = 4 // 已退回 RETURNED = 4 // 已退回
} }
export interface RefundAttachment {
file_key: string
file_name?: string
file_size?: number
}
export interface RefundApprovalTimelineItem {
time?: string
timestamp?: string
status?: string
status_name?: string
operator?: string
operator_name?: string
comment?: string
content?: string
[key: string]: unknown
}
export interface RefundApproval {
source?: string | null
sp_no?: string | null
status?: string | null
status_name?: string | null
template_version?: string | null
applicant?: unknown
approvers?: unknown[]
comments?: unknown[]
attachments?: RefundAttachment[]
timeline?: RefundApprovalTimelineItem[]
business_process_result?: unknown
}
// 退款申请 // 退款申请
export interface Refund { export interface Refund {
id: number id: number
@@ -25,8 +57,10 @@ export interface Refund {
approved_refund_amount?: number | null // 实际退款金额(分) approved_refund_amount?: number | null // 实际退款金额(分)
actual_received_amount?: number | null // 实收金额(分) actual_received_amount?: number | null // 实收金额(分)
status: RefundStatus status: RefundStatus
status_name?: string | null
refund_reason: string refund_reason: string
refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串 refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串
attachments?: RefundAttachment[]
reject_reason: string reject_reason: string
remark: string remark: string
asset_reset: boolean asset_reset: boolean
@@ -38,6 +72,10 @@ export interface Refund {
current_approver_summary?: string | null // 当前审批人摘要 current_approver_summary?: string | null // 当前审批人摘要
processing_status?: string | null // 业务处理状态 processing_status?: string | null // 业务处理状态
processing_status_name?: string | null // 业务处理状态名称 processing_status_name?: string | null // 业务处理状态名称
processing_failure_summary?: string | null
processing_message?: string | null
business_process_result?: unknown
approval?: RefundApproval | null
creator: number creator: number
processor_id: number | null processor_id: number | null
processed_at: string | null processed_at: string | null
@@ -71,30 +109,15 @@ export interface CreateRefundRequest {
order_id: number order_id: number
requested_refund_amount: number // 申请退款金额(分) requested_refund_amount: number // 申请退款金额(分)
actual_received_amount: number // 实收金额(分) actual_received_amount: number // 实收金额(分)
refund_voucher_key: string[] // 退款凭证附件列表 attachments: RefundAttachment[]
refund_reason?: string refund_reason?: string
}
// 审批通过退款申请请求
export interface ApproveRefundRequest {
approved_refund_amount?: number // 实际退款金额(分)
remark?: string remark?: string
} }
// 审批拒绝退款申请请求
export interface RejectRefundRequest {
reject_reason: string
}
// 重新提交退款申请请求 // 重新提交退款申请请求
export interface ResubmitRefundRequest { export interface ResubmitRefundRequest {
requested_refund_amount?: number requested_refund_amount?: number
actual_received_amount?: number actual_received_amount?: number
refund_voucher_key?: string[] attachments?: RefundAttachment[]
refund_reason?: string refund_reason?: string
} }
// 退回退款申请请求
export interface ReturnRefundRequest {
remark?: string
}

View File

@@ -10,6 +10,13 @@
返回 返回
</ElButton> </ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2> <h2 class="detail-title">{{ pageTitle }}</h2>
<ElButton
v-if="refund && canResubmit(refund) && hasAuth('refund:resubmit')"
type="primary"
@click="handleShowResubmit"
>
重新申请
</ElButton>
</div> </div>
<!-- 详情内容 --> <!-- 详情内容 -->
@@ -24,90 +31,10 @@
<PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" /> <PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" />
<!-- 审批通过对话框 -->
<ElDialog
v-model="approveDialogVisible"
title="审批通过"
width="500px"
@closed="handleApproveDialogClosed"
>
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ refund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="申请退款金额">
<span>{{ formatCurrency(refund?.requested_refund_amount) }}</span>
</ElFormItem>
<ElFormItem label="实际退款金额" prop="approved_refund_amount">
<ElInputNumber
v-model="approveForm.approved_refund_amount"
:min="1"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则默认使用申请金额"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
不填则默认使用申请金额
</div>
</ElFormItem>
<ElFormItem label="审批备注">
<ElInput
v-model="approveForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入审批备注"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="approveDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleApproveRefund" :loading="approveLoading">
确认通过
</ElButton>
</div>
</template>
</ElDialog>
<!-- 审批拒绝对话框 -->
<ElDialog
v-model="rejectDialogVisible"
title="审批拒绝"
width="500px"
@closed="handleRejectDialogClosed"
>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ refund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="拒绝原因" prop="reject_reason">
<ElInput
v-model="rejectForm.reject_reason"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入拒绝原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
<ElButton type="danger" @click="handleRejectRefund" :loading="rejectLoading">
确认拒绝
</ElButton>
</div>
</template>
</ElDialog>
<!-- 重新提交对话框 --> <!-- 重新提交对话框 -->
<ElDialog <ElDialog
v-model="resubmitDialogVisible" v-model="resubmitDialogVisible"
title="重新提交退款申请" title="重新申请退款"
width="500px" width="500px"
@closed="handleResubmitDialogClosed" @closed="handleResubmitDialogClosed"
> >
@@ -157,6 +84,7 @@
voucher-name="退款凭证" voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event" @uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')" @change="resubmitFormRef?.validateField('refund_voucher_key')"
@files-change="resubmitForm.attachments = $event"
/> />
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@@ -169,7 +97,7 @@
:loading="resubmitLoading || resubmitVoucherUploading" :loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading" :disabled="resubmitVoucherUploading"
> >
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }} {{ resubmitVoucherUploading ? '凭证上传中...' : '确认申请' }}
</ElButton> </ElButton>
</div> </div>
</template> </template>
@@ -180,27 +108,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed, h, reactive } from 'vue' import { ref, onMounted, computed, h, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElButton, ElCard, ElIcon, ElMessage, ElTag } from 'element-plus' import { ElButton, ElCard, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { RefundService } from '@/api/modules' import { RefundService } from '@/api/modules'
import type { import type { Refund, ResubmitRefundRequest, RefundAttachment } from '@/types/api'
Refund, import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
RefundStatus, import { toVoucherKeyList, getErrorMessage } from '@/utils/business'
ApproveRefundRequest,
RejectRefundRequest,
ResubmitRefundRequest
} from '@/types/api'
import { formatDateTime, yuanToFen } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue' import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue' import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { useAuth } from '@/composables/useAuth'
defineOptions({ name: 'RefundDetail' }) defineOptions({ name: 'RefundDetail' })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false) const loading = ref(false)
const refund = ref<Refund | null>(null) const refund = ref<Refund | null>(null)
@@ -208,44 +132,27 @@
const pageTitle = computed(() => `退款详情`) const pageTitle = computed(() => `退款详情`)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const resubmitDialogVisible = ref(false) const resubmitDialogVisible = ref(false)
const approveLoading = ref(false)
const rejectLoading = ref(false)
const resubmitLoading = ref(false) const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false) const resubmitVoucherUploading = ref(false)
const approveFormRef = ref()
const rejectFormRef = ref()
const resubmitFormRef = ref() const resubmitFormRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>() const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const approveRules = ref()
const rejectRules = ref({
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const resubmitRules = ref() const resubmitRules = ref()
const approveForm = reactive<ApproveRefundRequest>({
approved_refund_amount: undefined,
remark: ''
})
const rejectForm = reactive<RejectRefundRequest>({
reject_reason: ''
})
const resubmitForm = reactive<{ const resubmitForm = reactive<{
requested_refund_amount?: number requested_refund_amount?: number
actual_received_amount?: number actual_received_amount?: number
refund_voucher_key: string[] refund_voucher_key: string[]
attachments: RefundAttachment[]
refund_reason?: string refund_reason?: string
}>({ }>({
requested_refund_amount: undefined, requested_refund_amount: undefined,
actual_received_amount: undefined, actual_received_amount: undefined,
refund_voucher_key: [], refund_voucher_key: [],
attachments: [],
refund_reason: '' refund_reason: ''
}) })
@@ -255,45 +162,51 @@
return `¥${(amount / 100).toFixed(2)}` return `¥${(amount / 100).toFixed(2)}`
} }
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => { const formatStructuredValue = (value: unknown) => {
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = { if (value === undefined || value === null || value === '') return '-'
1: 'warning', if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
2: 'success', return String(value)
3: 'danger',
4: 'info'
} }
return statusMap[status] || 'info' return JSON.stringify(value, null, 2)
} }
const getStatusText = (status: RefundStatus): string => { const renderStructuredValue = (value: unknown) =>
const statusMap: Record<RefundStatus, string> = { h('pre', { class: 'structured-value' }, formatStructuredValue(value))
1: '待审批',
2: '已通过', const renderTimeline = (timeline: unknown) => {
3: '已拒绝', if (!Array.isArray(timeline) || timeline.length === 0) return h('span', '-')
4: '已退回'
return h(
'div',
{ class: 'approval-timeline' },
timeline.map((item: any) => {
const title = item.status_name || item.status || item.content || '审批节点'
const operator = item.operator_name || item.operator || ''
const time = item.time || item.timestamp || ''
const comment = item.comment || item.content || ''
return h('div', { class: 'approval-timeline__item' }, [
h('strong', title),
operator || time ? h('span', `${operator}${operator && time ? ' · ' : ''}${time}`) : null,
comment && comment !== title ? h('div', comment) : null
])
})
)
} }
return statusMap[status] || '-'
const getRefundAttachmentKeys = (item: Refund) => {
const keys = item.attachments?.map((attachment) => attachment.file_key).filter(Boolean) || []
return keys.length ? keys : toVoucherKeyList(item.refund_voucher_key)
} }
const detailSections = computed((): DetailSection[] => [ const detailSections = computed((): DetailSection[] => [
{ {
title: '退款信息', title: '退款业务信息',
fields: [ fields: [
{ label: '退款单号', prop: 'refund_no' }, { label: '退款单号', prop: 'refund_no' },
{ label: '店铺名称', prop: 'shop_name' }, { label: '店铺名称', prop: 'shop_name' },
{ label: '订单号', prop: 'order_no' }, { label: '订单号', prop: 'order_no' },
{ label: '资产标识符', prop: 'asset_identifier' }, { label: '资产标识符', prop: 'asset_identifier' },
{ { label: '资产类型', prop: 'asset_type' },
label: '资产类型',
formatter: (_, data) =>
data.asset_type === 'device'
? '设备'
: data.asset_type === 'card'
? '单卡'
: data.asset_type === 'iot_card'
? 'IoT卡'
: data.asset_type || '-'
},
{ {
label: '申请退款金额', label: '申请退款金额',
formatter: (_, data) => formatCurrency(data.requested_refund_amount) formatter: (_, data) => formatCurrency(data.requested_refund_amount)
@@ -307,22 +220,29 @@
formatter: (_, data) => formatCurrency(data.actual_received_amount) formatter: (_, data) => formatCurrency(data.actual_received_amount)
}, },
{ {
label: '状态', label: '退款原因',
render: (data) => prop: 'refund_reason',
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status)) formatter: (value) => value || '-',
fullWidth: true
},
{
label: '备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}, },
{ {
label: '退款凭证', label: '退款凭证',
fullWidth: true, fullWidth: true,
render: (data) => render: (data) =>
hasVoucherKeys(data.refund_voucher_key) getRefundAttachmentKeys(data).length
? h( ? h(
ElButton, ElButton,
{ {
type: 'primary', type: 'primary',
link: true, link: true,
onClick: () => { onClick: () => {
refundVoucherFileKeys.value = toVoucherKeyList(data.refund_voucher_key) refundVoucherFileKeys.value = getRefundAttachmentKeys(data)
} }
}, },
() => '查看退款凭证' () => '查看退款凭证'
@@ -331,60 +251,89 @@
}, },
{ {
label: '资产重置', label: '资产重置',
render: (data) => formatter: (_, data) => (data.asset_reset ? '是' : '否')
h(ElTag, { type: data.asset_reset ? 'success' : 'info' }, () =>
data.asset_reset ? '是' : '否'
)
}, },
{ {
label: '佣金扣除', label: '佣金扣除',
render: (data) => formatter: (_, data) => (data.commission_deducted ? '是' : '否')
h(ElTag, { type: data.commission_deducted ? 'success' : 'info' }, () => },
data.commission_deducted ? '是' : '否' { label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
]
},
{
title: '企微审批信息',
fields: [
{ label: '审批来源', prop: 'approval.source' },
{ label: '审批单号', prop: 'approval.sp_no' },
{
label: '审批状态',
formatter: (_, data) => data.approval?.status_name || data.approval?.status || '-'
},
{ label: '模板版本', prop: 'approval.template_version' },
{
label: '申请人',
render: (data) => renderStructuredValue(data.approval?.applicant)
},
{
label: '审批人',
render: (data) => renderStructuredValue(data.approval?.approvers)
},
{
label: '审批意见',
render: (data) => renderStructuredValue(data.approval?.comments),
fullWidth: true
},
{
label: '审批附件',
render: (data) => {
const keys = (data.approval?.attachments || [])
.map((item: RefundAttachment) => item.file_key)
.filter(Boolean)
return keys.length
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => (refundVoucherFileKeys.value = keys)
},
() => '查看审批附件'
) )
: h('span', '-')
} }
]
}, },
{ {
title: '退款原因', label: '审批时间线',
fields: [ render: (data) => renderTimeline(data.approval?.timeline),
{
label: '退款原因',
prop: 'refund_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '拒绝原因',
prop: 'reject_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '审批备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true fullWidth: true
} }
] ]
}, },
{ {
title: '时间信息', title: '业务处理结果',
fields: [ fields: [
{ {
label: '创建时间', label: '处理状态',
prop: 'created_at', formatter: (_, data) => data.processing_status_name || data.processing_status || '-'
formatter: (value) => formatDateTime(value)
}, },
{ {
label: '审批时间', label: '失败摘要',
prop: 'processed_at', formatter: (_, data) => data.processing_failure_summary || data.error_summary || '-',
formatter: (value) => (value ? formatDateTime(value) : '-') fullWidth: true
}, },
{ {
label: '更新时间', label: '处理提示',
prop: 'updated_at', formatter: (_, data) => data.processing_message || '-',
formatter: (value) => formatDateTime(value) fullWidth: true
},
{
label: '业务处理结果',
render: (data) =>
renderStructuredValue(
data.business_process_result || data.approval?.business_process_result
),
fullWidth: true
} }
] ]
} }
@@ -416,72 +365,44 @@
} }
}) })
const handleApproveDialogClosed = () => {
approveFormRef.value?.resetFields()
approveForm.approved_refund_amount = undefined
approveForm.remark = ''
}
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.reject_reason = ''
}
const handleResubmitDialogClosed = () => { const handleResubmitDialogClosed = () => {
resubmitFormRef.value?.resetFields() resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = [] resubmitForm.refund_voucher_key = []
resubmitForm.attachments = []
resubmitForm.refund_reason = '' resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false) resubmitUploadRef.value?.clearFiles(false)
} }
const handleApproveRefund = async () => { const canResubmit = (item: Refund) => {
if (!approveFormRef.value || !refund.value) return const statusText = [item.approval_status, item.approval_status_name, item.status_name]
.filter(Boolean)
.join(' ')
.toLowerCase()
const refundId = refund.value.id return (
await approveFormRef.value.validate(async (valid: boolean) => { item.status === 3 ||
if (valid) { statusText.includes('reject') ||
approveLoading.value = true statusText.includes('revoke') ||
try { statusText.includes('delete') ||
const data: ApproveRefundRequest = { statusText.includes('驳回') ||
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount), statusText.includes('撤销') ||
remark: approveForm.remark || undefined statusText.includes('删除')
)
} }
await RefundService.approveRefund(refundId, data) const handleShowResubmit = () => {
ElMessage.success('审批通过成功') if (!refund.value || !canResubmit(refund.value)) return
approveDialogVisible.value = false resubmitForm.requested_refund_amount =
await fetchRefundDetail(refundId) fenToYuan(refund.value.requested_refund_amount) || undefined
} catch (error) { resubmitForm.actual_received_amount =
console.error(error) fenToYuan(refund.value.actual_received_amount) || undefined
} finally { resubmitForm.refund_voucher_key = []
approveLoading.value = false resubmitForm.attachments = []
} resubmitForm.refund_reason = refund.value.refund_reason
} resubmitDialogVisible.value = true
})
}
const handleRejectRefund = async () => {
if (!rejectFormRef.value || !refund.value) return
const refundId = refund.value.id
await rejectFormRef.value.validate(async (valid: boolean) => {
if (valid) {
rejectLoading.value = true
try {
await RefundService.rejectRefund(refundId, rejectForm)
ElMessage.success('审批拒绝成功')
rejectDialogVisible.value = false
await fetchRefundDetail(refundId)
} catch (error) {
console.error(error)
} finally {
rejectLoading.value = false
}
}
})
} }
const handleResubmitRefund = async () => { const handleResubmitRefund = async () => {
@@ -499,18 +420,16 @@
const data: ResubmitRefundRequest = { const data: ResubmitRefundRequest = {
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount), requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount), actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined refund_reason: resubmitForm.refund_reason || undefined,
} attachments: resubmitForm.attachments
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
} }
await RefundService.resubmitRefund(refundId, data) await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功') ElMessage.success('重新申请成功')
resubmitDialogVisible.value = false resubmitDialogVisible.value = false
await fetchRefundDetail(refundId) await fetchRefundDetail(refundId)
} catch (error) { } catch (error) {
console.error(error) ElMessage.error(getErrorMessage(error, '重新申请失败'))
} finally { } finally {
resubmitLoading.value = false resubmitLoading.value = false
} }
@@ -530,6 +449,7 @@
margin-bottom: 24px; margin-bottom: 24px;
.detail-title { .detail-title {
flex: 1;
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
@@ -537,6 +457,33 @@
} }
} }
.structured-value {
max-width: 100%;
margin: 0;
overflow-x: auto;
font: inherit;
line-height: 1.6;
white-space: pre-wrap;
}
.approval-timeline {
display: flex;
flex-direction: column;
gap: 12px;
&__item {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 12px;
border-left: 2px solid var(--el-color-primary-light-5);
span {
color: var(--el-text-color-secondary);
}
}
}
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -54,130 +54,10 @@
@close="refundVoucherFileKeys = []" @close="refundVoucherFileKeys = []"
/> />
<!-- 审批通过对话框 -->
<ElDialog
v-model="approveDialogVisible"
title="审批通过"
width="500px"
@closed="handleApproveDialogClosed"
>
<ElForm
ref="approveFormRef"
:model="approveForm"
:rules="approveRules"
label-width="120px"
>
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="申请退款金额">
<span>{{ formatCurrency(currentRefund?.requested_refund_amount) }}</span>
</ElFormItem>
<ElFormItem label="实际退款金额" prop="approved_refund_amount">
<ElInputNumber
v-model="approveForm.approved_refund_amount"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则默认使用申请金额"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
不填则默认使用申请金额
</div>
</ElFormItem>
<ElFormItem label="审批备注">
<ElInput
v-model="approveForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入审批备注"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="approveDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleApproveRefund" :loading="approveLoading">
确认通过
</ElButton>
</div>
</template>
</ElDialog>
<!-- 审批拒绝对话框 -->
<ElDialog
v-model="rejectDialogVisible"
title="审批拒绝"
width="500px"
@closed="handleRejectDialogClosed"
>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="拒绝原因" prop="reject_reason">
<ElInput
v-model="rejectForm.reject_reason"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入拒绝原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
<ElButton type="danger" @click="handleRejectRefund" :loading="rejectLoading">
确认拒绝
</ElButton>
</div>
</template>
</ElDialog>
<!-- 退回对话框 -->
<ElDialog
v-model="returnDialogVisible"
title="驳回申请"
width="30%"
@closed="handleReturnDialogClosed"
>
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="80">
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="退回备注">
<ElInput
v-model="returnForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入退回备注"
/>
</ElFormItem>
</ElForm>
<div class="return-dialog-tip">
驳回后该订单不可再申请退款若还需申请退款点击审核拒绝
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="returnDialogVisible = false">取消</ElButton>
<ElButton type="warning" @click="handleReturnRefund" :loading="returnLoading">
确认退回
</ElButton>
</div>
</template>
</ElDialog>
<!-- 重新提交对话框 --> <!-- 重新提交对话框 -->
<ElDialog <ElDialog
v-model="resubmitDialogVisible" v-model="resubmitDialogVisible"
title="重新提交退款申请" title="重新申请退款"
width="500px" width="500px"
@closed="handleResubmitDialogClosed" @closed="handleResubmitDialogClosed"
> >
@@ -227,6 +107,7 @@
voucher-name="退款凭证" voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event" @uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')" @change="resubmitFormRef?.validateField('refund_voucher_key')"
@files-change="resubmitForm.attachments = $event"
/> />
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@@ -239,7 +120,7 @@
:loading="resubmitLoading || resubmitVoucherUploading" :loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading" :disabled="resubmitVoucherUploading"
> >
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }} {{ resubmitVoucherUploading ? '凭证上传中...' : '确认申请' }}
</ElButton> </ElButton>
</div> </div>
</template> </template>
@@ -259,10 +140,8 @@
Refund, Refund,
RefundQueryParams, RefundQueryParams,
RefundStatus, RefundStatus,
ApproveRefundRequest, ResubmitRefundRequest,
RejectRefundRequest, RefundAttachment
ReturnRefundRequest,
ResubmitRefundRequest
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -272,7 +151,7 @@
getCurrentApproverSummaryText, getCurrentApproverSummaryText,
getProcessingStatusText getProcessingStatusText
} from '@/utils/business/approvalSummary' } from '@/utils/business/approvalSummary'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business' import { toVoucherKeyList } from '@/utils/business'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue' import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue' import VoucherUpload from '@/components/business/VoucherUpload.vue'
@@ -286,17 +165,11 @@
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const loading = ref(false) const loading = ref(false)
const approveLoading = ref(false)
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false) const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false) const resubmitVoucherUploading = ref(false)
const tableRef = ref() const tableRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>() const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const returnDialogVisible = ref(false)
const resubmitDialogVisible = ref(false) const resubmitDialogVisible = ref(false)
const currentRefund = ref<Refund | null>(null) const currentRefund = ref<Refund | null>(null)
const refundVoucherFileKeys = ref<string[]>([]) const refundVoucherFileKeys = ref<string[]>([])
@@ -409,43 +282,21 @@
{ label: '审批时间', prop: 'processed_at' } { label: '审批时间', prop: 'processed_at' }
] ]
const approveFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const returnFormRef = ref<FormInstance>()
const resubmitFormRef = ref<FormInstance>() const resubmitFormRef = ref<FormInstance>()
const approveRules = reactive<FormRules>({})
const rejectRules = reactive<FormRules>({
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const returnRules = reactive<FormRules>({})
const resubmitRules = reactive<FormRules>({}) const resubmitRules = reactive<FormRules>({})
const approveForm = reactive<ApproveRefundRequest>({
approved_refund_amount: undefined,
remark: ''
})
const rejectForm = reactive<RejectRefundRequest>({
reject_reason: ''
})
const returnForm = reactive<ReturnRefundRequest>({
remark: ''
})
const resubmitForm = reactive<{ const resubmitForm = reactive<{
requested_refund_amount?: number requested_refund_amount?: number
actual_received_amount?: number actual_received_amount?: number
refund_voucher_key: string[] refund_voucher_key: string[]
attachments: RefundAttachment[]
refund_reason?: string refund_reason?: string
}>({ }>({
requested_refund_amount: undefined, requested_refund_amount: undefined,
actual_received_amount: undefined, actual_received_amount: undefined,
refund_voucher_key: [], refund_voucher_key: [],
attachments: [],
refund_reason: '' refund_reason: ''
}) })
@@ -752,119 +603,6 @@
getTableData() getTableData()
} }
// 显示审批通过对话框
const handleShowApprove = (row: Refund) => {
currentRefund.value = row
approveDialogVisible.value = true
}
// 审批通过对话框关闭后的清理
const handleApproveDialogClosed = () => {
approveFormRef.value?.resetFields()
approveForm.approved_refund_amount = undefined
approveForm.remark = ''
currentRefund.value = null
}
// 审批通过
const handleApproveRefund = async () => {
if (!approveFormRef.value || !currentRefund.value) return
await approveFormRef.value.validate(async (valid) => {
if (valid) {
approveLoading.value = true
try {
const data: ApproveRefundRequest = {
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
remark: approveForm.remark || undefined
}
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.approveRefund(refundId, data)
ElMessage.success('审批通过成功')
approveDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
approveLoading.value = false
}
}
})
}
// 显示审批拒绝对话框
const handleShowReject = (row: Refund) => {
currentRefund.value = row
rejectDialogVisible.value = true
}
// 审批拒绝对话框关闭后的清理
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.reject_reason = ''
currentRefund.value = null
}
// 审批拒绝
const handleRejectRefund = async () => {
if (!rejectFormRef.value || !currentRefund.value) return
await rejectFormRef.value.validate(async (valid) => {
if (valid) {
rejectLoading.value = true
try {
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.rejectRefund(refundId, rejectForm)
ElMessage.success('审批拒绝成功')
rejectDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
rejectLoading.value = false
}
}
})
}
// 显示退回对话框
const handleShowReturn = (row: Refund) => {
currentRefund.value = row
returnDialogVisible.value = true
}
// 退回对话框关闭后的清理
const handleReturnDialogClosed = () => {
returnFormRef.value?.resetFields()
returnForm.remark = ''
currentRefund.value = null
}
// 退回
const handleReturnRefund = async () => {
if (!returnFormRef.value || !currentRefund.value) return
await returnFormRef.value.validate(async (valid) => {
if (valid) {
returnLoading.value = true
try {
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.returnRefund(refundId, returnForm)
ElMessage.success('退回成功')
returnDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
returnLoading.value = false
}
}
})
}
// 显示重新提交对话框 // 显示重新提交对话框
const handleShowResubmit = (row: Refund) => { const handleShowResubmit = (row: Refund) => {
currentRefund.value = row currentRefund.value = row
@@ -872,6 +610,7 @@
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
resubmitForm.refund_voucher_key = [] resubmitForm.refund_voucher_key = []
resubmitForm.attachments = []
resubmitForm.refund_reason = row.refund_reason resubmitForm.refund_reason = row.refund_reason
resubmitDialogVisible.value = true resubmitDialogVisible.value = true
} }
@@ -882,6 +621,7 @@
resubmitForm.requested_refund_amount = undefined resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = [] resubmitForm.refund_voucher_key = []
resubmitForm.attachments = []
resubmitForm.refund_reason = '' resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false) resubmitUploadRef.value?.clearFiles(false)
@@ -905,14 +645,12 @@
const data: ResubmitRefundRequest = { const data: ResubmitRefundRequest = {
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount), requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount), actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined refund_reason: resubmitForm.refund_reason || undefined,
} attachments: resubmitForm.attachments
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
} }
await RefundService.resubmitRefund(refundId, data) await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功') ElMessage.success('重新申请成功')
resubmitDialogVisible.value = false resubmitDialogVisible.value = false
await getTableData() await getTableData()
} catch (error) { } catch (error) {
@@ -940,43 +678,36 @@
}) })
} }
const getRefundAttachmentKeys = (row: Refund) => {
const keys = row.attachments?.map((attachment) => attachment.file_key).filter(Boolean) || []
return keys.length ? keys : toVoucherKeyList(row.refund_voucher_key)
}
const canResubmit = (row: Refund) => {
const statusText = [row.approval_status, row.approval_status_name, row.status_name]
.filter(Boolean)
.join(' ')
.toLowerCase()
return (
row.status === 3 ||
statusText.includes('reject') ||
statusText.includes('revoke') ||
statusText.includes('delete') ||
statusText.includes('驳回') ||
statusText.includes('撤销') ||
statusText.includes('删除')
)
}
// 获取操作按钮 // 获取操作按钮
const getActions = (row: Refund) => { const getActions = (row: Refund) => {
const actions: any[] = [] const actions: any[] = []
const voucherKeys = toVoucherKeyList(row.refund_voucher_key) const voucherKeys = getRefundAttachmentKeys(row)
// 待审批状态可以打回重填、审批通过或审批拒绝 if (canResubmit(row) && hasAuth('refund:resubmit')) {
if (row.status === 1) {
// 审批通过
if (hasAuth('refund:approve')) {
actions.push({ actions.push({
label: '审批通过', label: '重新申请',
handler: () => handleShowApprove(row),
type: 'primary'
})
}
// 审批拒绝
if (hasAuth('refund:reject')) {
actions.push({
label: '审批拒绝',
handler: () => handleShowReject(row),
type: 'danger'
})
}
// 驳回
if (hasAuth('refund:return')) {
actions.push({
label: '驳回',
handler: () => handleShowReturn(row),
type: 'primary'
})
}
}
// 只有已退回状态可以重新提交
if (row.status === 4 && hasAuth('refund:resubmit')) {
actions.push({
label: '重新提交',
handler: () => handleShowResubmit(row), handler: () => handleShowResubmit(row),
type: 'primary' type: 'primary'
}) })
@@ -994,22 +725,14 @@
} }
const handleViewRefundVoucher = (row: Refund) => { const handleViewRefundVoucher = (row: Refund) => {
const voucherKey = row.refund_voucher_key const voucherKeys = getRefundAttachmentKeys(row)
if (!hasVoucherKeys(voucherKey)) return if (!voucherKeys.length) return
refundVoucherFileKeys.value = toVoucherKeyList(voucherKey) refundVoucherFileKeys.value = voucherKeys
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.refund-page { .refund-page {
height: 100%; height: 100%;
.return-dialog-tip {
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
color: var(--el-color-primary);
text-align: center;
}
} }
</style> </style>