feat: 手动实名和套餐列表退款
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m53s

This commit is contained in:
sexygoat
2026-04-23 18:13:07 +08:00
parent 719db517f2
commit cd2ca96873
12 changed files with 539 additions and 180 deletions

View File

@@ -174,7 +174,7 @@
<ElTableColumn prop="carrier_name" label="运营商" min-width="120" />
<ElTableColumn prop="slot_position" label="卡槽位置" width="130">
<template #default="scope">
<div style="display: flex; gap: 10px; align-items: center; justify-content: center">
<div style="display: flex; gap: 10px">
<span>SIM-{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="primary" size="small">当前</ElTag>
</div>
@@ -204,7 +204,7 @@
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="120">
<ElTableColumn label="操作" width="180">
<template #default="scope">
<!-- 根据网络状态显示启用或停用按钮 -->
<ElButton
@@ -227,6 +227,15 @@
>
停用此卡
</ElButton>
<ElButton
type="primary"
size="small"
link
@click="handleUpdateBindingCardRealnameStatus(scope.row)"
v-permission="'bound_card:update_realname_status'"
>
更新实名状态
</ElButton>
</template>
</ElTableColumn>
</ElTable>
@@ -395,6 +404,13 @@
>
实名认证策略
</ElButton>
<ElButton
v-permission="'iot_info:update_realname_status'"
type="primary"
@click="handleShowUpdateRealnameStatus"
>
更新实名状态
</ElButton>
</div>
<!-- 设备操作按钮 -->
@@ -575,8 +591,10 @@
(e: 'showSwitchMode'): void
(e: 'show-set-wifi'): void
(e: 'showRealnamePolicy'): void
(e: 'showUpdateRealnameStatus'): void
(e: 'enableBindingCard', payload: { card: BindingCard }): void
(e: 'disableBindingCard', payload: { card: BindingCard }): void
(e: 'updateBindingCardRealnameStatus', payload: { card: BindingCard }): void
(e: 'navigateToCard', iccid: string): void
(e: 'navigateToDevice', deviceNo: string): void
}
@@ -680,6 +698,10 @@
emit('showRealnamePolicy')
}
const handleShowUpdateRealnameStatus = () => {
emit('showUpdateRealnameStatus')
}
// 绑定卡操作
const handleEnableBindingCard = (card: BindingCard) => {
ElMessageBox.confirm(`确定要启用此卡(${card.iccid})吗?`, '启用确认', {
@@ -704,6 +726,10 @@
})
.catch(() => {})
}
const handleUpdateBindingCardRealnameStatus = (card: BindingCard) => {
emit('updateBindingCardRealnameStatus', { card })
}
</script>
<style scoped lang="scss">

View File

