Files
one-pipe-system/src/views/order-management/order-list/index.vue
sexygoat e4ed3784e3
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m0s
fix: pay
2026-04-24 11:17:39 +08:00

1299 lines
40 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="order-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
show-expand
@reset="handleReset"
label-width="100"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showCreateDialog" v-permission="'orders:add'">{{
'创建订单'
}}</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="orderList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="120"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 创建订单对话框 -->
<ElDialog
v-model="createDialogVisible"
:title="'创建订单'"
width="600px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElFormItem :label="'订单类型'" prop="order_type">
<ElSelect
v-model="createForm.order_type"
:placeholder="'请选择订单类型'"
style="width: 100%"
>
<ElOption :label="'单卡订单'" value="single_card" />
<ElOption :label="'设备订单'" value="device" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.order_type === 'single_card'"
:label="'IoT卡'"
prop="iot_card_id"
>
<ElSelect
v-model="createForm.iot_card_id"
filterable
remote
reserve-keyword
:placeholder="'请选择IoT卡'"
:remote-method="searchIotCards"
:loading="cardSearchLoading"
style="width: 100%"
clearable
@change="handleIotCardChange"
>
<ElOption
v-for="card in iotCardOptions"
:key="card.id"
:label="`${card.iccid} (${card.msisdn || '无接入号'})`"
:value="card.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ card.msisdn || '无接入号' }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem v-if="createForm.order_type === 'device'" :label="'设备'" prop="device_id">
<ElSelect
v-model="createForm.device_id"
filterable
remote
reserve-keyword
:placeholder="'请选择设备'"
:remote-method="searchDevices"
:loading="deviceSearchLoading"
style="width: 100%"
clearable
@change="handleDeviceChange"
>
<ElOption
v-for="device in deviceOptions"
:key="device.id"
:label="`${device.virtual_no} (${device.device_name})`"
:value="device.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ device.virtual_no }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ device.device_name }}
</span>
</div>
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem :label="'套餐'" prop="package_id">
<ElSelect
v-model="createForm.package_id"
:placeholder="'请选择套餐'"
filterable
reserve-keyword
:loading="packageSearchLoading"
:disabled="!createForm.iot_card_id && !createForm.device_id"
clearable
style="width: 100%"
>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
:label="`${pkg.package_name} (¥${((pkg.cost_price || 0) / 100).toFixed(2)})`"
:value="pkg.id"
>
<div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
</span>
</div>
</ElOption>
</ElSelect>
<template v-if="noSeriesId">
<div style="color: var(--el-text-color-warning); font-size: 12px; margin-top: 4px">
该设备未关联套餐系列无法购买套餐
</div>
</template>
<template v-else-if="packagesEmptyWithSeriesId">
<div style="color: var(--el-text-color-info); font-size: 12px; margin-top: 4px">
该系列暂无可用套餐
</div>
</template>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
>
<ElOption label="钱包支付" value="wallet" />
<!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
<ElOption
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
label="线下支付"
value="offline"
/>
</ElSelect>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
<template v-if="createForm.payment_method === 'wallet'">
提示: 使用钱包支付时,订单将直接完成
</template>
<template v-else-if="createForm.payment_method === 'offline'">
提示: 线下支付订单需要手动确认支付
</template>
</div>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="支付凭证"
prop="payment_voucher_key"
>
<ElUpload
ref="uploadRef"
:auto-upload="false"
:on-change="handleVoucherFileChange"
:on-remove="handleRemoveVoucher"
accept="image/*"
list-type="picture-card"
:limit="1"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div>
</template>
</ElUpload>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">{{ '取消' }}</ElButton>
<ElButton
type="primary"
@click="handleCreateOrder(createFormRef)"
:loading="createLoading"
>
{{ '提交' }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 订单详情对话框 -->
<ElDialog v-model="detailDialogVisible" :title="'订单详情'" width="800px">
<div v-if="currentOrder" class="order-detail">
<ElDescriptions :column="2" border>
<ElDescriptionsItem :label="'订单编号'">
{{ currentOrder.order_no }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'订单类型'">
{{ getOrderTypeText(currentOrder.order_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'支付状态'">
<ElTag :type="getPaymentStatusType(currentOrder.payment_status)">
{{ currentOrder.payment_status_text }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem :label="'订单金额'">
{{ formatCurrency(currentOrder.total_amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'支付方式'">
{{ getPaymentMethodText(currentOrder.payment_method) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'买家类型'">
{{ getBuyerTypeText(currentOrder.buyer_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'买家手机号'">
{{ currentOrder.buyer_phone || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'买家昵称'">
{{ currentOrder.buyer_nickname || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'佣金状态'">
{{ getCommissionStatusText(currentOrder.commission_status) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'支付时间'">
{{ currentOrder.paid_at ? formatDateTime(currentOrder.paid_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'创建时间'">
{{ formatDateTime(currentOrder.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem :label="'更新时间'">
{{ formatDateTime(currentOrder.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- 订单项列表 -->
<div
v-if="currentOrder.items && currentOrder.items.length > 0"
style="margin-top: 20px"
>
<h4>{{ '订单项' }}</h4>
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
<ElTableColumn prop="package_name" :label="'套餐名称'" min-width="150" />
<ElTableColumn prop="package_type" :label="'套餐类型'" width="120">
<template #default="{ row }">
{{
row.package_type === 'formal'
? '正式套餐'
: row.package_type === 'addon'
? '加油包'
: '-'
}}
</template>
</ElTableColumn>
<ElTableColumn prop="quantity" :label="'数量'" width="100" />
<ElTableColumn prop="unit_price" :label="'单价'" width="120">
<template #default="{ row }">
{{ formatCurrency(row.unit_price) }}
</template>
</ElTableColumn>
<ElTableColumn prop="amount" :label="'金额'" width="120">
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="detailDialogVisible = false">{{ '关闭' }}</ElButton>
</div>
</template>
</ElDialog>
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-key="paymentVoucherFileKey"
@close="paymentVoucherFileKey = ''"
/>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import {
OrderService,
CardService,
DeviceService,
PackageManageService,
StorageService,
ShopService
} from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules, UploadInstance, UploadFile } from 'element-plus'
import type {
Order,
OrderQueryParams,
CreateOrderRequest,
PaymentStatus,
OrderType,
BuyerType,
OrderPaymentMethod,
OrderCommissionStatus,
StandaloneIotCard,
Device,
PackageResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'OrderList' })
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const loading = ref(false)
const createLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const currentOrder = ref<Order | null>(null)
const paymentVoucherFileKey = ref('')
// 搜索表单初始值
const initialSearchState: OrderQueryParams = {
order_no: '',
buyer_phone: '',
payment_status: undefined,
payment_method: undefined,
order_type: undefined,
purchase_role: undefined,
identifier: '',
is_expired: undefined,
seller_shop_id: undefined,
start_time: '',
end_time: ''
}
// 搜索表单
const searchForm = reactive<OrderQueryParams>({ ...initialSearchState })
// 店铺列表(用于代理商搜索)
const shopOptions = ref<any[]>([])
// 搜索代理商
const searchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search shops failed:', error)
}
}
// 获取订单角色文本
const getPurchaseRoleText = (role: string): string => {
const roleMap: Record<string, string> = {
self_purchase: '自己购买',
purchased_by_parent: '上级代理购买',
purchased_by_platform: '平台代购',
purchase_for_subordinate: '给下级购买'
}
return roleMap[role] || role
}
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '订单号',
prop: 'order_no',
type: 'input',
placeholder: '请输入订单号',
config: {
maxlength: 50,
clearable: true
},
labelWidth: '100'
},
{
label: '资产标识符',
prop: 'identifier',
type: 'input',
config: {
maxlength: 30,
clearable: true,
placeholder: 'ICCID/VirtualNo/IMEI/SN/MSISDN'
},
labelWidth: '100'
},
{
label: '买家手机号',
prop: 'buyer_phone',
type: 'input',
placeholder: '请输入买家手机号',
config: {
maxlength: 20,
clearable: true
},
labelWidth: '100'
},
{
label: '支付方式',
prop: 'payment_method',
type: 'select',
placeholder: '请选择支付方式',
options: [
{ label: '钱包支付', value: 'wallet' },
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝支付', value: 'alipay' },
{ label: '线下支付', value: 'offline' }
],
config: {
clearable: true
}
},
{
label: '支付状态',
prop: 'payment_status',
type: 'select',
placeholder: '请选择支付状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已取消', value: 3 },
{ label: '已退款', value: 4 }
],
config: {
clearable: true
}
},
{
label: '是否过期',
prop: 'is_expired',
type: 'select',
placeholder: '请选择',
options: [
{ label: '已过期', value: true },
{ label: '未过期', value: false }
],
config: {
clearable: true
}
},
{
label: '订单类型',
prop: 'order_type',
type: 'select',
placeholder: '请选择订单类型',
options: [
{ label: '单卡订单', value: 'single_card' },
{ label: '设备订单', value: 'device' }
],
config: {
clearable: true
}
},
{
label: '所属代理商',
prop: 'seller_shop_id',
type: 'select',
placeholder: '请选择所属代理商',
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query)
}
},
{
label: '订单渠道',
prop: 'purchase_role',
type: 'select',
placeholder: '请选择订单渠道',
options: [
{ label: '自己购买', value: 'self_purchase' },
{ label: '上级代理购买', value: 'purchased_by_parent' },
{ label: '平台代购', value: 'purchased_by_platform' },
{ label: '给下级购买', value: 'purchase_for_subordinate' }
],
config: {
clearable: true
}
},
{
label: '开始至结束',
prop: 'dateRange',
type: 'date',
config: {
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '订单编号', prop: 'order_no' },
{ label: '买家手机号', prop: 'buyer_phone' },
{ label: '买家昵称', prop: 'buyer_nickname' },
{ label: '订单类型', prop: 'order_type' },
{ label: '买家类型', prop: 'buyer_type' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '订单渠道', prop: 'purchase_role' },
{ label: '购买备注', prop: 'purchase_remark' },
{ label: '操作者', prop: 'operator_name' },
{ label: '支付状态', prop: 'payment_status' },
{ label: '佣金状态', prop: 'commission_status_name' },
{ label: '订单金额', prop: 'total_amount' },
{ label: '实付金额', prop: 'actual_paid_amount' },
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付时间', prop: 'paid_at' }
]
const createFormRef = ref<FormInstance>()
const uploadRef = ref<UploadInstance>()
// 处理支付凭证文件选择
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
const file = uploadFile.raw
if (!file) return
// 验证文件类型
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
uploadRef.value?.clearFiles()
return
}
// 验证文件大小
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!')
uploadRef.value?.clearFiles()
return
}
// 开始上传
try {
ElMessage.info('正在上传支付凭证...')
// 1. 获取上传 URL
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: file.type,
purpose: 'attachment'
})
if (uploadUrlRes.code !== 0) {
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
uploadRef.value?.clearFiles()
return
}
const { upload_url, file_key } = uploadUrlRes.data
// 2. 上传文件到对象存储
await StorageService.uploadFile(upload_url, file, file.type)
// 3. 保存 file_key
createForm.payment_voucher_key = file_key
ElMessage.success('上传成功')
} catch (error: any) {
console.error('上传失败:', error)
console.log(error.message || '上传失败,请重试')
createForm.payment_voucher_key = undefined
uploadRef.value?.clearFiles()
}
}
// 删除支付凭证
const handleRemoveVoucher = () => {
createForm.payment_voucher_key = undefined
}
const createRules = computed<FormRules>(() => {
const baseRules: FormRules = {
order_type: [
{
required: true,
message: '请选择订单类型',
trigger: 'change'
}
],
package_ids: [
{
required: true,
message: '请选择套餐',
trigger: 'change'
}
],
payment_method: [
{
required: true,
message: '请选择支付方式',
trigger: 'change'
}
]
}
// 线下支付时,支付凭证为必填
if (createForm.payment_method === 'offline') {
baseRules.payment_voucher_key = [
{
required: true,
message: '请上传支付凭证',
trigger: 'change'
}
]
}
return baseRules
})
const createForm = reactive({
order_type: 'single_card' as OrderType,
package_id: undefined as number | undefined, // 前端使用单选
iot_card_id: null as number | null,
iot_card_series_id: null as number | null, // IoT卡的套餐系列ID
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 // 线下支付凭证
})
const orderList = ref<Order[]>([])
// 套餐搜索相关
const packageOptions = ref<PackageResponse[]>([])
const packageSearchLoading = ref(false)
const isLoadingPackagesFromSelection = ref(false)
// IoT卡搜索相关
const iotCardOptions = ref<StandaloneIotCard[]>([])
const cardSearchLoading = ref(false)
// 设备搜索相关
const deviceOptions = ref<Device[]>([])
const deviceSearchLoading = ref(false)
// 套餐列表为空但有seriesId的提示
const packagesEmptyWithSeriesId = ref(false)
// 检查是否选择了卡/设备但没有关联套餐系列
const noSeriesId = computed(() => {
if (createForm.order_type === 'single_card' && createForm.iot_card_id) {
return !createForm.iot_card_series_id
}
if (createForm.order_type === 'device' && createForm.device_id) {
return !createForm.device_series_id
}
return false
})
// IoT卡选择变化时根据series_id加载套餐列表
const handleIotCardChange = (cardId: number | null) => {
if (!cardId) {
createForm.iot_card_series_id = null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
return
}
const selectedCard = iotCardOptions.value.find((card) => card.id === cardId)
createForm.iot_card_series_id = selectedCard?.series_id || null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
if (selectedCard?.series_id) {
loadDefaultPackages(selectedCard.series_id)
} else {
packageOptions.value = []
}
}
// 设备选择变化时根据series_id加载套餐列表
const handleDeviceChange = (deviceId: number | null) => {
if (!deviceId) {
createForm.device_series_id = null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
return
}
const selectedDevice = deviceOptions.value.find((dev) => dev.id === deviceId)
createForm.device_series_id = selectedDevice?.series_id || null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
if (selectedDevice?.series_id) {
loadDefaultPackages(selectedDevice.series_id)
} else {
packageOptions.value = []
}
}
// 搜索IoT卡根据ICCID
const searchIotCards = async (query: string) => {
cardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
iccid: query || undefined,
page: 1,
page_size: 20
})
if (res.code === 0) {
iotCardOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search IoT cards failed:', error)
iotCardOptions.value = []
} finally {
cardSearchLoading.value = false
}
}
// 搜索设备根据设备号virtual_no
const searchDevices = async (query: string) => {
deviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
virtual_no: query || undefined,
page: 1,
page_size: 20
})
if (res.code === 0) {
deviceOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search devices failed:', error)
deviceOptions.value = []
} finally {
deviceSearchLoading.value = false
}
}
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取支付状态标签类型
const getPaymentStatusType = (
status: PaymentStatus
): 'success' | 'info' | 'warning' | 'danger' => {
const statusMap: Record<PaymentStatus, 'success' | 'info' | 'warning' | 'danger'> = {
1: 'warning', // 待支付
2: 'success', // 已支付
3: 'info', // 已取消
4: 'danger' // 已退款
}
return statusMap[status] || 'info'
}
// 获取订单类型文本
const getOrderTypeText = (type: OrderType): string => {
return type === 'single_card' ? '单卡订单' : '设备订单'
}
// 获取买家类型文本
const getBuyerTypeText = (type: BuyerType): string => {
return type === 'personal' ? '个人' : '代理'
}
// 获取支付方式文本
const getPaymentMethodText = (method: OrderPaymentMethod): string => {
const methodMap: Record<OrderPaymentMethod, string> = {
wallet: '钱包',
wechat: '微信',
alipay: '支付宝',
offline: '线下'
}
return methodMap[method] || method
}
// 获取佣金状态文本
const getCommissionStatusText = (status: OrderCommissionStatus): string => {
const statusMap: Record<OrderCommissionStatus, string> = {
0: '不适用',
1: '待结算',
2: '已结算'
}
return statusMap[status] || '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'order_no',
label: '订单编号',
minWidth: 220,
formatter: (row: Order) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.order_no
)
}
},
{
prop: 'buyer_phone',
label: '买家手机号',
width: 130,
formatter: (row: Order) => row.buyer_phone || '-'
},
{
prop: 'buyer_nickname',
label: '买家昵称',
width: 130,
formatter: (row: Order) => row.buyer_nickname || '-'
},
{
prop: 'order_type',
label: '订单类型',
width: 120,
formatter: (row: Order) => {
return h(ElTag, { type: row.order_type === 'single_card' ? 'primary' : 'success' }, () =>
getOrderTypeText(row.order_type)
)
}
},
{
prop: 'buyer_type',
label: '买家类型',
width: 120,
formatter: (row: Order) => {
return h(ElTag, { type: row.buyer_type === 'personal' ? 'info' : 'warning' }, () =>
getBuyerTypeText(row.buyer_type)
)
}
},
{
prop: 'payment_status',
label: '支付状态',
width: 120,
formatter: (row: Order) => {
return h(
ElTag,
{ type: getPaymentStatusType(row.payment_status) },
() => row.payment_status_text
)
}
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 180,
formatter: (row: Order) => row.asset_identifier || '-'
},
{
prop: 'purchase_role',
label: '订单渠道',
width: 140,
formatter: (row: Order) => {
if (!row.purchase_role) return '-'
const roleTypeMap: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
self_purchase: 'success',
purchased_by_parent: 'warning',
purchased_by_platform: 'danger',
purchase_for_subordinate: 'info'
}
return h(
ElTag,
{ type: roleTypeMap[row.purchase_role || ''] || 'info', size: 'small' },
() => getPurchaseRoleText(row.purchase_role || '')
)
}
},
{
prop: 'purchase_remark',
label: '购买备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Order) => row.purchase_remark || '-'
},
{
prop: 'operator_name',
label: '操作者',
width: 120,
formatter: (row: Order) => row.operator_name || '-'
},
{
prop: 'commission_status_name',
label: '佣金状态',
width: 120,
formatter: (row: Order) => row.commission_status_name || '-'
},
{
prop: 'total_amount',
label: '订单金额',
width: 120,
formatter: (row: Order) => formatCurrency(row.total_amount)
},
{
prop: 'actual_paid_amount',
label: '实付金额',
width: 120,
formatter: (row: Order) => {
if (row.actual_paid_amount === undefined || row.actual_paid_amount === null) return '-'
return formatCurrency(row.actual_paid_amount)
}
},
{
prop: 'payment_method',
label: '支付方式',
width: 120,
formatter: (row: Order) => getPaymentMethodText(row.payment_method)
},
{
prop: 'paid_at',
label: '支付时间',
width: 180,
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
}
])
// 当订单类型切换时,清空已选择的套餐
watch(
() => createForm.order_type,
() => {
createForm.package_id = undefined
packageOptions.value = []
}
)
onMounted(() => {
getTableData()
})
// 获取订单列表
const getTableData = async () => {
loading.value = true
try {
const params: OrderQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
order_no: searchForm.order_no || undefined,
buyer_phone: searchForm.buyer_phone || undefined,
payment_status: searchForm.payment_status,
payment_method: searchForm.payment_method,
order_type: searchForm.order_type,
purchase_role: searchForm.purchase_role,
identifier: searchForm.identifier || undefined,
is_expired: searchForm.is_expired,
seller_shop_id: searchForm.seller_shop_id,
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.start_time = searchForm.dateRange[0]
searchForm.end_time = searchForm.dateRange[1]
} else {
searchForm.start_time = ''
searchForm.end_time = ''
}
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建订单对话框
const showCreateDialog = async () => {
createDialogVisible.value = true
// IoT卡和设备列表在用户搜索时才加载不在此处预加载
}
// 加载默认套餐列表可选按series_id筛选
const loadDefaultPackages = async (seriesId?: number) => {
packageSearchLoading.value = true
isLoadingPackagesFromSelection.value = true
packagesEmptyWithSeriesId.value = false
try {
const res = await PackageManageService.getPackages({
series_id: seriesId,
page: 1,
page_size: 20,
status: 1, // 只获取启用的套餐
shelf_status: 1 // 只获取已上架的套餐
})
if (res.code === 0) {
packageOptions.value = res.data.items || []
// seriesId存在但返回空列表时显示提示
if (seriesId && (!res.data.items || res.data.items.length === 0)) {
packagesEmptyWithSeriesId.value = true
}
}
} catch (error) {
console.error('Load default packages failed:', error)
packageOptions.value = []
} finally {
packageSearchLoading.value = false
isLoadingPackagesFromSelection.value = false
}
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
// 重置表单(会同时清除验证状态)
createFormRef.value?.resetFields()
// 重置表单数据到初始值
createForm.order_type = 'single_card'
createForm.package_id = undefined
createForm.iot_card_id = null
createForm.device_id = null
createForm.payment_method = 'wallet'
createForm.payment_voucher_key = undefined
// 清空上传文件列表
uploadRef.value?.clearFiles()
// 清空套餐、IoT卡和设备搜索结果
packageOptions.value = []
iotCardOptions.value = []
deviceOptions.value = []
}
// 创建订单
const handleCreateOrder = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
// 获取 identifier (ICCID 或 VirtualNo)
let identifier = ''
if (createForm.order_type === 'single_card') {
if (!createForm.iot_card_id) {
ElMessage.error('请选择IoT卡')
return
}
const selectedCard = iotCardOptions.value.find(
(card) => card.id === createForm.iot_card_id
)
if (!selectedCard?.iccid) {
ElMessage.error('未找到选中的卡信息')
return
}
identifier = selectedCard.iccid
} else {
if (!createForm.device_id) {
ElMessage.error('请选择设备')
return
}
const selectedDevice = deviceOptions.value.find(
(dev) => dev.id === createForm.device_id
)
if (!selectedDevice?.virtual_no) {
ElMessage.error('未找到选中的设备信息')
return
}
identifier = selectedDevice.virtual_no
}
// 验证必填字段
if (!createForm.package_id) {
ElMessage.error('请选择套餐')
return
}
// 创建订单(后端接收数组,前端单选转为单元素数组)
const data: CreateOrderRequest = {
identifier,
package_ids: [createForm.package_id],
payment_method: createForm.payment_method
}
// 线下支付时,添加支付凭证
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
}
await OrderService.createOrder(data)
// 根据支付方式显示不同的成功消息
if (createForm.payment_method === 'wallet') {
ElMessage.success('订单创建成功,已自动完成支付')
} else {
ElMessage.success('订单创建成功')
}
createDialogVisible.value = false
formEl.resetFields()
await getTableData()
} catch (error: any) {
// 用户取消确认对话框
if (error === 'cancel') {
return
}
console.error(error)
} finally {
createLoading.value = false
}
}
})
}
// 查看订单详情
const showOrderDetail = (row: Order) => {
router.push({
path: `${RoutesAlias.OrderList}/detail/${row.id}`
})
}
// 处理名称点击
const handleNameClick = (row: Order) => {
if (hasAuth('orders:view_detail')) {
showOrderDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 取消订单
const handleCancelOrder = (row: Order) => {
// 已支付的订单不能取消
if (row.payment_status === 2) {
ElMessage.warning('已支付的订单无法取消')
return
}
ElMessageBox.confirm('确定要取消该订单吗?', '取消订单', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await OrderService.cancelOrder(row.id)
ElMessage.success('订单已取消')
await getTableData()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 用户取消操作
})
}
// 获取操作按钮
const getActions = (row: Order) => {
const actions: any[] = []
// 线下支付且有凭证可以查看
if (
row.payment_method === 'offline' &&
row.payment_voucher_key &&
hasAuth('orders:view_payment_voucher')
) {
actions.push({
label: '查看支付凭证',
handler: () => handleViewPaymentVoucher(row),
type: 'primary'
})
}
// 只有待支付的订单可以删除
if (row.payment_status === 1 && hasAuth('orders:delete')) {
actions.push({
label: '删除',
handler: () => handleCancelOrder(row),
type: 'danger'
})
}
return actions
}
// 查看支付凭证
const handleViewPaymentVoucher = (row: Order) => {
if (!row.payment_voucher_key) return
paymentVoucherFileKey.value = row.payment_voucher_key
}
</script>
<style scoped lang="scss">
.order-list-page {
height: 100%;
}
.order-detail {
:deep(.el-descriptions__label) {
width: 140px;
}
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>