This commit is contained in:
@@ -24,7 +24,7 @@ The system SHALL provide a paginated list of all commission withdrawal requests
|
||||
|
||||
### Requirement: Withdrawal Request Approval
|
||||
|
||||
The system SHALL allow administrators to approve withdrawal requests.
|
||||
The system SHALL allow administrators to approve withdrawal requests using the approval API contract. The administrator SHALL enter the remark manually, and the system SHALL submit the request's account name, account number, amount, and withdrawal method from the selected list item together with the manual payment type.
|
||||
|
||||
#### Scenario: Approve withdrawal request
|
||||
|
||||
@@ -32,6 +32,13 @@ The system SHALL allow administrators to approve withdrawal requests.
|
||||
- **THEN** the system marks the request as approved
|
||||
- **AND** updates the processor information and timestamp
|
||||
|
||||
#### Scenario: Submit approval details
|
||||
|
||||
- **WHEN** administrator confirms approval after optionally entering a remark
|
||||
- **THEN** the system submits the selected request's account name, account number, amount, and withdrawal method with `payment_type` set to `manual`
|
||||
- **AND** submits the manually entered remark
|
||||
- **AND** refreshes the list after a successful response
|
||||
|
||||
### Requirement: Withdrawal Request Rejection
|
||||
|
||||
The system SHALL allow administrators to reject withdrawal requests with a reason.
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
- [x] 5.6 Implement approve action with confirmation dialog
|
||||
- [x] 5.7 Implement reject action with reason input dialog
|
||||
- [x] 5.8 Add status badges using unified status components
|
||||
- [x] 5.9 Submit approval request details from the selected list item and collect a manual remark
|
||||
|
||||
## 6. Withdrawal Settings Module
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
WithdrawalRequestQueryParams,
|
||||
WithdrawalRequestPageResult,
|
||||
ApproveWithdrawalParams,
|
||||
WithdrawalApprovalResponse,
|
||||
RejectWithdrawalParams,
|
||||
WithdrawalSettingListResult,
|
||||
WithdrawalSettingItem,
|
||||
@@ -46,8 +47,11 @@ export class CommissionService extends BaseService {
|
||||
* 审批通过提现申请
|
||||
* POST /api/admin/commission/withdrawal-requests/{id}/approve
|
||||
*/
|
||||
static approveWithdrawal(id: number, params?: ApproveWithdrawalParams): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(
|
||||
static approveWithdrawal(
|
||||
id: number,
|
||||
params: ApproveWithdrawalParams
|
||||
): Promise<BaseResponse<WithdrawalApprovalResponse>> {
|
||||
return this.post<BaseResponse<WithdrawalApprovalResponse>>(
|
||||
`/api/admin/commission/withdrawal-requests/${id}/approve`,
|
||||
params
|
||||
)
|
||||
|
||||
@@ -88,7 +88,23 @@ export interface WithdrawalRequestQueryParams extends PaginationParams {
|
||||
* 审批提现申请参数
|
||||
*/
|
||||
export interface ApproveWithdrawalParams {
|
||||
account_name?: string | null // 修正后的收款人姓名
|
||||
account_number?: string | null // 修正后的收款账号
|
||||
amount?: number | null // 修正后的提现金额(分)
|
||||
payment_type: PaymentType // 放款类型
|
||||
remark?: string // 审批备注
|
||||
withdrawal_method?: WithdrawalMethod | null // 修正后的收款类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批提现申请响应
|
||||
*/
|
||||
export interface WithdrawalApprovalResponse {
|
||||
id?: number // 提现申请ID
|
||||
processed_at?: string // 处理时间
|
||||
status?: WithdrawalStatus // 状态
|
||||
status_name?: string // 状态名称
|
||||
withdrawal_no?: string // 提现单号
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
src/types/components.d.ts
vendored
7
src/types/components.d.ts
vendored
@@ -93,11 +93,9 @@ declare module 'vue' {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
@@ -115,6 +113,7 @@ declare module 'vue' {
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
@@ -122,13 +121,13 @@ declare module 'vue' {
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -592,7 +592,8 @@
|
||||
{
|
||||
prop: 'username',
|
||||
label: '账号名称',
|
||||
minWidth: 120
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
@@ -617,6 +618,7 @@
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: any) => {
|
||||
return row.shop_name || '-'
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
import type {
|
||||
AssetBoundCard,
|
||||
AssetWalletResponse,
|
||||
@@ -32,6 +33,53 @@ const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
||||
|
||||
// 详情接口是绑定卡实名时间的唯一来源;实时状态接口不应覆盖或保留旧值。
|
||||
const DETAIL_ONLY_BOUND_CARD_FIELDS = new Set(['real_name_at'])
|
||||
const ASSET_NOT_FOUND_CODE = 1006
|
||||
|
||||
const isLegacyNineteenDigitIdentifier = (identifier: string) => /^89\d{17}$/.test(identifier)
|
||||
|
||||
const isAssetNotFound = (value: any) =>
|
||||
value?.code === ASSET_NOT_FOUND_CODE || value?.response?.data?.code === ASSET_NOT_FOUND_CODE
|
||||
|
||||
const isUnauthorized = (value: any) =>
|
||||
value?.code === 401 || value?.response?.status === 401 || value?.response?.data?.code === 401
|
||||
|
||||
const calculateLuhnCheckDigit = (identifier: string) => {
|
||||
let sum = 0
|
||||
let doubleDigit = true
|
||||
|
||||
for (let index = identifier.length - 1; index >= 0; index -= 1) {
|
||||
let digit = Number(identifier[index])
|
||||
if (doubleDigit) {
|
||||
digit *= 2
|
||||
if (digit > 9) digit -= 9
|
||||
}
|
||||
sum += digit
|
||||
doubleDigit = !doubleDigit
|
||||
}
|
||||
|
||||
return String((10 - (sum % 10)) % 10)
|
||||
}
|
||||
|
||||
const resolveAssetForAssetInfo = async (identifier: string) => {
|
||||
const params = { include_usage_summary: true }
|
||||
if (!isLegacyNineteenDigitIdentifier(identifier)) {
|
||||
return AssetService.resolveAsset(identifier, params)
|
||||
}
|
||||
|
||||
const silentConfig = { requestOptions: { errorMessageMode: 'none' as const } }
|
||||
let firstResponse = null
|
||||
|
||||
try {
|
||||
firstResponse = await AssetService.resolveAsset(identifier, params, silentConfig)
|
||||
} catch (error) {
|
||||
if (!isAssetNotFound(error)) throw error
|
||||
}
|
||||
|
||||
if (firstResponse && !isAssetNotFound(firstResponse)) return firstResponse
|
||||
|
||||
const correctedIdentifier = `${identifier}${calculateLuhnCheckDigit(identifier)}`
|
||||
return AssetService.resolveAsset(correctedIdentifier, params, silentConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数据大小(MB -> GB/MB)
|
||||
@@ -96,9 +144,10 @@ export function useAssetInfo() {
|
||||
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
|
||||
*/
|
||||
const fetchAssetDetail = async (identifier: string) => {
|
||||
const shouldShowSilentRequestError = isLegacyNineteenDigitIdentifier(identifier)
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await AssetService.resolveAsset(identifier, { include_usage_summary: true })
|
||||
const response = await resolveAssetForAssetInfo(identifier)
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data
|
||||
@@ -199,8 +248,12 @@ export function useAssetInfo() {
|
||||
ElMessage.error(response.msg || '查询失败')
|
||||
cardInfo.value = null
|
||||
}
|
||||
} catch {
|
||||
} catch (error: any) {
|
||||
cardInfo.value = null
|
||||
if (shouldShowSilentRequestError && !isUnauthorized(error)) {
|
||||
const backendMessage = error?.response?.data?.msg
|
||||
ElMessage.error(backendMessage || normalizeApiError(error).message)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -409,12 +409,14 @@
|
||||
|
||||
// fetchAssetDetail 内部已经会并行加载所有相关数据,不需要重复调用
|
||||
await fetchAssetDetail(identifier)
|
||||
if (!cardInfo.value) return
|
||||
|
||||
// 如果是设备类型,需要再次调用搜索接口获取完整数据
|
||||
if (cardInfo.value?.asset_type === 'device') {
|
||||
const deviceIdentifier = cardInfo.value.virtual_no || cardInfo.value.iccid || identifier
|
||||
await fetchAssetDetail(deviceIdentifier)
|
||||
}
|
||||
if (!cardInfo.value) return
|
||||
|
||||
if (cardInfo.value) {
|
||||
walletTransactionSearchToken.value += 1
|
||||
@@ -428,7 +430,8 @@
|
||||
|
||||
// 重置分页并加载套餐列表
|
||||
packagePagination.value.page = 1
|
||||
const result = await loadPackageList(identifier, 1, packagePagination.value.pageSize)
|
||||
const resolvedIdentifier = cardInfo.value.identifier || identifier
|
||||
const result = await loadPackageList(resolvedIdentifier, 1, packagePagination.value.pageSize)
|
||||
packagePagination.value.total = result.total
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,30 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog v-model="approveDialogVisible" title="审批通过提现申请" width="40%">
|
||||
<ElForm :model="approveForm">
|
||||
<ElFormItem label="备注信息" prop="remark">
|
||||
<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">{{ $t('common.cancel') }}</ElButton>
|
||||
<ElButton type="primary" @click="handleApproveSubmit" :loading="approveSubmitLoading">
|
||||
{{ $t('common.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -77,28 +101,30 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { CommissionService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
WithdrawalRequestItem,
|
||||
WithdrawalStatus,
|
||||
WithdrawalMethod
|
||||
} from '@/types/api/commission'
|
||||
import { PaymentType } from '@/types/api/commission'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import { WithdrawalStatusMap, WithdrawalMethodMap } from '@/config/constants'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'WithdrawalApproval' })
|
||||
|
||||
const { t } = useI18n() // 仅保留用于 common.tips, common.confirm, common.cancel
|
||||
|
||||
const rejectDialogVisible = ref(false)
|
||||
const approveDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const approveSubmitLoading = ref(false)
|
||||
const rejectSubmitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const currentWithdrawalId = ref<number>(0)
|
||||
const currentWithdrawal = ref<WithdrawalRequestItem | null>(null)
|
||||
const approveForm = reactive({ remark: '' })
|
||||
|
||||
// 店铺选项
|
||||
const shopOptions = ref<any[]>([])
|
||||
@@ -216,7 +242,8 @@
|
||||
{
|
||||
prop: 'withdrawal_no',
|
||||
label: '提现单号',
|
||||
minWidth: 180
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
@@ -232,21 +259,18 @@
|
||||
prop: 'amount',
|
||||
label: '提现金额',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
formatter: (row: WithdrawalRequestItem) => formatMoney(row.amount)
|
||||
},
|
||||
{
|
||||
prop: 'fee',
|
||||
label: '手续费',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
formatter: (row: WithdrawalRequestItem) => formatMoney(row.fee)
|
||||
},
|
||||
{
|
||||
prop: 'actual_amount',
|
||||
label: '实际到账',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
formatter: (row: WithdrawalRequestItem) => formatMoney(row.actual_amount)
|
||||
},
|
||||
{
|
||||
@@ -367,25 +391,36 @@
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 审批通过
|
||||
const handleApprove = (row: WithdrawalRequestItem) => {
|
||||
ElMessageBox.confirm('确定要通过该提现申请吗?', t('common.tips'), {
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await CommissionService.approveWithdrawal(row.id)
|
||||
ElMessage.success('审批通过成功')
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
// 显示审批通过对话框
|
||||
const showApproveDialog = (row: WithdrawalRequestItem) => {
|
||||
currentWithdrawal.value = row
|
||||
approveForm.remark = ''
|
||||
approveDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交审批通过
|
||||
const handleApproveSubmit = async () => {
|
||||
const row = currentWithdrawal.value
|
||||
if (!row) return
|
||||
|
||||
approveSubmitLoading.value = true
|
||||
try {
|
||||
await CommissionService.approveWithdrawal(row.id, {
|
||||
account_name: row.account_name,
|
||||
account_number: row.account_number,
|
||||
amount: row.amount,
|
||||
payment_type: PaymentType.MANUAL,
|
||||
remark: approveForm.remark,
|
||||
withdrawal_method: row.withdrawal_method
|
||||
})
|
||||
ElMessage.success('审批通过成功')
|
||||
approveDialogVisible.value = false
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
approveSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示拒绝对话框
|
||||
@@ -428,7 +463,7 @@
|
||||
return [
|
||||
{
|
||||
label: '审批通过',
|
||||
handler: () => handleApprove(row),
|
||||
handler: () => showApproveDialog(row),
|
||||
type: 'primary' as const
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user