@@ -77,6 +77,9 @@
<!-- 套餐详情表格 -->
<ElDescriptions :column="3" border class="package-info-table">
<ElDescriptionsItem label="订单号">
{{ currentPackage.order_no || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>

View File

@@ -25,6 +25,7 @@
</ElEmpty>
<div v-else-if="packageList && packageList.length > 0" class="table-scroll-container">
<ElTable :data="packageList" class="package-table" border>
<ElTableColumn prop="order_no" label="订单号" width="180" show-overflow-tooltip />
<ElTableColumn label="套餐名称" width="150">
<template #default="scope">
<ElButton
@@ -54,7 +55,7 @@
{{ formatDataSize(scope.row.data_limit_mb) }}
</template>
</ElTableColumn>
<ElTableColumn label="流量" min-width="220">
<ElTableColumn label="流量" min-width="320">
<template #default="scope">
<div class="traffic-progress">
<div class="progress-text">
@@ -107,17 +108,29 @@
{{ formatDateTime(scope.row.expires_at) }}
</template>
</ElTableColumn>
<ElTableColumn prop="created_at" label="创建时间" width="170" show-overflow-tooltip>
<ElTableColumn prop="created_at" label="购买时间" width="170" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="100" fixed="right">
<template #default="scope">
<ElButton
v-if="scope.row.status !== 4 && hasAuth('package:create_refund')"
type="primary"
link
@click="handleCreateRefund(scope.row)"
>
退款申请
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElPagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
v-model:page-size="pagination.page_size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
:page-sizes="[5, 10, 20]"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@@ -126,6 +139,13 @@
</div>
<ElEmpty v-else-if="!props.boundDeviceId" description="暂无套餐数据" :image-size="100" />
</div>
<!-- 退款申请对话框 -->
<CreateRefundDialog
v-model="createRefundDialogVisible"
:initial-order-no="currentRefundOrderNo"
@success="handleRefundSuccess"
/>
</ElCard>
</template>
@@ -147,9 +167,27 @@
import { formatDateTime } from '@/utils/business/format'
import type { AssetPackageUsageRecord } from '@/types/api'
import { useAuth } from '@/composables/useAuth'
import CreateRefundDialog from '@/components/business/CreateRefundDialog.vue'
const { hasAuth } = useAuth()
// 退款申请对话框
const createRefundDialogVisible = ref(false)
const currentRefundOrderNo = ref<string | undefined>(undefined)
const handleCreateRefund = (row: PackageInfo) => {
if (!hasAuth('package:create_refund')) {
ElMessage.warning('您没有创建退款申请的权限')
return
}
currentRefundOrderNo.value = row.order_no
createRefundDialogVisible.value = true
}
const handleRefundSuccess = () => {
emit('refresh')
}
// Props 使用 API 类型
type PackageInfo = AssetPackageUsageRecord
@@ -179,6 +217,7 @@
(e: 'page-change', page: number): void
(e: 'size-change', size: number): void
(e: 'navigateToDevice', deviceNo: string): void
(e: 'refresh'): void
}
const emit = defineEmits<Emits>()
@@ -186,7 +225,7 @@
// 分页状态
const pagination = ref({
page: props.page,
pageSize: props.pageSize,
page_size: props.pageSize,
total: props.total
})
@@ -195,7 +234,7 @@
() => [props.page, props.pageSize, props.total],
([newPage, newPageSize, newTotal]) => {
pagination.value.page = newPage
pagination.value.pageSize = newPageSize
pagination.value.page_size = newPageSize
pagination.value.total = newTotal
}
)

View File

@@ -27,8 +27,10 @@
@show-switch-mode="showSwitchModeDialog"
@show-set-wifi="showSetWiFiDialog"
@show-realname-policy="showRealnamePolicyDialog"
@show-update-realname-status="showUpdateRealnameStatusDialog"
@enable-binding-card="handleEnableBindingCard"
@disable-binding-card="handleDisableBindingCard"
@update-binding-card-realname-status="handleUpdateBindingCardRealnameStatus"
@navigate-to-card="handleNavigateToCard"
@navigate-to-device="handleNavigateToDevice"
/>
@@ -63,6 +65,7 @@
@page-change="handlePackagePageChange"
@size-change="handlePackageSizeChange"
@navigate-to-device="handleNavigateToDevice"
@refresh="handlePackageRefresh"
/>
</div>
@@ -127,6 +130,18 @@
:current-policy="cardInfo?.realname_policy"
@confirm="handleConfirmRealnamePolicy"
/>
<UpdateRealnameStatusDialog
v-model="updateRealnameStatusDialogVisible"
:asset-identifier="cardInfo?.identifier || ''"
:current-realname-status="cardInfo?.real_name_status"
@success="handleUpdateRealnameStatusSuccess"
/>
<UpdateRealnameStatusDialog
v-model="bindingCardRealnameStatusDialogVisible"
:asset-identifier="bindingCardRealnameStatusIccid"
:current-realname-status="bindingCardRealnameStatusValue"
@success="handleBindingCardRealnameStatusSuccess"
/>
</div>
</template>
@@ -152,6 +167,7 @@
} from './components/dialogs'
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import UpdateRealnameStatusDialog from '@/components/business/UpdateRealnameStatusDialog.vue'
// 引入 composables
import { useAssetInfo } from './composables/useAssetInfo'
@@ -211,12 +227,18 @@
const dailyRecordsDialogVisible = ref(false)
const orderHistoryDialogVisible = ref(false)
const realnamePolicyDialogVisible = ref(false)
const updateRealnameStatusDialogVisible = ref(false)
const selectedPackageUsageId = ref<number | null>(null)
// 绑定卡实名状态更新
const bindingCardRealnameStatusDialogVisible = ref(false)
const bindingCardRealnameStatusIccid = ref('')
const bindingCardRealnameStatusValue = ref<number>(0)
// ========== 套餐列表分页状态 ==========
const packagePagination = ref({
page: 1,
pageSize: 10,
pageSize: 5,
total: 0
})
@@ -254,7 +276,7 @@
// 重置分页并加载套餐列表
packagePagination.value.page = 1
const result = await loadPackageList(identifier, 1, packagePagination.pageSize)
const result = await loadPackageList(identifier, 1, packagePagination.value.pageSize)
packagePagination.value.total = result.total
}
@@ -365,6 +387,36 @@
realnamePolicyDialogVisible.value = true
}
/**
* 显示更新实名状态对话框
*/
const showUpdateRealnameStatusDialog = () => {
updateRealnameStatusDialogVisible.value = true
}
/**
* 更新实名状态成功
*/
const handleUpdateRealnameStatusSuccess = async () => {
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
}
/**
* 显示绑定卡更新实名状态对话框
*/
const handleUpdateBindingCardRealnameStatus = (payload: { card: any }) => {
bindingCardRealnameStatusIccid.value = payload.card.iccid
bindingCardRealnameStatusValue.value = payload.card.real_name_status ?? 0
bindingCardRealnameStatusDialogVisible.value = true
}
/**
* 绑定卡更新实名状态成功
*/
const handleBindingCardRealnameStatusSuccess = async () => {
await refreshAsset(cardInfo.value!.identifier, cardInfo.value!.asset_type)
}
/**
* 确认实名认证策略
*/
@@ -472,6 +524,19 @@
packagePagination.value.total = result.total
}
/**
* 套餐列表刷新
*/
const handlePackageRefresh = async () => {
if (!cardInfo.value) return
const result = await loadPackageList(
cardInfo.value.identifier,
packagePagination.value.page,
packagePagination.value.pageSize
)
packagePagination.value.total = result.total
}
/**
* 显示流量详单
*/

