fix: some

This commit is contained in:
luo
2026-09-17 12:16:20 +08:00
parent 2308d82d0f
commit 76d4e4ec02
126 changed files with 11671 additions and 294 deletions

View File

@@ -0,0 +1,269 @@
<template>
<ElDialog
:model-value="modelValue"
title="佣金明细详情"
width="780px"
@update:model-value="handleVisibleChange"
@closed="handleDialogClosed"
>
<div v-loading="loading" class="commission-record-detail">
<template v-if="record">
<ElDescriptions :column="2" border size="small">
<ElDescriptionsItem label="记录类型">
<ElTag :type="isClawback ? 'danger' : 'primary'" size="small">
{{ recordSourceName }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="statusTagType" size="small">
{{ record.status_name || statusName }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="佣金金额">
<span :class="{ 'amount-negative': isNegative(record.amount) }">
{{ formatMoney(record.amount) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="入账后佣金余额">
<span :class="{ 'amount-negative': isNegative(record.balance_after) }">
{{ formatMoney(record.balance_after) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="佣金来源">
<ElTag :type="sourceTagType" size="small">{{ sourceName }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="是否可提现">
<ElTag v-if="isClawback" type="danger" size="small">不可提现</ElTag>
<span v-else>可提现</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="订单号">{{ record.order_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="卖家店铺">
{{ record.seller_shop_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="ICCID">{{ record.iccid || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备虚拟号">
{{ record.virtual_no || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="记录生成时间">
{{ record.created_at ? formatDateTime(record.created_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="佣金入账时间">
{{ record.released_at ? formatDateTime(record.released_at) : '-' }}
</ElDescriptionsItem>
<template v-if="isClawback">
<ElDescriptionsItem label="被回溯原佣金ID">
{{ record.original_commission_id ?? '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="来源退款单号">
{{ record.refund_no || '-' }}
</ElDescriptionsItem>
</template>
</ElDescriptions>
<template v-if="!isClawback">
<div class="detail-section-title">
回溯明细
<span class="detail-section-summary">
累计回溯金额:{{ formatMoney(clawbackTotalAmount) }}
</span>
</div>
<ElTable :data="clawbackRecords" size="small" border :max-height="260">
<ElTableColumn label="回溯金额" prop="amount" width="120">
<template #default="{ row }">
<span class="amount-negative">{{ formatMoney(row.amount) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="回溯后余额" prop="balance_after" width="130">
<template #default="{ row }">
<span :class="{ 'amount-negative': isNegative(row.balance_after) }">
{{ formatMoney(row.balance_after) }}
</span>
</template>
</ElTableColumn>
<ElTableColumn
label="来源退款单号"
prop="refund_no"
min-width="180"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.refund_no || '-' }}</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status_name" width="100">
<template #default="{ row }">
<ElTag type="danger" size="small">{{ row.status_name || '回溯' }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="生成时间" prop="created_at" width="180">
<template #default="{ row }">
{{ row.created_at ? formatDateTime(row.created_at) : '-' }}
</template>
</ElTableColumn>
</ElTable>
</template>
<template v-else-if="originalCommission">
<div class="detail-section-title">来源原佣金</div>
<ElDescriptions :column="2" border size="small">
<ElDescriptionsItem label="原佣金记录ID">
{{ originalCommission.id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="原佣金金额">
{{ formatMoney(originalCommission.amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="原佣金余额">
{{ formatMoney(originalCommission.balance_after) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="原佣金状态">
{{ originalCommission.status_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="订单号">
{{ originalCommission.order_no || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="佣金入账时间">
{{
originalCommission.released_at
? formatDateTime(originalCommission.released_at)
: '-'
}}
</ElDescriptionsItem>
</ElDescriptions>
</template>
</template>
<ElEmpty v-else-if="!loading" description="佣金明细不存在" />
</div>
</ElDialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { CommissionService } from '@/api/modules'
import type { ShopCommissionRecordDetail, ShopCommissionRecordSource } from '@/types/api'
import {
COMMISSION_RECORD_SOURCE_MAP,
CommissionSourceMap,
CommissionStatusMap,
isCommissionClawbackRecord
} from '@/config/constants/commission'
import { formatDateTime, formatMoney } from '@/utils/business/format'
import { getErrorMessage } from '@/utils/business'
defineOptions({ name: 'CommissionRecordDetailDialog' })
const props = withDefaults(
defineProps<{
modelValue: boolean
shopId?: number | null
recordId?: number | null
source?: ShopCommissionRecordSource
}>(),
{
shopId: null,
recordId: null,
source: 'original'
}
)
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const loading = ref(false)
const detail = ref<ShopCommissionRecordDetail | null>(null)
const record = computed(() => detail.value?.record || null)
const isClawback = computed(() =>
isCommissionClawbackRecord({
source: detail.value?.source || props.source,
status: detail.value?.record?.status
})
)
const commissionStatusMeta = computed(
() =>
CommissionStatusMap[Number(record.value?.status) as keyof typeof CommissionStatusMap] || null
)
const commissionSourceMeta = computed(
() =>
CommissionSourceMap[
(record.value?.commission_source || '') as keyof typeof CommissionSourceMap
] || null
)
const recordSourceMeta = computed(
() => COMMISSION_RECORD_SOURCE_MAP[isClawback.value ? 'clawback' : 'original']
)
const recordSourceName = computed(() => recordSourceMeta.value.label)
const statusName = computed(() => commissionStatusMeta.value?.label || '-')
const statusTagType = computed(() => commissionStatusMeta.value?.type || 'info')
const sourceName = computed(
() => commissionSourceMeta.value?.label || record.value?.commission_source || '-'
)
const sourceTagType = computed(() => commissionSourceMeta.value?.type || 'info')
const clawbackRecords = computed(() => detail.value?.clawback_records ?? [])
const clawbackTotalAmount = computed(() => record.value?.clawback_total_amount ?? 0)
const originalCommission = computed(() => detail.value?.original_commission || null)
const isNegative = (value?: number | null) => typeof value === 'number' && value < 0
const loadDetail = async () => {
if (!props.shopId || !props.recordId) return
loading.value = true
detail.value = null
try {
const res = await CommissionService.getShopCommissionRecordDetail(
props.shopId,
props.recordId,
props.source
)
if (res.code === 0) {
detail.value = res.data
} else {
ElMessage.error(res.msg || '佣金明细不存在')
}
} catch (error) {
ElMessage.error(getErrorMessage(error, '佣金明细不存在'))
} finally {
loading.value = false
}
}
const handleVisibleChange = (value: boolean) => {
emit('update:modelValue', value)
}
const handleDialogClosed = () => {
detail.value = null
}
watch(
() => [props.modelValue, props.shopId, props.recordId, props.source] as const,
([visible]) => {
if (visible) loadDetail()
}
)
</script>
<style scoped lang="scss">
.commission-record-detail {
min-height: 120px;
}
.amount-negative {
color: var(--el-color-danger);
}
.detail-section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
}
.detail-section-summary {
font-size: 12px;
font-weight: 400;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @closed="handleDialogClosed">
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<ElDialog v-model="dialogVisible" title="创建退款申请" width="45%" @closed="handleDialogClosed">
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<ElFormItem label="订单号" prop="order_id">
<ElSelect
v-if="!hasInitialOrder"
@@ -23,6 +23,13 @@
</ElSelect>
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem label="退款方式" prop="method">
<ElRadioGroup v-model="formData.method">
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
{{ opt.label }}
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="退款金额" prop="requested_refund_amount">
<ElInputNumber
v-model="formData.requested_refund_amount"
@@ -46,7 +53,21 @@
placeholder="请输入退款原因"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key">
<template v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT">
<ElFormItem label="收款账户名" prop="customer_account_name">
<ElInput v-model="formData.customer_account_name" placeholder="请输入收款账户名" maxlength="50" />
</ElFormItem>
<ElFormItem label="收款账号" prop="customer_account_number">
<ElInput v-model="formData.customer_account_number" placeholder="请输入收款账号" maxlength="64" />
</ElFormItem>
<ElFormItem label="开户银行">
<ElInput v-model="formData.customer_bank_name" placeholder="选填" maxlength="50" />
</ElFormItem>
</template>
<ElFormItem
:label="formData.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'"
prop="refund_voucher_key"
>
<VoucherUpload
ref="uploadRef"
v-model="formData.refund_voucher_key"
@@ -54,6 +75,9 @@
@uploading-change="voucherUploading = $event"
@change="formRef?.validateField('refund_voucher_key')"
/>
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
客户收款信息方式下须同时提供客户收款信息与至少 1 个退款凭证
</div>
</ElFormItem>
</ElForm>
<template #footer>
@@ -77,7 +101,8 @@
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { RefundService, OrderService } from '@/api/modules'
import type { CreateRefundRequest, Order } from '@/types/api'
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
import { RefundMethodOptions } from '@/config/constants/refund'
import { formatMoney, yuanToFen } from '@/utils/business/format'
import { getErrorMessage } from '@/utils/business'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
@@ -110,16 +135,24 @@
const formData = reactive<{
order_id: number | null
package_usage_id?: number
method?: RefundMethod
requested_refund_amount?: number
actual_received_amount: number
refund_reason: string
customer_account_name: string
customer_account_number: string
customer_bank_name: string
refund_voucher_key: string[]
}>({
order_id: null,
package_usage_id: undefined,
method: RefundMethod.ORIGINAL_ROUTE,
requested_refund_amount: undefined,
actual_received_amount: 0,
refund_reason: '',
customer_account_name: '',
customer_account_number: '',
customer_bank_name: '',
refund_voucher_key: []
})
@@ -134,14 +167,43 @@
}
}
const requireCustomerAccount = (field: string, message: string) => ({
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
callback(new Error(message))
} else {
callback()
}
},
trigger: 'blur'
})
const validateVoucherKey = (
_rule: unknown,
value: string[],
callback: (error?: Error) => void
) => {
if (
formData.method === RefundMethod.CUSTOMER_ACCOUNT &&
(!Array.isArray(value) || value.length === 0)
) {
callback(new Error('请上传至少 1 个退款凭证'))
} else {
callback()
}
}
const formRules = reactive<FormRules>({
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
requested_refund_amount: [
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
{ validator: validateRefundAmount, trigger: 'blur' }
],
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
refund_voucher_key: [{ required: true, message: '请上传退款凭证', trigger: 'change' }]
customer_account_name: requireCustomerAccount('customer_account_name', '请填写收款账户名'),
customer_account_number: requireCustomerAccount('customer_account_number', '请填写收款账号'),
refund_voucher_key: [{ validator: validateVoucherKey, trigger: 'change' }]
})
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
@@ -202,9 +264,13 @@
voucherUploading.value = false
formData.order_id = null
formData.package_usage_id = undefined
formData.method = RefundMethod.ORIGINAL_ROUTE
formData.requested_refund_amount = undefined
formData.actual_received_amount = 0
formData.refund_reason = ''
formData.customer_account_name = ''
formData.customer_account_number = ''
formData.customer_bank_name = ''
formData.refund_voucher_key = []
orderSearchOptions.value = []
uploadRef.value?.clearFiles(false)
@@ -222,20 +288,30 @@
submitLoading.value = true
try {
const data: CreateRefundRequest = {
actual_received_amount: formData.actual_received_amount,
order_id: formData.order_id!,
method: formData.method!,
package_usage_id: formData.package_usage_id ?? 0,
refund_reason: formData.refund_reason,
refund_voucher_key: formData.refund_voucher_key,
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
}
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT) {
data.customer_account_info = {
account_name: formData.customer_account_name.trim(),
account_number: formData.customer_account_number.trim(),
...(formData.customer_bank_name.trim()
? { bank_name: formData.customer_bank_name.trim() }
: {})
}
data.refund_voucher_key = formData.refund_voucher_key
} else if (formData.refund_voucher_key.length) {
data.refund_voucher_key = formData.refund_voucher_key
}
await RefundService.createRefund(data)
ElMessage.success('退款申请创建成功')
dialogVisible.value = false
emit('success')
} catch (error) {
console.error(error)
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
} finally {
submitLoading.value = false
}
@@ -257,3 +333,12 @@
}
})
</script>
<style scoped lang="scss">
.voucher-tip {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<ElDialog
:model-value="modelValue"
:title="dialogTitle"
width="480px"
@update:model-value="handleVisibleChange"
@closed="handleDialogClosed"
>
<template v-if="action === 'approve'">
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ refund?.refund_no || '-' }}</span>
</ElFormItem>
<ElFormItem label="审批退款金额">
<ElInputNumber
v-model="approveForm.approved_refund_amount"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则按申请金额审批"
/>
</ElFormItem>
<ElFormItem label="审批备注">
<ElInput
v-model="approveForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入审批备注(选填)"
/>
</ElFormItem>
</ElForm>
</template>
<template v-else-if="action === 'reject'">
<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="1000"
show-word-limit
placeholder="请输入拒绝原因"
/>
</ElFormItem>
</ElForm>
</template>
<template v-else>
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ refund?.refund_no || '-' }}</span>
</ElFormItem>
<ElFormItem label="退回备注">
<ElInput
v-model="returnForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入退回备注(选填)"
/>
</ElFormItem>
</ElForm>
</template>
<template #footer>
<div class="dialog-footer">
<ElButton @click="handleVisibleChange(false)">取消</ElButton>
<ElButton type="primary" :loading="submitLoading" @click="handleConfirm">
{{ confirmText }}
</ElButton>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { RefundService } from '@/api/modules'
import type { Refund } from '@/types/api'
import { yuanToFen } from '@/utils/business/format'
defineOptions({ name: 'RefundApprovalDialog' })
type ApprovalAction = 'approve' | 'reject' | 'return'
const props = defineProps<{
modelValue: boolean
refund: Refund | null
action: ApprovalAction
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const submitLoading = ref(false)
const approveFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const returnFormRef = ref<FormInstance>()
const approveForm = reactive<{
approved_refund_amount?: number
remark: string
}>({
approved_refund_amount: undefined,
remark: ''
})
const rejectForm = reactive<{
reject_reason: string
}>({
reject_reason: ''
})
const returnForm = reactive<{
remark: string
}>({
remark: ''
})
const approveRules = reactive<FormRules>({})
const rejectRules = reactive<FormRules>({
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const returnRules = reactive<FormRules>({})
const dialogTitle = computed(() => {
if (props.action === 'approve') return '审批通过退款申请'
if (props.action === 'reject') return '审批拒绝退款申请'
return '退回退款申请'
})
const confirmText = computed(() => {
if (props.action === 'approve') return '确认通过'
if (props.action === 'reject') return '确认拒绝'
return '确认退回'
})
const handleVisibleChange = (visible: boolean) => {
emit('update:modelValue', visible)
}
const handleDialogClosed = () => {
approveForm.approved_refund_amount = undefined
approveForm.remark = ''
rejectForm.reject_reason = ''
returnForm.remark = ''
submitLoading.value = false
}
const handleConfirm = async () => {
if (!props.refund || submitLoading.value) return
let valid = true
if (props.action === 'approve') {
valid = await approveFormRef.value?.validate().catch(() => false) !== false
} else if (props.action === 'reject') {
valid = await rejectFormRef.value?.validate().catch(() => false) !== false
} else {
valid = await returnFormRef.value?.validate().catch(() => false) !== false
}
if (!valid) return
submitLoading.value = true
try {
const refundId = props.refund.id
if (props.action === 'approve') {
await RefundService.approveRefund(refundId, {
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
remark: approveForm.remark.trim() || undefined
})
ElMessage.success('审批通过成功')
} else if (props.action === 'reject') {
await RefundService.rejectRefund(refundId, {
reject_reason: rejectForm.reject_reason.trim()
})
ElMessage.success('审批拒绝成功')
} else {
await RefundService.returnRefund(refundId, {
remark: returnForm.remark.trim() || undefined
})
ElMessage.success('退回成功')
}
emit('update:modelValue', false)
emit('success')
} catch (error) {
console.error(error)
ElMessage.error('操作失败,请稍后重试')
} finally {
submitLoading.value = false
}
}
</script>

View File

@@ -71,7 +71,7 @@
<!-- 分页 -->
<div
v-if="pagination && tableData.length > 0"
v-if="pagination && (alwaysShowPagination || tableData.length > 0)"
class="table-pagination"
:class="paginationAlign"
>
@@ -113,7 +113,7 @@
/** 是否显示加载状态 */
loading?: boolean
/** 行数据的 Key,用于标识每一行数据 */
rowKey?: string
rowKey?: string | ((row: any) => string)
/** 行的 className 的回调方法 */
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
/** 是否显示边框 */
@@ -144,6 +144,8 @@
pageSizes?: number[]
/** 只有一页时是否隐藏分页器 */
hideOnSinglePage?: boolean
/** 数据为空时也始终显示分页器 */
alwaysShowPagination?: boolean
/** 分页器的对齐方式 */
paginationAlign?: 'left' | 'center' | 'right'
/** 分页器的大小 */
@@ -185,6 +187,7 @@
currentPage: 1,
pageSize: 20,
hideOnSinglePage: false,
alwaysShowPagination: false,
pageSizes: () => [10, 20, 30, 50],
paginationAlign: 'center',
paginationSize: 'default',