feat: 手动实名和套餐列表退款
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m53s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m53s
This commit is contained in:
@@ -17,7 +17,9 @@ import type {
|
||||
AssetWalletTransactionParams,
|
||||
AssetWalletResponse,
|
||||
AssetOrdersParams,
|
||||
AssetOrdersResponse
|
||||
AssetOrdersResponse,
|
||||
UpdateAssetRealnameStatusRequest,
|
||||
DtoUpdateAssetRealnameStatusResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class AssetService extends BaseService {
|
||||
@@ -208,12 +210,25 @@ export class AssetService extends BaseService {
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param realname_policy 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
*/
|
||||
static updateRealnamePolicy(
|
||||
identifier: string,
|
||||
realname_policy: string
|
||||
): Promise<BaseResponse> {
|
||||
static updateRealnamePolicy(identifier: string, realname_policy: string): Promise<BaseResponse> {
|
||||
return this.patch<BaseResponse>(`/api/admin/assets/${identifier}/realname-mode`, {
|
||||
realname_policy
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动更新资产实名状态
|
||||
* PATCH /api/admin/assets/:identifier/realname-status
|
||||
* @param identifier 资产标识符(ICCID 或 VirtualNo)
|
||||
* @param data 实名状态数据
|
||||
*/
|
||||
static updateRealnameStatus(
|
||||
identifier: string,
|
||||
data: UpdateAssetRealnameStatusRequest
|
||||
): Promise<BaseResponse<DtoUpdateAssetRealnameStatusResponse>> {
|
||||
return this.patch<BaseResponse<DtoUpdateAssetRealnameStatusResponse>>(
|
||||
`/api/admin/assets/${identifier}/realname-status`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
207
src/components/business/CreateRefundDialog.vue
Normal file
207
src/components/business/CreateRefundDialog.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="创建退款申请"
|
||||
width="30%"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="订单号" prop="order_id">
|
||||
<ElSelect
|
||||
v-if="!hasInitialOrder"
|
||||
v-model="formData.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>
|
||||
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="申请退款金额" prop="requested_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="formData.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="formData.refund_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认创建
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
initialOrderId?: number
|
||||
initialOrderNo?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
const orderSearchLoading = ref(false)
|
||||
const orderSearchOptions = ref<Order[]>([])
|
||||
|
||||
const formData = 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 formRules = reactive<FormRules>({
|
||||
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
||||
requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
|
||||
|
||||
const selectedOrderActualPaid = computed(() => {
|
||||
if (!formData.order_id) return 0
|
||||
const selectedOrder = orderSearchOptions.value.find((o) => o.id === formData.order_id)
|
||||
if (!selectedOrder?.actual_paid_amount) return 0
|
||||
const amount = selectedOrder.actual_paid_amount / 100
|
||||
return isNaN(amount) ? 0 : amount
|
||||
})
|
||||
|
||||
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 || []
|
||||
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||
const matchedOrder = orderSearchOptions.value.find(
|
||||
(o) => o.order_no === props.initialOrderNo
|
||||
)
|
||||
if (matchedOrder) {
|
||||
formData.order_id = matchedOrder.id
|
||||
formData.actual_received_amount = (matchedOrder.actual_paid_amount || 0) / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search orders failed:', error)
|
||||
} finally {
|
||||
orderSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrderSelectChange = (orderId: number | null) => {
|
||||
if (!orderId) {
|
||||
formData.actual_received_amount = 0
|
||||
return
|
||||
}
|
||||
const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId)
|
||||
if (selectedOrder) {
|
||||
formData.actual_received_amount = selectedOrder.actual_paid_amount / 100
|
||||
}
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
formData.order_id = null
|
||||
formData.requested_refund_amount = 0
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
orderSearchOptions.value = []
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data: CreateRefundRequest = {
|
||||
order_id: formData.order_id!,
|
||||
requested_refund_amount: formData.requested_refund_amount * 100,
|
||||
actual_received_amount: formData.actual_received_amount * 100,
|
||||
refund_reason: formData.refund_reason || undefined
|
||||
}
|
||||
await RefundService.createRefund(data)
|
||||
ElMessage.success('退款申请创建成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (val) {
|
||||
if (hasInitialOrder.value && props.initialOrderNo) {
|
||||
searchOrders(props.initialOrderNo)
|
||||
} else {
|
||||
searchOrders('')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
102
src/components/business/UpdateRealnameStatusDialog.vue
Normal file
102
src/components/business/UpdateRealnameStatusDialog.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="手动更新卡实名状态"
|
||||
width="400px"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<ElFormItem label="资产标识">
|
||||
<span style="font-weight: bold; color: #409eff">{{ assetIdentifier }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="当前状态">
|
||||
<ElTag :type="currentRealnameStatus === 1 ? 'success' : 'warning'">
|
||||
{{ currentRealnameStatus === 1 ? '已实名' : '未实名' }}
|
||||
</ElTag>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实名状态" prop="real_name_status">
|
||||
<ElRadioGroup v-model="formData.real_name_status">
|
||||
<ElRadio :value="0">未实名</ElRadio>
|
||||
<ElRadio :value="1">已实名</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit">确认</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import type { RealNameStatus } from '@/types/api'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
assetIdentifier: string
|
||||
currentRealnameStatus?: RealNameStatus
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
currentRealnameStatus: 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
real_name_status: 0 as 0 | 1
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
real_name_status: [{ required: true, message: '请选择实名状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (val) {
|
||||
formData.real_name_status = props.currentRealnameStatus ?? 0
|
||||
}
|
||||
})
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await AssetService.updateRealnameStatus(props.assetIdentifier, {
|
||||
real_name_status: formData.real_name_status
|
||||
})
|
||||
ElMessage.success('实名状态更新成功')
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('更新实名状态失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -216,6 +216,7 @@ export interface AssetPackageUsageRecord {
|
||||
master_usage_id?: number | null // 主套餐 ID(加油包时有值)
|
||||
priority?: number // 优先级
|
||||
created_at?: string // 创建时间
|
||||
order_no?: string // 订单号
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,3 +413,23 @@ export interface AssetOrdersResponse {
|
||||
previous_generations?: GenerationOrders[] // 前代订单(仅 include_previous=true 时有)
|
||||
truncated: boolean // 是否截断(换货链超过 10 代)
|
||||
}
|
||||
|
||||
// ========== 更新实名状态 ==========
|
||||
|
||||
/**
|
||||
* 更新实名状态请求
|
||||
*/
|
||||
export interface UpdateAssetRealnameStatusRequest {
|
||||
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实名状态响应
|
||||
*/
|
||||
export interface DtoUpdateAssetRealnameStatusResponse {
|
||||
asset_id: number // 资产ID
|
||||
asset_type: string // 资产类型 (card:IoT卡)
|
||||
first_realname_at: string | null // 首次实名时间(仅已实名时可能有值)
|
||||
real_name_status: number // 更新后的实名状态 (0:未实名, 1:已实名)
|
||||
real_name_status_name: string // 实名状态名称(中文)
|
||||
}
|
||||
|
||||
2
src/types/components.d.ts
vendored
2
src/types/components.d.ts
vendored
@@ -81,6 +81,7 @@ declare module 'vue' {
|
||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||
CommissionDisplay: typeof import('./../components/business/CommissionDisplay.vue')['default']
|
||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
|
||||
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
|
||||
DetailPage: typeof import('./../components/common/DetailPage.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
@@ -151,6 +152,7 @@ declare module 'vue' {
|
||||
SwitchCardDialog: typeof import('./../components/device/SwitchCardDialog.vue')['default']
|
||||
TableContextMenuHint: typeof import('./../components/core/others/TableContextMenuHint.vue')['default']
|
||||
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
||||
UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
|
||||
<!-- 套餐详情表格 -->
|
||||
<ElDescriptions :column="3" border class="package-info-table">
|
||||
<ElDescriptionsItem label="订单号">
|
||||
{{ currentPackage.order_no || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐名称">
|
||||
{{ currentPackage.package_name || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示流量详单
|
||||
*/
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface PackageInfo {
|
||||
virtual_limit_mb?: number // 虚流量上限
|
||||
virtual_remain_mb?: number // 虚流量剩余
|
||||
data_usage_mb?: number // 已用流量
|
||||
order_no?: string // 订单号
|
||||
}
|
||||
|
||||
// 钱包信息接口 (使用已有的 AssetWalletResponse)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// 显示审批通过对话框
|
||||
|
||||
Reference in New Issue
Block a user