Files
ApiServer-Web-admin_dashboa…/src/views/order/OrderList.vue
T
shiran 4180f73c53
Build and Deploy Vue3 / build (push) Successful in 1m27s
Build and Deploy Vue3 / deploy (push) Successful in 36s
feat(admin): 订单管理重构、设置管理增强、短信签名模板管理及通知渠道优化
- 订单列表重构为卡片式布局并新增筛选功能

- 设置管理支持struct/struct_list类型配置

- 新增短信签名和模板独立管理页面

- 通知渠道新增短信渠道配置

- 产品参数管理优化

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 18:27:23 +08:00

1323 lines
44 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>
<div class="order-list-container">
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="关键词">
<el-input v-model="queryParams.key" placeholder="订单名称/ID" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户ID">
<el-input v-model="queryParams.user_id" placeholder="用户ID" clearable style="width: 120px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="用户关键词">
<el-input v-model="queryParams.user_key" placeholder="用户名/手机号/邮箱" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.state" placeholder="全部" clearable style="width: 120px">
<el-option label="待支付" value="0" />
<el-option label="已支付" value="1" />
<el-option label="已失效" value="2" />
</el-select>
</el-form-item>
<el-form-item label="错误信息">
<el-select v-model="queryParams.error" placeholder="全部" clearable style="width: 140px">
<el-option label="有错误的订单" :value="true" />
<el-option label="无错误的订单" :value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增订单
</el-button>
<el-button type="success" @click="fetchOrderList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 订单列表 -->
<div class="table-section">
<!-- 骨架屏 -->
<div v-if="loading" class="skeleton-container">
<div v-for="i in 5" :key="i" class="skeleton-row">
<div class="skeleton-cell skeleton-checkbox"></div>
<div class="skeleton-cell skeleton-id"></div>
<div class="skeleton-cell skeleton-name"></div>
<div class="skeleton-cell skeleton-user"></div>
<div class="skeleton-cell skeleton-price"></div>
<div class="skeleton-cell skeleton-status"></div>
<div class="skeleton-cell skeleton-time"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<el-table
v-else
v-loading="loading"
:data="orderList"
@selection-change="handleSelectionChange"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="订单ID" width="100" />
<el-table-column prop="name" label="订单名称" min-width="180" />
<el-table-column label="类型" width="90">
<template #default="{ row }">
<el-tag size="small" :type="row.type === 'create' ? 'success' : row.type === 'renew' ? 'warning' : 'info'">{{ getTypeText(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="用户ID" width="100">
<template #default="{ row }">
<el-link v-if="row.userId" type="primary" :underline="false" @click.stop="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品ID" width="100">
<template #default="{ row }">
<el-link v-if="row.commodityId" type="primary" :underline="false" @click.stop="router.push({ path: '/user-goods/list', query: { good_id: row.commodityId } })">{{ row.commodityId }}</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="表名" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.table || '未知' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="订单金额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ (row.price / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="续费价格" width="120">
<template #default="{ row }">
<span class="renew-price">¥{{ (row.renewPrice / 100).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="数量" width="80">
<template #default="{ row }">
<span>{{ row.payNum }}</span>
</template>
</el-table-column>
<el-table-column label="订单状态" width="120">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-tag :type="getStatusType(row.state)">
{{ getStatusText(row.state) }}
</el-tag>
<el-tag v-if="row.error" type="danger" size="small">异常</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="错误信息" min-width="250">
<template #default="{ row }">
<el-tooltip v-if="row.error" :content="row.error" placement="top" :show-after="300">
<span class="error-text">{{ row.error }}</span>
</el-tooltip>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="支付方式" width="100">
<template #default="{ row }">
<span>{{ row.payType || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="过期时间" width="170">
<template #default="{ row }">
<span>{{ formatDate(row.expireTime) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">
<span>{{ formatDate(row.CreatedAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 订单详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="订单详情"
width="800px"
append-to-body
>
<el-descriptions :column="2" border v-if="orderDetail">
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
<el-descriptions-item label="订单名称">{{ orderDetail.name }}</el-descriptions-item>
<el-descriptions-item label="订单类型">
<el-tag size="small">{{ getTypeText(orderDetail.type) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ orderDetail.userId }}</el-descriptions-item>
<el-descriptions-item label="商品ID">{{ orderDetail.commodityId }}</el-descriptions-item>
<el-descriptions-item label="套餐ID">{{ orderDetail.planId || '-' }}</el-descriptions-item>
<el-descriptions-item label="表名">{{ orderDetail.table }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ orderDetail.payNum }}</el-descriptions-item>
<el-descriptions-item label="订单金额">¥{{ (orderDetail.price / 100).toFixed(2) }}</el-descriptions-item>
<el-descriptions-item label="续费价格">¥{{ (orderDetail.renewPrice / 100).toFixed(2) }}</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag :type="getStatusType(orderDetail.state)">
{{ getStatusText(orderDetail.state) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="支付方式">{{ orderDetail.payType || '-' }}</el-descriptions-item>
<el-descriptions-item label="过期时间">{{ formatDate(orderDetail.expireTime) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(orderDetail.CreatedAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(orderDetail.UpdatedAt) }}</el-descriptions-item>
<el-descriptions-item label="参数信息">{{ orderDetail.args || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="orderDetail.error" label="错误信息" :span="2">
<el-tag type="danger" size="small" style="margin-right: 6px;">异常</el-tag>
<span style="color: #f56c6c;">{{ orderDetail.error }}</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ orderDetail.note || '无' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 订单表单对话框分步向导 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
width="820px"
append-to-body
destroy-on-close
>
<div class="wizard-container">
<el-steps :active="currentStep" finish-status="success" align-center class="wizard-steps">
<el-step title="选择用户" description="指定订单用户与类型" />
<el-step title="商品配置" description="选择商品与套餐" />
<el-step title="订单详情" description="价格、支付与优惠" />
</el-steps>
<div class="wizard-body">
<!-- Step 1: 用户与订单类型 -->
<el-form v-show="currentStep === 0" ref="step1FormRef" :model="orderForm" :rules="step1Rules" label-width="110px">
<el-form-item label="用户" prop="user_id">
<el-input
v-if="selectedUserInfo"
:model-value="`${selectedUserInfo.user_name} (ID: ${orderForm.user_id})`"
readonly
>
<template #suffix>
<el-icon class="clear-icon" @click="clearUser"><Close /></el-icon>
</template>
<template #append>
<el-button @click="userSelectorVisible = true"><el-icon><User /></el-icon></el-button>
</template>
</el-input>
<el-input v-else placeholder="请选择用户" readonly @click="userSelectorVisible = true">
<template #append>
<el-button @click="userSelectorVisible = true"><el-icon><User /></el-icon></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="订单类型" prop="type">
<el-select v-model="orderForm.type" placeholder="请选择订单类型" style="width: 100%" @change="handleTypeChange">
<el-option label="新购" value="create" />
<el-option label="续费" value="renew" />
<el-option label="升级" value="update" />
<el-option label="快照" value="snapshot" />
<el-option label="备份" value="backup" />
<el-option label="数据盘" value="data_volume" />
<el-option label="IPv4" value="ipv4" />
<el-option label="IPv6" value="ipv6" />
</el-select>
</el-form-item>
<el-form-item label="订单名称" prop="name">
<el-input v-model="orderForm.name" placeholder="请输入订单名称" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<!-- Step 2: 商品与套餐 -->
<el-form v-show="currentStep === 1" ref="step2FormRef" :model="orderForm" :rules="step2Rules" label-width="110px">
<el-form-item label="商品" prop="commodity_id">
<el-input
v-if="selectedProductInfo"
:model-value="`${selectedProductInfo.name} (ID: ${orderForm.commodity_id})`"
readonly
>
<template #suffix>
<el-icon class="clear-icon" @click="clearProduct"><Close /></el-icon>
</template>
<template #append>
<el-button @click="productSelectorVisible = true"><el-icon><ShoppingCart /></el-icon></el-button>
</template>
</el-input>
<el-input v-else placeholder="请选择商品" readonly @click="productSelectorVisible = true">
<template #append>
<el-button @click="productSelectorVisible = true"><el-icon><ShoppingCart /></el-icon></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="套餐" prop="plan_id">
<el-select v-model="orderForm.plan_id" placeholder="请选择套餐(可选)" clearable style="width: 100%" :loading="planLoading" @change="handlePlanChange">
<el-option v-for="p in planList" :key="p.id" :label="`${p.name} - ¥${(p.price / 100).toFixed(2)}`" :value="p.id" />
</el-select>
<div v-if="!planList.length && orderForm.commodity_id && !planLoading" class="form-tip">该商品暂无套餐可直接进入下一步手动设置价格</div>
</el-form-item>
<el-form-item label="所属表">
<el-input v-model="orderForm.table" placeholder="选择商品后自动填充,或手动输入" />
</el-form-item>
<!-- 动态商品参数 -->
<div v-if="productParams.length" class="args-section">
<div class="args-section-title">
<el-icon><Setting /></el-icon>商品参数配置
</div>
<el-form-item
v-for="param in productParams"
:key="param.id"
:label="param.name"
:required="param.must"
>
<!-- select 类型 -->
<el-select
v-if="param.type === 'select'"
v-model="argValues[param.id]"
:placeholder="`请选择${param.name}`"
style="width: 100%"
>
<el-option
v-for="attr in param.attrs"
:key="attr.id"
:label="`${attr.name}${attr.price ? ' (¥' + (attr.price / 100).toFixed(2) + ')' : ''}`"
:value="attr.id"
/>
</el-select>
<!-- number 类型 -->
<div v-else-if="param.type === 'number'" class="number-arg-row">
<el-input-number
v-model="argValues[param.id]"
:min="param.min || 0"
:max="param.max || 9999"
:step="param.step || 1"
style="flex: 1"
/>
<span v-if="param.arg_key" class="unit-text">{{ param.arg_key }}</span>
</div>
<!-- string 类型 -->
<el-input
v-else
v-model="argValues[param.id]"
:placeholder="`请输入${param.name}`"
/>
</el-form-item>
</div>
<div v-else-if="paramsLoading" class="form-tip" style="text-align: center; padding: 12px 0;">
<el-icon class="is-loading"><Loading /></el-icon> 加载商品参数中...
</div>
</el-form>
<!-- Step 3: 价格与支付 -->
<el-form v-show="currentStep === 2" ref="step3FormRef" :model="orderForm" :rules="step3Rules" label-width="110px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="价格" prop="price">
<div class="unit-input-row">
<el-input-number v-model="orderForm.price" :min="0" :precision="0" style="flex:1" />
<span class="unit-text"></span>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="续费价格">
<div class="unit-input-row">
<el-input-number v-model="orderForm.renew_price" :min="0" :precision="0" style="flex:1" />
<span class="unit-text"></span>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="购买数量" prop="pay_num">
<el-input-number v-model="orderForm.pay_num" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="过期时间">
<el-date-picker v-model="orderForm.expire_time" type="datetime" placeholder="请选择" format="YYYY-MM-DD HH:mm:ss" value-format="x" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="订单状态">
<el-radio-group v-model="orderForm.state">
<el-radio :value="0">待支付</el-radio>
<el-radio :value="1">已支付</el-radio>
<el-radio :value="2">已失效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="支付方式">
<el-select v-model="orderForm.pay_type" placeholder="请选择" clearable style="width: 100%">
<el-option label="默认(余额)" value="default" />
<el-option label="支付宝" value="ali" />
<el-option label="微信" value="wx" />
</el-select>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="优惠码">
<el-input
v-if="selectedDiscountCodeInfo"
:model-value="`${selectedDiscountCodeInfo.name || selectedDiscountCodeInfo.code} (ID: ${orderForm.discount_code_id})`"
readonly
>
<template #suffix><el-icon class="clear-icon" @click="clearDiscountCode"><Close /></el-icon></template>
<template #append><el-button @click="discountCodeSelectorVisible = true"><el-icon><Ticket /></el-icon></el-button></template>
</el-input>
<el-input v-else placeholder="选择优惠码(可选)" readonly @click="discountCodeSelectorVisible = true">
<template #append><el-button @click="discountCodeSelectorVisible = true"><el-icon><Ticket /></el-icon></el-button></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="代金券">
<el-input
v-if="selectedVoucherInfo"
:model-value="`${selectedVoucherInfo.name || selectedVoucherInfo.code} (ID: ${orderForm.user_coupon_id})`"
readonly
>
<template #suffix><el-icon class="clear-icon" @click="clearVoucher"><Close /></el-icon></template>
<template #append><el-button @click="voucherSelectorVisible = true"><el-icon><Money /></el-icon></el-button></template>
</el-input>
<el-input v-else placeholder="选择代金券(可选)" readonly @click="voucherSelectorVisible = true">
<template #append><el-button @click="voucherSelectorVisible = true"><el-icon><Money /></el-icon></el-button></template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="orderForm.note" type="textarea" :rows="2" placeholder="请输入备注(可选)" />
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<div class="wizard-footer">
<el-button v-if="currentStep > 0" @click="currentStep--">上一步</el-button>
<el-button v-if="currentStep < 2" type="primary" @click="handleNextStep">下一步</el-button>
<el-button v-if="currentStep === 2" type="primary" :loading="submitLoading" @click="submitForm">提交订单</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
<!-- 用户选择器 -->
<UserListSelector
v-model="userSelectorVisible"
:current-user-id="orderForm.user_id"
@confirm="handleUserSelect"
/>
<!-- 商品选择器 -->
<ProductSelector
v-model="productSelectorVisible"
:current-product-id="orderForm.commodity_id"
@confirm="handleProductSelect"
/>
<!-- 优惠码选择器 -->
<DiscountCodeSelector
v-model="discountCodeSelectorVisible"
:current-code-id="orderForm.discount_code_id"
@confirm="handleDiscountCodeSelect"
/>
<!-- 代金券选择器 -->
<VoucherSelector
v-model="voucherSelectorVisible"
:current-voucher-id="orderForm.user_coupon_id"
@confirm="handleVoucherSelect"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close, Setting, Loading } from '@element-plus/icons-vue'
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
import { getProductPlanList, getProductParameterList, getProductParameterDetail } from '@/api/admin/product'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import ProductSelector from '@/components/admin/ProductSelector.vue'
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
import VoucherSelector from '@/components/admin/VoucherSelector.vue'
import { isoToMilliseconds, timeToTimestamp, formatDate as formatDateTool } from '@/utils/tool'
const router = useRouter()
const route = useRoute()
// 查询参数
const queryParams = reactive({
page: 1,
count: 10,
key: '',
state: '',
user_id: '',
user_key: '',
error: null
})
// 订单表单
const orderForm = reactive({
order_id: undefined,
name: '',
table: '',
type: 'create',
user_id: undefined,
commodity_id: 0,
plan_id: null,
pay_num: 1,
price: 0,
renew_price: 0,
expire_time: 0,
discount_code_id: 0,
user_coupon_id: 0,
state: 0,
pay_type: 'default',
payment_order_id: '',
args: '',
note: ''
})
// 分步验证规则
const step1Rules = {
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
type: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
name: [{ required: true, message: '请输入订单名称', trigger: 'blur' }]
}
const step2Rules = {
commodity_id: [{ required: true, message: '请选择商品', trigger: 'change' }]
}
const step3Rules = {
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
pay_num: [{ required: true, message: '请输入数量', trigger: 'blur' }]
}
// 分步向导
const currentStep = ref(0)
const submitLoading = ref(false)
const step1FormRef = ref(null)
const step2FormRef = ref(null)
const step3FormRef = ref(null)
// 套餐相关
const planList = ref([])
const planLoading = ref(false)
// 商品参数相关
const productParams = ref([])
const paramsLoading = ref(false)
const argValues = reactive({})
// 状态数据
const loading = ref(false)
const orderList = ref([])
const orderDetail = ref(null)
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
const dialogType = ref('add')
// 选择器弹窗状态
const userSelectorVisible = ref(false)
const productSelectorVisible = ref(false)
const discountCodeSelectorVisible = ref(false)
const voucherSelectorVisible = ref(false)
// 选择的显示信息
const selectedUserInfo = ref(null)
const selectedProductInfo = ref(null)
const selectedDiscountCodeInfo = ref(null)
const selectedVoucherInfo = ref(null)
// 获取订单列表
const fetchOrderList = async () => {
loading.value = true
try {
// 过滤空值参数
const params = {}
Object.keys(queryParams).forEach(key => {
if (queryParams[key] !== '' && queryParams[key] !== null && queryParams[key] !== undefined) {
params[key] = queryParams[key]
}
})
const res = await getOrderList(params)
console.log('订单列表数据:', res.data)
if (res.data.code === 200) {
// 处理时间数据:将ISO格式转换为毫秒级时间戳(用于时间选择器)
const list = (res.data.data.list || []).map(item => {
if (item.expireTime) {
// 保存原始时间用于显示
item._originalExpireTime = item.expireTime
// 转换为毫秒级时间戳用于时间选择器
item._expireTimeMs = isoToMilliseconds(item.expireTime)
}
return item
})
orderList.value = list
total.value = res.data.data.all_count || 0
}
} catch (error) {
console.error('获取订单列表失败:', error)
ElMessage.error('获取订单列表失败')
} finally {
loading.value = false
}
}
// 格式化日期 - 使用工具函数
const formatDate = (dateStr) => {
return formatDateTool(dateStr)
}
// 获取订单状态类型
const getStatusType = (status) => {
const statusMap = {
0: 'warning', // 待支付
1: 'success', // 已支付
2: 'info', // 已失效
}
return statusMap[status] || 'info'
}
// 获取订单状态文本
const getStatusText = (status) => {
const statusMap = { 0: '待支付', 1: '已支付', 2: '已失效' }
return statusMap[status] || '未知'
}
// 获取订单类型文本
const getTypeText = (type) => {
const typeMap = { create: '新购', renew: '续费', update: '升级', snapshot: '快照', backup: '备份', data_volume: '数据盘', ipv4: 'IPv4', ipv6: 'IPv6' }
return typeMap[type] || type || '-'
}
// 查询
const handleQuery = () => {
queryParams.page = 1
fetchOrderList()
}
// 重置查询
const resetQuery = () => {
queryParams.key = ''
queryParams.state = ''
queryParams.user_id = ''
queryParams.user_key = ''
queryParams.error = null
queryParams.page = 1
fetchOrderList()
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchOrderList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchOrderList()
}
// 新增订单
const handleAdd = () => {
dialogType.value = 'add'
currentStep.value = 0
clearAllSelections()
planList.value = []
productParams.value = []
Object.keys(argValues).forEach(k => delete argValues[k])
Object.assign(orderForm, {
order_id: undefined,
name: '',
table: 'good',
type: 'create',
user_id: undefined,
commodity_id: 0,
plan_id: null,
pay_num: 1,
price: 0,
renew_price: 0,
expire_time: 0,
discount_code_id: 0,
user_coupon_id: 0,
state: 0,
pay_type: 'default',
payment_order_id: '',
args: '',
note: ''
})
dialogVisible.value = true
}
// 查看订单详情
const handleView = async (row) => {
try {
const res = await getOrderDetail({ order_id: row.id })
if (res.data.code === 200) {
orderDetail.value = res.data.data
detailDialogVisible.value = true
}
} catch (error) {
console.error('获取订单详情失败:', error)
ElMessage.error('获取订单详情失败')
}
}
// 编辑订单
const handleEdit = (row) => {
dialogType.value = 'edit'
currentStep.value = 0
clearAllSelections()
planList.value = []
productParams.value = []
Object.keys(argValues).forEach(k => delete argValues[k])
let expireTimeMs = null
if (row._expireTimeMs !== undefined) {
expireTimeMs = row._expireTimeMs
} else if (row.expireTime) {
expireTimeMs = isoToMilliseconds(row.expireTime)
}
Object.assign(orderForm, {
order_id: row.id,
name: row.name,
table: row.table || '',
type: row.type || 'create',
user_id: row.userId,
commodity_id: row.commodityId || 0,
plan_id: row.planId || null,
pay_num: row.payNum,
price: row.price,
renew_price: row.renewPrice,
expire_time: expireTimeMs,
discount_code_id: 0,
user_coupon_id: 0,
state: row.state,
pay_type: row.payType || 'default',
payment_order_id: row.paymentOrderId || '',
args: row.args || '',
note: row.note || ''
})
if (row.userId) {
selectedUserInfo.value = { user_id: row.userId, user_name: `用户${row.userId}` }
}
if (row.commodityId) {
selectedProductInfo.value = { id: row.commodityId, name: `商品${row.commodityId}` }
fetchPlanList(row.commodityId)
fetchProductParams(row.commodityId).then(() => {
// 从已有 args 中恢复参数值
if (row.args) {
try {
const existingArgs = JSON.parse(row.args)
if (Array.isArray(existingArgs)) {
for (const a of existingArgs) {
const param = productParams.value.find(p => p.id === a.arg_id)
if (!param) continue
if (param.type === 'select') {
argValues[param.id] = a.attr_id
} else if (param.type === 'number') {
argValues[param.id] = a.number
} else {
argValues[param.id] = a.value || ''
}
}
}
} catch {}
}
})
}
dialogVisible.value = true
}
// 重试订单流程
const handleRetryOrder = (row) => {
ElMessageBox.confirm(
`确认对订单「${row.name}」(ID: ${row.id}) 重试流程吗?`,
'重试订单流程',
{
confirmButtonText: '确认重试',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await retryOrderHook({ order_id: row.id })
if (res.data.code === 200) {
ElMessage.success('重试流程已触发')
fetchOrderList()
} else {
ElMessage.error(res.data.message || '重试失败')
}
} catch (error) {
console.error('重试订单流程失败:', error)
ElMessage.error(error.response?.data?.message || '重试订单流程失败')
}
}).catch(() => {})
}
// 删除订单
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除订单 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteOrder({ id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchOrderList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('批量删除成功')
fetchOrderList()
}).catch(() => {})
}
// 订单类型变更
const handleTypeChange = (type) => {
if (type === 'create') {
orderForm.table = 'good'
}
}
// 分步导航
const handleNextStep = async () => {
if (currentStep.value === 0) {
const formEl = step1FormRef.value
if (!formEl) return
const valid = await formEl.validate().catch(() => false)
if (!valid) return
} else if (currentStep.value === 1) {
const formEl = step2FormRef.value
if (!formEl) return
const valid = await formEl.validate().catch(() => false)
if (!valid) return
}
currentStep.value++
}
// 加载套餐列表
const fetchPlanList = async (goodId) => {
if (!goodId) { planList.value = []; return }
planLoading.value = true
try {
const res = await getProductPlanList({ good_id: goodId, page: 1, count: 100 })
if (res.data.code === 200) {
planList.value = res.data.data?.list || res.data.data || []
} else {
planList.value = []
}
} catch { planList.value = [] }
finally { planLoading.value = false }
}
// 加载商品参数列表
const fetchProductParams = async (goodId) => {
if (!goodId) { productParams.value = []; return }
paramsLoading.value = true
try {
const res = await getProductParameterList({ good_id: goodId })
if (res.data.code === 200) {
const list = res.data.data || []
await Promise.all(list.map(async (param) => {
try {
const detail = await getProductParameterDetail({ good_id: goodId, arg_id: param.id })
if (detail.data.code === 200) {
const attrs = detail.data.data.attrs || []
attrs.sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0))
param.attrs = attrs
} else { param.attrs = [] }
} catch { param.attrs = [] }
}))
productParams.value = list
// 初始化默认值
for (const param of list) {
if (argValues[param.id] !== undefined) continue
if (param.type === 'select' && param.attrs?.length) {
argValues[param.id] = param.attrs[0].id
} else if (param.type === 'number') {
argValues[param.id] = param.min || 0
} else {
argValues[param.id] = ''
}
}
} else { productParams.value = [] }
} catch { productParams.value = [] }
finally { paramsLoading.value = false }
}
// 根据参数表单值构建 args JSON
const buildArgsJson = () => {
if (!productParams.value.length) return ''
const args = []
for (const param of productParams.value) {
const val = argValues[param.id]
if (val === undefined || val === null || val === '') continue
if (param.type === 'select') {
const attr = param.attrs?.find(a => a.id === val)
if (attr) {
args.push({ arg_id: param.id, name: param.name, attr_id: attr.id, value: attr.value || attr.name })
}
} else if (param.type === 'number') {
const matchedAttr = findMatchedAttr(param, val)
args.push({ arg_id: param.id, name: param.name, attr_id: matchedAttr?.id || (param.attrs?.[0]?.id || 0), number: val })
} else {
const attr = param.attrs?.[0]
args.push({ arg_id: param.id, name: param.name, attr_id: attr?.id || 0, value: val })
}
}
return args.length > 0 ? JSON.stringify(args) : ''
}
const findMatchedAttr = (param, numValue) => {
if (!param.attrs || !param.attrs.length) return null
const sorted = [...param.attrs].sort((a, b) => (a.phase || 0) - (b.phase || 0))
for (const attr of sorted) {
const phase = attr.phase || 0
if (attr.rangeType === 'before' && numValue <= phase) return attr
if (attr.rangeType === 'after' && numValue >= phase) return attr
if (attr.rangeType === 'equal' && numValue === phase) return attr
}
return sorted[sorted.length - 1]
}
// 套餐选择变更
const handlePlanChange = (planId) => {
if (!planId) return
const plan = planList.value.find(p => p.id === planId)
if (plan) {
if (plan.price) orderForm.price = plan.price
if (plan.renew_price) orderForm.renew_price = plan.renew_price
}
}
// 提交表单
const submitForm = async () => {
const formEl = step3FormRef.value
if (formEl) {
const valid = await formEl.validate().catch(() => false)
if (!valid) return
}
submitLoading.value = true
try {
let expireTimeStr = ''
if (orderForm.expire_time) {
expireTimeStr = new Date(Number(orderForm.expire_time)).toISOString()
}
const argsStr = productParams.value.length ? buildArgsJson() : orderForm.args || ''
const submitData = {
name: orderForm.name,
table: orderForm.table || '',
type: orderForm.type,
user_id: Number(orderForm.user_id),
commodity_id: Number(orderForm.commodity_id),
pay_num: Number(orderForm.pay_num),
price: Number(orderForm.price) / 100,
renew_price: Number(orderForm.renew_price) / 100,
expire_time: expireTimeStr,
state: Number(orderForm.state),
pay_type: orderForm.pay_type || 'default',
args: argsStr,
note: orderForm.note || ''
}
if (orderForm.plan_id) submitData.plan_id = Number(orderForm.plan_id)
if (orderForm.discount_code_id) submitData.discount_code_id = Number(orderForm.discount_code_id)
if (orderForm.user_coupon_id) submitData.user_coupon_id = Number(orderForm.user_coupon_id)
if (orderForm.payment_order_id) submitData.payment_order_id = orderForm.payment_order_id
if (dialogType.value === 'edit') {
submitData.order_id = Number(orderForm.order_id)
}
let res
if (dialogType.value === 'add') {
res = await createOrder(submitData)
} else {
res = await updateOrder(submitData)
}
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchOrderList()
} else {
ElMessage.error(res.data.message || '操作失败')
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
} finally {
submitLoading.value = false
}
}
// 用户选择处理
const handleUserSelect = (user) => {
orderForm.user_id = user.user_id
selectedUserInfo.value = user
}
const clearUser = () => {
orderForm.user_id = undefined
selectedUserInfo.value = null
}
// 商品选择处理
const handleProductSelect = (product) => {
orderForm.commodity_id = product.id
selectedProductInfo.value = product
if (product.table) orderForm.table = product.table
orderForm.plan_id = null
// 清空旧参数值
Object.keys(argValues).forEach(k => delete argValues[k])
fetchPlanList(product.id)
fetchProductParams(product.id)
}
const clearProduct = () => {
orderForm.commodity_id = 0
orderForm.plan_id = null
selectedProductInfo.value = null
planList.value = []
productParams.value = []
Object.keys(argValues).forEach(k => delete argValues[k])
}
// 优惠码选择处理
const handleDiscountCodeSelect = (code) => {
orderForm.discount_code_id = code.id
selectedDiscountCodeInfo.value = code
}
const clearDiscountCode = () => {
orderForm.discount_code_id = 0
selectedDiscountCodeInfo.value = null
}
// 代金券选择处理
const handleVoucherSelect = (voucher) => {
orderForm.user_coupon_id = voucher.id
selectedVoucherInfo.value = voucher
}
const clearVoucher = () => {
orderForm.user_coupon_id = 0
selectedVoucherInfo.value = null
}
// 清除所有选择信息
const clearAllSelections = () => {
selectedUserInfo.value = null
selectedProductInfo.value = null
selectedDiscountCodeInfo.value = null
selectedVoucherInfo.value = null
}
// 初始化
onMounted(() => {
if (route.query.key) queryParams.key = String(route.query.key)
if (route.query.user_id) queryParams.user_id = String(route.query.user_id)
if (route.query.state) queryParams.state = String(route.query.state)
fetchOrderList()
})
</script>
<style scoped>
.order-list-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 8px;
}
.filter-form :deep(.el-form-item__label) {
font-size: 13px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.amount {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
}
.renew-price {
color: #409eff;
font-weight: 500;
font-size: 14px;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
/* 分步向导样式 */
.wizard-container {
padding: 0 10px;
}
.wizard-steps {
margin-bottom: 30px;
}
.wizard-body {
min-height: 280px;
}
.wizard-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
.args-section {
margin-top: 16px;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #ebeef5;
}
.args-section-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.number-arg-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 100px; }
.skeleton-name { width: 180px; }
.skeleton-user { width: 100px; }
.skeleton-price { width: 120px; }
.skeleton-status { width: 100px; }
.skeleton-time { width: 170px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 选择器清除图标样式 */
.clear-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
}
.clear-icon:hover {
color: #f56c6c;
}
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
.error-text {
color: #f56c6c;
font-size: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
cursor: pointer;
}
.text-muted {
color: #c0c4cc;
}
</style>