让凭证数组与套餐作废任务进入可联调状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s

Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传
  purpose 只能使用 attachment。
  Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。
  Confidence: high
  Scope-risk: broad
  Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与
  order_package_invalidate_task:create/detail 按钮权限。
  Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。
  Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
This commit is contained in:
2026-06-23 09:50:12 +08:00
parent ebf9739f06
commit 0c3065f970
71 changed files with 4264 additions and 240 deletions

View File

@@ -91,7 +91,7 @@
<ElFormItem label="所在地区" prop="region">
<ElCascader
v-model="form.region"
:options="regionData"
:options="regionData as CascaderOption[]"
placeholder="请选择省/市/区"
clearable
filterable
@@ -206,7 +206,7 @@
import { RoutesAlias } from '@/router/routesAlias'
import { EnterpriseService, ShopService } from '@/api/modules'
import { ElMessage, ElSwitch, ElCascader } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { CascaderOption, CascaderValue, FormInstance, FormRules } from 'element-plus'
import type { EnterpriseItem } from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -649,11 +649,12 @@
}
// 处理地区选择变化
const handleRegionChange = (value: string[]) => {
if (value && value.length === 3) {
form.province = value[0]
form.city = value[1]
form.district = value[2]
const handleRegionChange = (value: CascaderValue | null | undefined) => {
const region = Array.isArray(value) ? value.map(String) : []
if (region.length === 3) {
form.province = region[0]
form.city = region[1]
form.district = region[2]
} else {
form.province = ''
form.city = ''

View File

@@ -20,7 +20,7 @@
style="width: 100%"
>
<template v-if="!props.seriesId">
<ElOption :value="null" disabled>
<ElOption value="__NO_PACKAGE__" disabled>
<span style="color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐
</span>
@@ -125,6 +125,7 @@
import type { CreateOrderRequest, PackageResponse } from '@/types/api'
import { useUserStore } from '@/store/modules/user'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
interface Props {
modelValue: boolean
@@ -154,7 +155,7 @@
const form = reactive({
package_id: undefined as number | undefined,
payment_method: 'wallet' as 'wallet' | 'offline',
payment_voucher_key: undefined as string | undefined
payment_voucher_key: [] as string[]
})
const rules = computed<FormRules>(() => {
@@ -317,7 +318,7 @@
return
}
if (form.payment_method === 'offline' && !form.payment_voucher_key) {
if (form.payment_method === 'offline' && !hasVoucherKeys(form.payment_voucher_key)) {
ElMessage.error('线下支付必须上传支付凭证')
return
}
@@ -331,8 +332,8 @@
payment_method: form.payment_method
}
if (form.payment_method === 'offline' && form.payment_voucher_key) {
data.payment_voucher_key = form.payment_voucher_key
if (form.payment_method === 'offline' && hasVoucherKeys(form.payment_voucher_key)) {
data.payment_voucher_key = toVoucherKeyList(form.payment_voucher_key)
}
await OrderService.createOrder(data)
@@ -360,7 +361,7 @@
formRef.value?.resetFields()
form.package_id = undefined
form.payment_method = 'wallet'
form.payment_voucher_key = undefined
form.payment_voucher_key = []
voucherUploading.value = false
packageOptions.value = []
packagesEmptyWithSeriesId.value = false

View File

@@ -198,6 +198,7 @@
import { useAssetOperations } from './composables/useAssetOperations'
// 引入类型
import type { AssetPackageUsageRecord } from '@/types/api'
import type { BindingCard, PackageInfo, SwitchCardForm, WiFiForm } from './types'
defineOptions({ name: 'SingleCard' })
@@ -613,10 +614,10 @@
/**
* 显示流量详单
*/
const handleShowDailyRecords = (payload: { packageRow: PackageInfo }) => {
const handleShowDailyRecords = (payload: { packageRow: AssetPackageUsageRecord }) => {
const { packageRow } = payload
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id
selectedPackageRow.value = packageRow
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id ?? null
selectedPackageRow.value = packageRow as PackageInfo
dailyRecordsDialogVisible.value = true
}

View File

@@ -30,6 +30,7 @@
import { ExchangeService } from '@/api/modules'
import type { ExchangeResponse } from '@/api/modules/exchange'
import { ElTag, ElCard, ElButton, ElIcon } from 'element-plus'
import type { TagProps } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import DetailPage from '@/components/common/DetailPage.vue'
@@ -44,8 +45,8 @@
const exchangeDetail = ref<ExchangeResponse | null>(null)
const exchangeId = ref<number>(0)
const getStatusType = (status: number) => {
const types: Record<number, string> = {
const getStatusType = (status: number): TagProps['type'] => {
const types: Record<number, TagProps['type']> = {
1: 'warning',
2: 'info',
3: 'primary',

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:label-width="90"
label-width="90px"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>

View File

@@ -48,7 +48,6 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { formatDateTime } from '@/utils/business/format'
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
import { RoutesAlias } from '@/router/routesAlias'
@@ -347,7 +346,7 @@
const getTableData = async () => {
loading.value = true
try {
const params = {
const params: Record<string, any> = {
page: pagination.page,
page_size: pagination.pageSize,
...formFilters

View File

@@ -370,6 +370,13 @@
minWidth: 180,
showOverflowTooltip: true
},
{
prop: 'creator_name',
label: '操作人',
width: 140,
showOverflowTooltip: true,
formatter: (row: DeviceImportTask) => row.creator_name || '-'
},
{
prop: 'created_at',
label: '创建时间',

View File

@@ -500,6 +500,13 @@
minWidth: 250,
showOverflowTooltip: true
},
{
prop: 'creator_name',
label: '操作人',
width: 140,
showOverflowTooltip: true,
formatter: (row: IotCardImportTask) => row.creator_name || '-'
},
{
prop: 'created_at',
label: '创建时间',

View File

@@ -0,0 +1,171 @@
<template>
<div class="order-package-invalidate-task-detail-page">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="router.back()">返回</ElButton>
<h2 class="detail-title">订单套餐作废任务详情</h2>
</div>
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
<ElTable
v-if="taskDetail?.failed_items?.length"
:data="taskDetail.failed_items"
border
style="width: 100%; margin-top: 16px"
>
<ElTableColumn prop="line" label="行号" width="100" />
<ElTableColumn prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
<ElTableColumn prop="reason" label="失败原因" min-width="240" show-overflow-tooltip />
</ElTable>
<ElEmpty v-else-if="taskDetail" description="暂无失败明细" />
</ElCard>
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
</div>
</template>
<script setup lang="ts">
import { computed, h, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElButton, ElCard, ElEmpty, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { OrderPackageInvalidateTaskService } from '@/api/modules'
import type {
OrderPackageInvalidateTaskDetail,
OrderPackageInvalidateTaskStatus
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'OrderPackageInvalidateTaskDetail' })
const route = useRoute()
const router = useRouter()
const taskDetail = ref<OrderPackageInvalidateTaskDetail | null>(null)
const voucherPreviewKeys = ref<string[]>([])
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return statusMap[status] || 'info'
}
const getStatusName = (task: OrderPackageInvalidateTaskDetail) =>
task.status_text || task.status_name || '-'
const detailSections = computed<DetailSection[]>(() => [
{
title: '任务信息',
columns: 2,
fields: [
{ label: '任务编号', prop: 'task_no' },
{
label: '任务状态',
render: (data: OrderPackageInvalidateTaskDetail) =>
h(ElTag, { type: getStatusType(data.status) }, () => getStatusName(data))
},
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
{ label: '操作人', prop: 'creator_name', formatter: (value: string) => value || '-' },
{
label: '凭证附件',
render: (data: OrderPackageInvalidateTaskDetail) =>
hasVoucherKeys(data.voucher_keys)
? h(
ElButton,
{
link: true,
type: 'primary',
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(data.voucher_keys))
},
() => '查看凭证'
)
: h('span', '-')
},
{
label: '运营备注',
prop: 'remark',
formatter: (value: string) => value || '-',
fullWidth: true
},
{
label: '错误信息',
prop: 'error_message',
formatter: (value: string) => value || '-',
fullWidth: true
},
{
label: '创建时间',
prop: 'created_at',
formatter: (value: string) => formatDateTime(value)
},
{
label: '开始时间',
prop: 'started_at',
formatter: (value: string) => (value ? formatDateTime(value) : '-')
},
{
label: '完成时间',
prop: 'completed_at',
formatter: (value: string) => (value ? formatDateTime(value) : '-')
}
]
},
{
title: '统计信息',
columns: 2,
fields: [
{ label: '总数', prop: 'total_count' },
{ label: '成功数', prop: 'success_count' },
{ label: '失败数', prop: 'fail_count' },
{ label: '跳过数', prop: 'skip_count', formatter: (value: number) => String(value ?? 0) }
]
}
])
const getTaskDetail = async () => {
const taskId = Number(route.query.id)
if (!taskId) {
ElMessage.error('任务ID无效')
return
}
const res = await OrderPackageInvalidateTaskService.getTaskDetail(taskId)
if (res.code === 0) {
taskDetail.value = res.data
}
}
onMounted(() => {
getTaskDetail()
})
</script>
<style scoped lang="scss">
.order-package-invalidate-task-detail-page {
padding: 20px;
.detail-header {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 24px;
}
.detail-title {
min-width: 0;
margin: 0;
overflow: hidden;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@@ -0,0 +1,476 @@
<template>
<ArtTableFullScreen>
<div class="order-package-invalidate-task-page" id="table-full-screen">
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
/>
<ElCard shadow="never" class="art-table-card">
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton
type="primary"
:icon="Upload"
@click="createDialogVisible = true"
v-permission="'order_package_invalidate_task:create'"
>
新建作废任务
</ElButton>
</template>
</ArtTableHeader>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="taskList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="180"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</ElCard>
<ElDialog
v-model="createDialogVisible"
title="新建订单套餐作废任务"
width="560px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="CSV文件" prop="file">
<ElUpload
ref="uploadRef"
drag
:auto-upload="false"
:limit="1"
accept=".csv,text/csv"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
<div class="el-upload__text"> CSV 文件拖到此处<em>点击选择</em></div>
<template #tip>
<div class="el-upload__tip">仅支持 .csv 文件必填列order_no</div>
</template>
</ElUpload>
</ElFormItem>
<ElFormItem label="凭证附件">
<VoucherUpload
ref="voucherUploadRef"
v-model="createForm.voucher_keys"
voucher-name="凭证附件"
@uploading-change="voucherUploading = $event"
/>
</ElFormItem>
<ElFormItem label="运营备注" prop="remark">
<ElInput
v-model="createForm.remark"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入运营备注(可选)"
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
:loading="creating || voucherUploading"
:disabled="voucherUploading"
@click="handleCreateTask"
>
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
</ElButton>
</template>
</ElDialog>
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElMessage,
ElTag,
ElUpload
} from 'element-plus'
import { Upload, UploadFilled } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
import { OrderPackageInvalidateTaskService, StorageService } from '@/api/modules'
import type {
CreateOrderPackageInvalidateTaskRequest,
OrderPackageInvalidateTask,
OrderPackageInvalidateTaskQueryParams,
OrderPackageInvalidateTaskStatus
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import { getErrorMessage, hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
defineOptions({ name: 'OrderPackageInvalidateTask' })
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const creating = ref(false)
const voucherUploading = ref(false)
const tableRef = ref()
const uploadRef = ref<UploadInstance>()
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createFormRef = ref<FormInstance>()
const createDialogVisible = ref(false)
const selectedFile = ref<File | null>(null)
const taskList = ref<OrderPackageInvalidateTask[]>([])
const voucherPreviewKeys = ref<string[]>([])
const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({
page: 1,
page_size: 20,
status: undefined
})
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
const createForm = reactive({
file: '',
voucher_keys: [] as string[],
remark: ''
})
const createRules = reactive<FormRules>({
file: [{ required: true, message: '请选择 CSV 文件', trigger: 'change' }],
remark: [{ max: 1000, message: '运营备注最多1000字', trigger: 'blur' }]
})
const searchFormItems: SearchFormItem[] = [
{
label: '任务状态',
prop: 'status',
type: 'select',
config: { clearable: true, placeholder: '全部' },
options: [
{ label: '待处理', value: 1 },
{ label: '处理中', value: 2 },
{ label: '已完成', value: 3 },
{ label: '失败', value: 4 }
]
}
]
const columnOptions = [
{ label: '任务编号', prop: 'task_no' },
{ label: '任务状态', prop: 'status' },
{ label: '总数', prop: 'total_count' },
{ label: '成功数', prop: 'success_count' },
{ label: '失败数', prop: 'fail_count' },
{ label: '跳过数', prop: 'skip_count' },
{ label: '文件名', prop: 'file_name' },
{ label: '凭证', prop: 'voucher_keys' },
{ label: '运营备注', prop: 'remark' },
{ label: '操作人', prop: 'creator_name' },
{ label: '错误信息', prop: 'error_message' },
{ label: '创建时间', prop: 'created_at' },
{ label: '开始时间', prop: 'started_at' },
{ label: '完成时间', prop: 'completed_at' }
]
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return statusMap[status] || 'info'
}
const getStatusName = (task: OrderPackageInvalidateTask) =>
task.status_text || task.status_name || '-'
const handleNameClick = (row: OrderPackageInvalidateTask) => {
if (!hasAuth('order_package_invalidate_task:detail')) {
ElMessage.warning('您没有查看详情的权限')
return
}
router.push({
path: RoutesAlias.OrderPackageInvalidateTaskDetail,
query: { id: row.id }
})
}
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'task_no',
label: '任务编号',
width: 220,
showOverflowTooltip: true,
formatter: (row: OrderPackageInvalidateTask) =>
h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (event: MouseEvent) => {
event.stopPropagation()
handleNameClick(row)
}
},
row.task_no
)
},
{
prop: 'status',
label: '任务状态',
width: 100,
formatter: (row: OrderPackageInvalidateTask) =>
h(ElTag, { type: getStatusType(row.status) }, () => getStatusName(row))
},
{ prop: 'total_count', label: '总数', width: 80 },
{
prop: 'success_count',
label: '成功数',
width: 80,
formatter: (row: OrderPackageInvalidateTask) =>
h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
},
{
prop: 'fail_count',
label: '失败数',
width: 80,
formatter: (row: OrderPackageInvalidateTask) =>
h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
},
{
prop: 'skip_count',
label: '跳过数',
width: 80,
formatter: (row: OrderPackageInvalidateTask) => row.skip_count ?? 0
},
{ prop: 'file_name', label: '文件名', minWidth: 180, showOverflowTooltip: true },
{
prop: 'voucher_keys',
label: '凭证',
width: 100,
formatter: (row: OrderPackageInvalidateTask) =>
hasVoucherKeys(row.voucher_keys)
? h(
ElButton,
{
link: true,
type: 'primary',
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(row.voucher_keys))
},
() => '查看'
)
: '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: OrderPackageInvalidateTask) => row.remark || '-'
},
{
prop: 'creator_name',
label: '操作人',
width: 140,
showOverflowTooltip: true,
formatter: (row: OrderPackageInvalidateTask) => row.creator_name || '-'
},
{
prop: 'error_message',
label: '错误信息',
minWidth: 200,
showOverflowTooltip: true,
formatter: (row: OrderPackageInvalidateTask) => row.error_message || '-'
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: OrderPackageInvalidateTask) => formatDateTime(row.created_at)
},
{
prop: 'started_at',
label: '开始时间',
width: 180,
formatter: (row: OrderPackageInvalidateTask) =>
row.started_at ? formatDateTime(row.started_at) : '-'
},
{
prop: 'completed_at',
label: '完成时间',
width: 180,
formatter: (row: OrderPackageInvalidateTask) =>
row.completed_at ? formatDateTime(row.completed_at) : '-'
}
])
const getTableData = async () => {
loading.value = true
try {
const res = await OrderPackageInvalidateTaskService.getTasks({
page: pagination.page,
page_size: pagination.page_size,
status: searchForm.status
})
if (res.code === 0) {
taskList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('加载订单套餐作废任务失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
getTableData()
}
const handleReset = () => {
searchForm.status = undefined
pagination.page = 1
getTableData()
}
const handleRefresh = () => getTableData()
const handleSizeChange = (pageSize: number) => {
pagination.page_size = pageSize
getTableData()
}
const handleCurrentChange = (page: number) => {
pagination.page = page
getTableData()
}
const handleFileChange = (uploadFile: UploadFile) => {
const file = uploadFile.raw
if (!file) return
if (!/\.csv$/i.test(file.name)) {
ElMessage.error('仅支持 CSV 文件')
uploadRef.value?.clearFiles()
selectedFile.value = null
createForm.file = ''
return
}
selectedFile.value = file
createForm.file = file.name
createFormRef.value?.validateField('file')
}
const handleFileRemove = () => {
selectedFile.value = null
createForm.file = ''
createFormRef.value?.validateField('file')
}
const uploadCsvFile = async (file: File) => {
const contentType = file.type || 'text/csv'
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: contentType,
purpose: 'attachment'
})
if (uploadUrlRes.code !== 0) {
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
}
await StorageService.uploadFile(uploadUrlRes.data.upload_url, file, contentType)
return uploadUrlRes.data.file_key
}
const handleCreateTask = async () => {
if (!createFormRef.value) return
if (voucherUploading.value) {
ElMessage.warning('凭证上传中,请稍候')
return
}
const valid = await createFormRef.value.validate().catch(() => false)
if (!valid || !selectedFile.value) return
creating.value = true
try {
const fileKey = await uploadCsvFile(selectedFile.value)
const data: CreateOrderPackageInvalidateTaskRequest = {
file_key: fileKey,
file_name: selectedFile.value.name,
voucher_keys: toVoucherKeyList(createForm.voucher_keys),
remark: createForm.remark || undefined
}
await OrderPackageInvalidateTaskService.createTask(data)
ElMessage.success('任务创建成功')
createDialogVisible.value = false
await getTableData()
} catch (error) {
console.error('创建订单套餐作废任务失败:', error)
ElMessage.error(getErrorMessage(error, '任务创建失败'))
} finally {
creating.value = false
}
}
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
uploadRef.value?.clearFiles()
voucherUploadRef.value?.clearFiles(false)
selectedFile.value = null
createForm.file = ''
createForm.voucher_keys = []
createForm.remark = ''
voucherUploading.value = false
}
const getActions = (row: OrderPackageInvalidateTask) => {
const actions: any[] = []
if (hasAuth('order_package_invalidate_task:detail')) {
actions.push({ label: '详情', handler: () => handleNameClick(row), type: 'primary' })
}
return actions
}
onMounted(() => {
getTableData()
})
</script>

View File

@@ -21,7 +21,7 @@
<ElDivider content-position="left">
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
</ElDivider>
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
<ElTable :data="taskDetail.failed_items || []" border style="width: 100%">
<ElTableColumn prop="line" label="行号" width="100" />
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
@@ -34,7 +34,7 @@
<ElDivider content-position="left">
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
</ElDivider>
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
<ElTable :data="taskDetail.skipped_items || []" border style="width: 100%">
<ElTableColumn prop="line" label="行号" width="100" />
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
@@ -52,9 +52,10 @@
import { ref, onMounted } from 'vue'
import { CardService, DeviceService } from '@/api/modules'
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
import type { TagProps } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/business/format'
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
import type { IotCardImportTaskDetail } from '@/types/api/card'
import type { DeviceImportTaskDetail } from '@/types/api/device'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import DetailPage from '@/components/common/DetailPage.vue'
@@ -72,7 +73,7 @@
const taskType = ref<TaskType>('card')
// 获取状态标签类型
const getStatusType = (status?: IotCardImportTaskStatus) => {
const getStatusType = (status?: number): TagProps['type'] => {
if (!status) return 'info'
switch (status) {
case 1:
@@ -107,6 +108,11 @@
},
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
{
label: '操作人',
prop: 'creator_name',
formatter: (value: string) => value || '-'
},
{
label: '任务状态',
render: (data: TaskDetail) => {

View File

@@ -106,7 +106,10 @@
"
>
{{
scope.row.status_name || CommissionStatusMap[scope.row.status]?.label || '-'
scope.row.status_name ||
CommissionStatusMap[scope.row.status as keyof typeof CommissionStatusMap]
?.label ||
'-'
}}
</ElTag>
</template>
@@ -1057,13 +1060,13 @@
.main-wallet-filter__actions {
:deep(.el-form-item__content) {
display: flex;
flex-wrap: nowrap;
gap: 8px;
justify-content: flex-end;
flex-wrap: nowrap;
}
}
@media (max-width: 1200px) {
@media (width <= 1200px) {
.main-wallet-filter__grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
}
@@ -1075,7 +1078,7 @@
}
}
@media (max-width: 768px) {
@media (width <= 768px) {
.main-wallet-filter__grid {
grid-template-columns: 1fr;
}

View File

@@ -210,13 +210,11 @@
import { WithdrawalStatusMap, WithdrawalMethodMap } from '@/config/constants/commission'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime, formatMoney } from '@/utils/business/format'
defineOptions({ name: 'MyCommission' })
const { hasAuth } = useAuth()
const userStore = useUserStore()
// 获取当前用户的 shop_id
@@ -365,14 +363,6 @@
one_time: { label: '一次性佣金', type: 'success' as const }
}
// 状态映射
const CommissionRecordStatusMap = {
1: { label: '已冻结', type: 'info' as const },
2: { label: '解冻中', type: 'warning' as const },
3: { label: '已发放', type: 'success' as const },
4: { label: '已失效', type: 'danger' as const }
}
// 动态列配置
const { columnChecks: commissionColumnChecks, columns: commissionColumns } = useCheckedColumns(
() => [
@@ -809,7 +799,7 @@
params.bank_name = withdrawalForm.bank_name
}
await CommissionService.submitShopWithdrawalRequest(currentShopId.value, params)
await CommissionService.submitShopWithdrawalRequest(currentShopId.value!, params)
ElMessage.success('提现申请提交成功')
withdrawalDialogVisible.value = false

View File

@@ -351,8 +351,8 @@
}
// 列配置
const columns = computed(() => {
const baseColumns = [
const columns = computed<any[]>(() => {
const baseColumns: any[] = [
{
prop: 'username',
label: '用户名'
@@ -378,14 +378,14 @@
baseColumns.push({
prop: 'shop_name',
label: '店铺名称',
formatter: (row: PlatformAccount) => (row as any).shop_name || '-'
formatter: (row: PlatformAccount) => h('span', (row as any).shop_name || '-')
})
} else {
// 企业类型:显示企业名称
baseColumns.push({
prop: 'enterprise_name',
label: '企业名称',
formatter: (row: PlatformAccount) => (row as any).enterprise_name || '-'
formatter: (row: PlatformAccount) => h('span', (row as any).enterprise_name || '-')
})
}
@@ -401,7 +401,7 @@
{
prop: 'created_at',
label: '创建时间',
formatter: (row: PlatformAccount) => formatDateTime((row as any).created_at)
formatter: (row: PlatformAccount) => h('span', formatDateTime((row as any).created_at))
},
{
prop: 'operation',

View File

@@ -22,7 +22,10 @@
</div>
</ElCard>
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
<PaymentVoucherDialog
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</div>
</template>
@@ -36,6 +39,7 @@
import { AgentRechargeService } from '@/api/modules'
import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'AgentRechargeDetail' })
@@ -45,7 +49,7 @@
const loading = ref(false)
const detailData = ref<AgentRecharge | null>(null)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
const pageTitle = computed(() => `充值订单详情`)
@@ -132,19 +136,25 @@
label: '支付凭证',
fullWidth: true,
render: (data) =>
data.payment_voucher_key
hasVoucherKeys(data.payment_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
paymentVoucherFileKey.value = data.payment_voucher_key
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
}
},
() => '查看支付凭证'
)
: h('span', '-')
},
{
label: '运营备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
]
},

View File

@@ -103,6 +103,16 @@
@change="createFormRef?.validateField('payment_voucher_key')"
/>
</ElFormItem>
<ElFormItem label="运营备注" prop="remark">
<ElInput
v-model="createForm.remark"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入运营备注(可选)"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
@@ -159,8 +169,8 @@
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-key="paymentVoucherFileKey"
@close="paymentVoucherFileKey = ''"
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</ElCard>
</div>
@@ -176,6 +186,7 @@
ElTag,
ElButton,
ElCascader,
ElInput,
ElInputNumber,
ElSelect,
ElOption
@@ -192,6 +203,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { useAuth } from '@/composables/useAuth'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
@@ -209,7 +221,7 @@
const createDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
@@ -366,12 +378,14 @@
amount: number
payment_method: string
shop_id: number | null
payment_voucher_key?: string
payment_voucher_key: string[]
remark: string
}>({
amount: MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_voucher_key: undefined
payment_voucher_key: [],
remark: ''
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
@@ -475,6 +489,13 @@
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.remark || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -628,7 +649,8 @@
createForm.amount = MIN_RECHARGE_AMOUNT
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = undefined
createForm.payment_voucher_key = []
createForm.remark = ''
voucherUploading.value = false
uploadRef.value?.clearFiles(false)
}
@@ -649,11 +671,15 @@
const data: CreateAgentRechargeRequest = {
amount: Math.round(createForm.amount * 100), // 元转分
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
shop_id: createForm.shop_id!
shop_id: createForm.shop_id!,
remark: createForm.remark || undefined
}
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
if (
createForm.payment_method === 'offline' &&
hasVoucherKeys(createForm.payment_voucher_key)
) {
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
}
await AgentRechargeService.createAgentRecharge(data)
@@ -733,7 +759,7 @@
// 线下支付且有凭证可以查看
if (
row.payment_method === 'offline' &&
row.payment_voucher_key &&
hasVoucherKeys(row.payment_voucher_key) &&
hasAuth('agent_recharge:view_payment_voucher')
) {
actions.push({
@@ -761,8 +787,8 @@
// 查看支付凭证
const handleViewPaymentVoucher = (row: AgentRecharge) => {
if (!row.payment_voucher_key) return
paymentVoucherFileKey.value = row.payment_voucher_key
if (!hasVoucherKeys(row.payment_voucher_key)) return
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
}
</script>

View File

@@ -22,7 +22,7 @@
</div>
</ElCard>
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
<PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" />
<!-- 审批通过对话框 -->
<ElDialog
@@ -150,12 +150,26 @@
placeholder="请输入退款原因"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key">
<VoucherUpload
ref="resubmitUploadRef"
v-model="resubmitForm.refund_voucher_key"
voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
确认提交
<ElButton
type="primary"
@click="handleResubmitRefund"
:loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading"
>
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
</ElButton>
</div>
</template>
@@ -179,7 +193,9 @@
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 VoucherUpload from '@/components/business/VoucherUpload.vue'
defineOptions({ name: 'RefundDetail' })
@@ -188,7 +204,7 @@
const loading = ref(false)
const refund = ref<Refund | null>(null)
const refundVoucherFileKey = ref('')
const refundVoucherFileKeys = ref<string[]>([])
const pageTitle = computed(() => `退款详情`)
@@ -199,10 +215,12 @@
const approveLoading = ref(false)
const rejectLoading = ref(false)
const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false)
const approveFormRef = ref()
const rejectFormRef = ref()
const resubmitFormRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const approveRules = ref()
const rejectRules = ref({
@@ -222,10 +240,12 @@
const resubmitForm = reactive<{
requested_refund_amount?: number
actual_received_amount?: number
refund_voucher_key: string[]
refund_reason?: string
}>({
requested_refund_amount: undefined,
actual_received_amount: undefined,
refund_voucher_key: [],
refund_reason: ''
})
@@ -295,14 +315,14 @@
label: '退款凭证',
fullWidth: true,
render: (data) =>
data.refund_voucher_key
hasVoucherKeys(data.refund_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
refundVoucherFileKey.value = data.refund_voucher_key
refundVoucherFileKeys.value = toVoucherKeyList(data.refund_voucher_key)
}
},
() => '查看退款凭证'
@@ -411,7 +431,10 @@
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false)
}
const handleApproveRefund = async () => {
@@ -463,6 +486,10 @@
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !refund.value) return
if (resubmitVoucherUploading.value) {
ElMessage.warning('退款凭证上传中,请稍候')
return
}
const refundId = refund.value.id
await resubmitFormRef.value.validate(async (valid: boolean) => {
@@ -474,6 +501,9 @@
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined
}
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
}
await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功')

View File

@@ -49,7 +49,10 @@
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
<!-- 退款凭证预览 -->
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
<PaymentVoucherDialog
:file-keys="refundVoucherFileKeys"
@close="refundVoucherFileKeys = []"
/>
<!-- 审批通过对话框 -->
<ElDialog
@@ -217,12 +220,26 @@
placeholder="请输入退款原因"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key">
<VoucherUpload
ref="resubmitUploadRef"
v-model="resubmitForm.refund_voucher_key"
voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
确认提交
<ElButton
type="primary"
@click="handleResubmitRefund"
:loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading"
>
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
</ElButton>
</div>
</template>
@@ -250,8 +267,10 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { useAuth } from '@/composables/useAuth'
@@ -266,14 +285,16 @@
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false)
const tableRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createDialogVisible = ref(false)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const returnDialogVisible = ref(false)
const resubmitDialogVisible = ref(false)
const currentRefund = ref<Refund | null>(null)
const refundVoucherFileKey = ref('')
const refundVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: RefundQueryParams = {
@@ -410,10 +431,12 @@
const resubmitForm = reactive<{
requested_refund_amount?: number
actual_received_amount?: number
refund_voucher_key: string[]
refund_reason?: string
}>({
requested_refund_amount: undefined,
actual_received_amount: undefined,
refund_voucher_key: [],
refund_reason: ''
})
@@ -812,6 +835,7 @@
// 预填充原有数据
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = row.refund_reason
resubmitDialogVisible.value = true
}
@@ -821,13 +845,20 @@
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false)
currentRefund.value = null
}
// 重新提交
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !currentRefund.value) return
if (resubmitVoucherUploading.value) {
ElMessage.warning('退款凭证上传中,请稍候')
return
}
await resubmitFormRef.value.validate(async (valid) => {
if (valid) {
@@ -840,6 +871,9 @@
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined
}
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
}
await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功')
@@ -873,7 +907,7 @@
// 获取操作按钮
const getActions = (row: Refund) => {
const actions: any[] = []
const voucherKey = row.refund_voucher_key
const voucherKeys = toVoucherKeyList(row.refund_voucher_key)
// 待审批状态可以打回重填、审批通过或审批拒绝
if (row.status === 1) {
@@ -912,7 +946,7 @@
})
}
if (voucherKey && hasAuth('refund:view_voucher')) {
if (voucherKeys.length && hasAuth('refund:view_voucher')) {
actions.push({
label: '查看退款凭证',
handler: () => handleViewRefundVoucher(row),
@@ -925,8 +959,8 @@
const handleViewRefundVoucher = (row: Refund) => {
const voucherKey = row.refund_voucher_key
if (!voucherKey) return
refundVoucherFileKey.value = voucherKey
if (!hasVoucherKeys(voucherKey)) return
refundVoucherFileKeys.value = toVoucherKeyList(voucherKey)
}
</script>

View File

@@ -56,7 +56,10 @@
</div>
</ElCard>
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
<PaymentVoucherDialog
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</div>
</template>
@@ -66,7 +69,7 @@
import { ElButton, ElCard, ElIcon, ElMessage, ElTable, ElTableColumn } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import type { DetailField, DetailSection } from '@/components/common/DetailPage.vue'
import { OrderService } from '@/api/modules'
import type {
Order,
@@ -77,6 +80,7 @@
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'OrderDetail' })
@@ -88,7 +92,7 @@
const loading = ref(false)
const detailData = ref<Order | null>(null)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
@@ -164,20 +168,20 @@
// 详情页配置
const detailSections = computed<DetailSection[]>(() => {
const baseFields = [
const baseFields: DetailField[] = [
{ label: '订单号', prop: 'order_no' },
{
label: '订单类型',
formatter: (_, data) => getOrderTypeText(data.order_type)
formatter: (_: any, data: any) => getOrderTypeText(data.order_type)
},
{
label: '支付状态',
formatter: (_, data) => data.payment_status_text || '-'
formatter: (_: any, data: any) => data.payment_status_text || '-'
},
{
label: '订单金额',
prop: 'total_amount',
formatter: (value) => formatCurrency(value)
formatter: (value: any) => formatCurrency(value)
}
]
@@ -186,7 +190,8 @@
baseFields.push({
label: '实付金额',
prop: 'actual_paid_amount',
formatter: (value) => (value !== undefined && value !== null ? formatCurrency(value) : '-')
formatter: (value: any) =>
value !== undefined && value !== null ? formatCurrency(value) : '-'
})
}
@@ -194,8 +199,11 @@
{
label: '支付凭证',
fullWidth: true,
render: (data) => {
if (!data.payment_voucher_key || !hasAuth('orders:view_payment_voucher')) {
render: (data: any) => {
if (
!hasVoucherKeys(data.payment_voucher_key) ||
!hasAuth('orders:view_payment_voucher')
) {
return h('span', '-')
}
@@ -205,7 +213,7 @@
type: 'primary',
link: true,
onClick: () => {
paymentVoucherFileKey.value = data.payment_voucher_key
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
}
},
() => '查看支付凭证'

View File

@@ -225,8 +225,8 @@
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-key="paymentVoucherFileKey"
@close="paymentVoucherFileKey = ''"
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
<!-- 导出任务对话框 -->
@@ -273,6 +273,7 @@
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
@@ -294,7 +295,7 @@
const tableRef = ref()
const createDialogVisible = ref(false)
const exportDialogVisible = ref(false)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: OrderSearchFormState = {
@@ -570,7 +571,7 @@
device_id: null as number | null,
device_series_id: null as number | null, // 设备的套餐系列ID
payment_method: 'wallet' as OrderPaymentMethod, // 默认使用钱包支付
payment_voucher_key: undefined as string | undefined // 线下支付凭证
payment_voucher_key: [] as string[] // 线下支付凭证
})
const orderList = ref<Order[]>([])
@@ -1142,7 +1143,7 @@
createForm.iot_card_id = null
createForm.device_id = null
createForm.payment_method = 'wallet'
createForm.payment_voucher_key = undefined
createForm.payment_voucher_key = []
voucherUploading.value = false
// 清空上传文件列表
@@ -1203,7 +1204,10 @@
}
// 线下支付时,支付凭证为必填
if (createForm.payment_method === 'offline' && !createForm.payment_voucher_key) {
if (
createForm.payment_method === 'offline' &&
!hasVoucherKeys(createForm.payment_voucher_key)
) {
ElMessage.error('线下支付必须上传支付凭证')
return
}
@@ -1215,9 +1219,12 @@
payment_method: createForm.payment_method
}
// 线下支付时,添加支付凭证
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
// 线下支付时,添加支付凭证附件列表
if (
createForm.payment_method === 'offline' &&
hasVoucherKeys(createForm.payment_voucher_key)
) {
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
}
const res = await OrderService.createOrder(data)
@@ -1296,7 +1303,7 @@
// 线下支付且有凭证可以查看
if (
row.payment_method === 'offline' &&
row.payment_voucher_key &&
hasVoucherKeys(row.payment_voucher_key) &&
hasAuth('orders:view_payment_voucher')
) {
actions.push({
@@ -1320,8 +1327,8 @@
// 查看支付凭证
const handleViewPaymentVoucher = (row: Order) => {
if (!row.payment_voucher_key) return
paymentVoucherFileKey.value = row.payment_voucher_key
if (!hasVoucherKeys(row.payment_voucher_key)) return
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
}
</script>

View File

@@ -71,7 +71,7 @@
{
label: '成本价',
formatter: (_: unknown, data: PackageResponse) => {
return `¥${(data.cost_price / 100).toFixed(2)}`
return `¥${((data.cost_price || 0) / 100).toFixed(2)}`
}
},
{
@@ -190,10 +190,11 @@
{
label: '总流量',
formatter: (_: unknown, data: PackageResponse) => {
if (data.real_data_mb >= 1024) {
return `${(data.real_data_mb / 1024).toFixed(2)} GB`
const realDataMb = data.real_data_mb || 0
if (realDataMb >= 1024) {
return `${(realDataMb / 1024).toFixed(2)} GB`
}
return `${data.real_data_mb} MB`
return `${realDataMb} MB`
}
},
...(shouldHideVirtualTrafficFields.value
@@ -209,10 +210,11 @@
label: '虚流量额度',
formatter: (_: unknown, data: PackageResponse) => {
if (!data.enable_virtual_data) return '-'
if (data.virtual_data_mb >= 1024) {
return `${(data.virtual_data_mb / 1024).toFixed(2)} GB`
const virtualDataMb = data.virtual_data_mb || 0
if (virtualDataMb >= 1024) {
return `${(virtualDataMb / 1024).toFixed(2)} GB`
}
return `${data.virtual_data_mb} MB`
return `${virtualDataMb} MB`
}
}
])
@@ -243,7 +245,7 @@
label: '下一档位比例',
formatter: (_: unknown, data: PackageResponse) => {
if (!data.tier_info || !data.tier_info.next_rate) return '-'
return data.tier_info.next_rate
return String(data.tier_info.next_rate)
}
},
{
@@ -255,7 +257,7 @@
data.tier_info.next_threshold === undefined
)
return '-'
return data.tier_info.next_threshold
return String(data.tier_info.next_threshold)
}
}
]

View File

@@ -231,14 +231,16 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { PollingConfigService, CarrierService } from '@/api/modules'
import type {
PollingConfig,
PollingConfigQueryParams,
CreatePollingConfigRequest,
UpdatePollingConfigRequest
UpdatePollingConfigRequest,
PollingCardCategory,
CardCondition
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format'
@@ -262,8 +264,8 @@
// 搜索表单
const searchForm = reactive({
config_name: '',
card_category: undefined as string | undefined,
card_condition: undefined as string | undefined,
card_category: undefined as PollingCardCategory | undefined,
card_condition: undefined as CardCondition | undefined,
carrier_id: undefined as number | undefined,
status: undefined as number | undefined
})
@@ -530,7 +532,7 @@
const { data } = await PollingConfigService.getPollingConfigs(queryParams)
tableData.value = data.items
pagination.total = data.total
} catch (error) {
} catch {
console.log('加载配置列表失败')
} finally {
loading.value = false
@@ -612,7 +614,7 @@
nextTick(() => {
formRef.value?.clearValidate()
})
} catch (error) {
} catch {
console.log('获取配置详情失败')
}
}
@@ -623,7 +625,7 @@
await PollingConfigService.updatePollingConfigStatus(row.id, { status: newStatus })
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
await loadData()
} catch (error) {
} catch {
console.log('状态更新失败')
}
}
@@ -677,7 +679,7 @@
dialogVisible.value = false
await loadData()
} catch (error) {
} catch {
console.log(dialogType.value === 'create' ? '创建失败' : '更新失败')
}
})

View File

@@ -6,7 +6,7 @@
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
label-width="100"
label-width="100px"
@reset="handleReset"
@search="handleSearch"
/>
@@ -63,11 +63,11 @@
<div style="display: flex; gap: 8px; align-items: center">
<el-progress
:percentage="getProgressPercentage(scope.row)"
stroke-width="6"
:stroke-width="6"
:show-text="false"
style="flex: 1"
/>
<span style=" width: 50px;font-size: 12px"
<span style="width: 50px; font-size: 12px"
>{{ getProgressPercentage(scope.row) }}%</span
>
</div>
@@ -103,7 +103,7 @@
label="任务类型"
prop="task_type"
label-width="80"
style=" width: 300px;margin-bottom: 0"
style="width: 300px; margin-bottom: 0"
>
<el-select
v-model="batchForm.task_type"
@@ -757,28 +757,6 @@
batchCardPagination.pageSize = 20
}
// 获取卡状态类型
const getCardStatusType = (status: number) => {
const typeMap: Record<number, 'success' | 'warning' | 'danger' | 'info'> = {
0: 'info',
1: 'success',
2: 'warning',
3: 'danger'
}
return typeMap[status] || 'info'
}
// 获取卡状态名称
const getCardStatusName = (status: number) => {
const nameMap: Record<number, string> = {
0: '未激活',
1: '已激活',
2: '停机',
3: '异常'
}
return nameMap[status] || '未知'
}
// 获取卡业务类型文本
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
@@ -790,7 +768,7 @@
// 条件触发表单
const conditionForm = reactive<ConditionManualTriggerRequest>({
task_type: '' as TaskType | '',
task_type: undefined as unknown as TaskType,
limit: 100,
card_status: undefined,
card_type: undefined,
@@ -838,16 +816,6 @@
}
])
const getStatusTag = (status: string) => {
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
pending: 'info',
processing: 'warning',
completed: 'success',
cancelled: 'danger'
}
return typeMap[status] || 'info'
}
const getProgressPercentage = (row: ManualTriggerLog) => {
if (row.total_count === 0) return 0
return Math.round((row.processed_count / row.total_count) * 100)
@@ -918,7 +886,7 @@
const { data } = await ManualTriggerService.getHistory(queryParams)
tableData.value = data.items
pagination.total = data.total
} catch (error) {
} catch {
console.log('加载触发历史失败')
} finally {
loading.value = false
@@ -1046,7 +1014,7 @@
ElMessage.success('批量触发成功')
batchDialogVisible.value = false
await loadData()
} catch (error) {
} catch {
console.log('批量触发失败')
}
})
@@ -1073,7 +1041,7 @@
ElMessage.success('条件触发成功')
conditionDialogVisible.value = false
await loadData()
} catch (error) {
} catch {
console.log('条件触发失败')
}
})
@@ -1092,7 +1060,7 @@
ElMessage.success('单卡触发成功')
singleDialogVisible.value = false
await loadData()
} catch (error) {
} catch {
console.log('单卡触发失败')
}
})