View File

@@ -95,6 +95,7 @@ export interface PackageInfo {
virtual_limit_mb?: number // 虚流量上限
virtual_remain_mb?: number // 虚流量剩余
data_usage_mb?: number // 已用流量
order_no?: string // 订单号
}
// 钱包信息接口 (使用已有的 AssetWalletResponse)

View File

@@ -553,6 +553,14 @@
:current-policy="currentRealnamePolicy"
@confirm="handleConfirmRealnamePolicy"
/>
<!-- 更新实名状态对话框 -->
<UpdateRealnameStatusDialog
v-model="updateRealnameStatusDialogVisible"
:asset-identifier="currentUpdateRealnameStatusIccid"
:current-realname-status="currentUpdateRealnameStatus"
@success="handleUpdateRealnameStatusSuccess"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -572,6 +580,7 @@
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import UpdateRealnameStatusDialog from '@/components/business/UpdateRealnameStatusDialog.vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -711,6 +720,11 @@
const currentRealnamePolicyIccid = ref('')
const currentRealnamePolicy = ref('none')
// 更新实名状态对话框
const updateRealnameStatusDialogVisible = ref(false)
const currentUpdateRealnameStatusIccid = ref('')
const currentUpdateRealnameStatus = ref<number>(0)
// 显示实名认证策略对话框
const handleShowRealnamePolicy = async (iccid: string, currentPolicy?: string) => {
currentRealnamePolicyIccid.value = iccid
@@ -754,6 +768,18 @@
}
}
// 显示更新实名状态对话框
const handleShowUpdateRealnameStatus = (iccid: string, realnameStatus: number) => {
currentUpdateRealnameStatusIccid.value = iccid
currentUpdateRealnameStatus.value = realnameStatus
updateRealnameStatusDialogVisible.value = true
}
// 更新实名状态成功
const handleUpdateRealnameStatusSuccess = async () => {
await getTableData()
}
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
@@ -1393,6 +1419,14 @@
})
}
if (hasAuth('iot_card:update_realname_status')) {
actions.push({
label: '更新实名状态',
handler: () => handleCardOperation('update-realname-status', row.iccid, row),
type: 'primary'
})
}
if (hasAuth('iot_card:clear_series')) {
actions.push({
label: '清除关联',
@@ -1875,6 +1909,9 @@
case 'realname-policy':
handleShowRealnamePolicy(iccid, row?.realname_policy)
break
case 'update-realname-status':
handleShowUpdateRealnameStatus(iccid, row?.real_name_status)
break
case 'clear-series':
handleClearSingleCardSeries(iccid)
break

View File

@@ -46,66 +46,7 @@
</ArtTable>
<!-- 创建退款申请对话框 -->
<ElDialog
v-model="createDialogVisible"
title="创建退款申请"
width="30%"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem label="订单号" prop="order_id">
<ElSelect
v-model="createForm.order_id"
filterable
remote
clearable
:remote-method="searchOrders"
:loading="orderSearchLoading"
placeholder="请输入订单号搜索"
style="width: 100%"
@change="handleOrderSelectChange"
>
<ElOption
v-for="order in orderSearchOptions"
:key="order.id"
:label="order.order_no"
:value="order.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="申请退款金额" prop="requested_refund_amount">
<ElInputNumber
v-model="createForm.requested_refund_amount"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
placeholder="请输入申请退款金额(元)"
/>
</ElFormItem>
<ElFormItem label="实收金额">
<ElInput :model-value="selectedOrderActualPaid" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem label="退款原因">
<ElInput
v-model="createForm.refund_reason"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入退款原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleCreateRefund" :loading="createLoading">
确认创建
</ElButton>
</div>
</template>
</ElDialog>
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
<!-- 审批通过对话框 -->
<ElDialog
@@ -295,7 +236,6 @@
Refund,
RefundQueryParams,
RefundStatus,
CreateRefundRequest,
ApproveRefundRequest,
RejectRefundRequest,
ReturnRefundRequest,
@@ -305,20 +245,20 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { useAuth } from '@/composables/useAuth'
import CreateRefundDialog from '@/components/business/CreateRefundDialog.vue'
defineOptions({ name: 'RefundList' })
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const createLoading = ref(false)
const approveLoading = ref(false)
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const orderSearchLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const approveDialogVisible = ref(false)
@@ -341,9 +281,6 @@
const shopOptions = ref<any[]>([])
// 订单选项
const orderOptions = ref<any[]>([])
// 订单搜索选项(用于创建弹窗)
const orderSearchOptions = ref<any[]>([])
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
@@ -430,11 +367,6 @@
const returnFormRef = ref<FormInstance>()
const resubmitFormRef = ref<FormInstance>()
const createRules = reactive<FormRules>({
order_id: [{ required: true, message: '请输入订单ID', trigger: 'blur' }],
requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }]
})
const approveRules = reactive<FormRules>({})
const rejectRules = reactive<FormRules>({
@@ -445,18 +377,6 @@
const resubmitRules = reactive<FormRules>({})
const createForm = reactive<{
order_id: number | null
requested_refund_amount: number
actual_received_amount: number
refund_reason: string
}>({
order_id: null,
requested_refund_amount: 0,
actual_received_amount: 0,
refund_reason: ''
})
const approveForm = reactive<ApproveRefundRequest>({
approved_refund_amount: undefined,
remark: ''
@@ -482,15 +402,6 @@
const refundList = ref<Refund[]>([])
// 选中的订单实收金额
const selectedOrderActualPaid = computed(() => {
if (!createForm.order_id) return 0
const selectedOrder = orderSearchOptions.value.find((o) => o.id === createForm.order_id)
if (!selectedOrder?.actual_paid_amount) return 0
const amount = selectedOrder.actual_paid_amount / 100
return isNaN(amount) ? 0 : amount
})
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
@@ -687,43 +598,6 @@
}
}
// 搜索订单(用于创建弹窗)
const searchOrders = async (query: string) => {
orderSearchLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
// 如果有搜索关键词,则按订单号搜索
if (query) {
params.order_no = query
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderSearchOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search orders failed:', error)
} finally {
orderSearchLoading.value = false
}
}
// 订单选择变化 - 自动填充实收金额
const handleOrderSelectChange = (orderId: number | null) => {
if (!orderId) {
createForm.actual_received_amount = 0
return
}
const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId)
if (selectedOrder) {
createForm.actual_received_amount = selectedOrder.actual_paid_amount / 100
}
}
// 获取退款申请列表
const getTableData = async () => {
loading.value = true
@@ -782,42 +656,9 @@
createDialogVisible.value = true
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
createForm.order_id = null
createForm.requested_refund_amount = 0
createForm.actual_received_amount = 0
createForm.refund_reason = ''
orderSearchOptions.value = []
}
// 创建退款申请
const handleCreateRefund = async () => {
if (!createFormRef.value) return
await createFormRef.value.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
const data: CreateRefundRequest = {
order_id: createForm.order_id!,
requested_refund_amount: createForm.requested_refund_amount * 100, // 元转分
actual_received_amount: createForm.actual_received_amount * 100, // 元转分
refund_reason: createForm.refund_reason || undefined
}
await RefundService.createRefund(data)
ElMessage.success('退款申请创建成功')
createDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
})
// 创建退款申请成功
const handleCreateSuccess = () => {
getTableData()
}
// 显示审批通过对话框