526 lines
13 KiB
Vue
526 lines
13 KiB
Vue
|
|
<template>
|
||
|
|
<div class="orders">
|
||
|
|
<!-- 页面标题和操作 -->
|
||
|
|
<div class="page-header">
|
||
|
|
<div class="page-title">
|
||
|
|
<h2>
|
||
|
|
<el-icon><List /></el-icon>
|
||
|
|
订单管理
|
||
|
|
</h2>
|
||
|
|
</div>
|
||
|
|
<div class="page-actions">
|
||
|
|
<el-button type="primary" @click="$router.push('/orders/create')">
|
||
|
|
<el-icon><Plus /></el-icon>
|
||
|
|
创建订单
|
||
|
|
</el-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 筛选和搜索 -->
|
||
|
|
<el-card class="filter-card">
|
||
|
|
<el-row :gutter="20">
|
||
|
|
<el-col :xs="24" :sm="12" :md="8">
|
||
|
|
<el-select
|
||
|
|
v-model="filters.status"
|
||
|
|
placeholder="选择订单状态"
|
||
|
|
clearable
|
||
|
|
@change="handleFilterChange"
|
||
|
|
>
|
||
|
|
<el-option
|
||
|
|
v-for="status in orderStatuses"
|
||
|
|
:key="status.value"
|
||
|
|
:label="status.label"
|
||
|
|
:value="status.value"
|
||
|
|
/>
|
||
|
|
</el-select>
|
||
|
|
</el-col>
|
||
|
|
<el-col :xs="24" :sm="12" :md="8">
|
||
|
|
<el-input
|
||
|
|
v-model="filters.search"
|
||
|
|
placeholder="搜索订单号"
|
||
|
|
clearable
|
||
|
|
@input="handleSearch"
|
||
|
|
>
|
||
|
|
<template #prefix>
|
||
|
|
<el-icon><Search /></el-icon>
|
||
|
|
</template>
|
||
|
|
</el-input>
|
||
|
|
</el-col>
|
||
|
|
<el-col :xs="24" :sm="12" :md="8">
|
||
|
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||
|
|
</el-col>
|
||
|
|
</el-row>
|
||
|
|
</el-card>
|
||
|
|
|
||
|
|
<!-- 订单列表 -->
|
||
|
|
<el-card class="orders-card">
|
||
|
|
<el-table
|
||
|
|
:data="orderStore.orders"
|
||
|
|
v-loading="orderStore.loading"
|
||
|
|
empty-text="暂无订单"
|
||
|
|
@sort-change="handleSortChange"
|
||
|
|
>
|
||
|
|
<el-table-column prop="orderNumber" label="订单号" width="150" sortable="custom">
|
||
|
|
<template #default="{ row }">
|
||
|
|
<router-link :to="`/orders/${row.id}`" class="order-link">
|
||
|
|
{{ row.orderNumber }}
|
||
|
|
</router-link>
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
|
||
|
|
<el-table-column prop="totalAmount" label="金额" width="120" sortable="custom">
|
||
|
|
<template #default="{ row }">
|
||
|
|
<span class="amount">{{ row.currency }} {{ row.totalAmount }}</span>
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
|
||
|
|
<el-table-column prop="status" label="状态" width="120">
|
||
|
|
<template #default="{ row }">
|
||
|
|
<el-tag :type="getStatusType(row.status)">
|
||
|
|
{{ getStatusText(row.status) }}
|
||
|
|
</el-tag>
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
|
||
|
|
<el-table-column prop="orderType" label="类型" width="120">
|
||
|
|
<template #default="{ row }">
|
||
|
|
{{ getOrderTypeText(row.orderType) }}
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
|
||
|
|
<el-table-column prop="createdAt" label="创建时间" width="160" sortable="custom">
|
||
|
|
<template #default="{ row }">
|
||
|
|
{{ formatDate(row.createdAt) }}
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
|
||
|
|
<el-table-column label="操作" width="200" fixed="right">
|
||
|
|
<template #default="{ row }">
|
||
|
|
<el-button-group>
|
||
|
|
<el-button size="small" @click="$router.push(`/orders/${row.id}`)">
|
||
|
|
查看
|
||
|
|
</el-button>
|
||
|
|
|
||
|
|
<el-dropdown v-if="canPay(row)" trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handlePayment(row, command)">
|
||
|
|
<el-button size="small" type="success">
|
||
|
|
支付<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||
|
|
</el-button>
|
||
|
|
<template #dropdown>
|
||
|
|
<el-dropdown-menu>
|
||
|
|
<el-dropdown-item command="ALIPAY">
|
||
|
|
<el-icon><CreditCard /></el-icon>
|
||
|
|
支付宝支付
|
||
|
|
</el-dropdown-item>
|
||
|
|
<el-dropdown-item command="PAYPAL">
|
||
|
|
<el-icon><CreditCard /></el-icon>
|
||
|
|
PayPal支付
|
||
|
|
</el-dropdown-item>
|
||
|
|
</el-dropdown-menu>
|
||
|
|
</template>
|
||
|
|
</el-dropdown>
|
||
|
|
|
||
|
|
<el-button
|
||
|
|
v-if="canCancel(row)"
|
||
|
|
size="small"
|
||
|
|
type="danger"
|
||
|
|
@click="handleCancel(row)"
|
||
|
|
>
|
||
|
|
取消
|
||
|
|
</el-button>
|
||
|
|
</el-button-group>
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
</el-table>
|
||
|
|
|
||
|
|
<!-- 分页 -->
|
||
|
|
<div class="pagination-container">
|
||
|
|
<el-pagination
|
||
|
|
v-model:current-page="pagination.page"
|
||
|
|
v-model:page-size="pagination.size"
|
||
|
|
:page-sizes="[10, 20, 50, 100]"
|
||
|
|
:total="pagination.total"
|
||
|
|
layout="total, sizes, prev, pager, next, jumper"
|
||
|
|
@size-change="handleSizeChange"
|
||
|
|
@current-change="handleCurrentChange"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</el-card>
|
||
|
|
|
||
|
|
<!-- 取消订单对话框 -->
|
||
|
|
<el-dialog
|
||
|
|
v-model="cancelDialogVisible"
|
||
|
|
title="取消订单"
|
||
|
|
width="400px"
|
||
|
|
>
|
||
|
|
<el-form :model="cancelForm" label-width="80px">
|
||
|
|
<el-form-item label="取消原因">
|
||
|
|
<el-input
|
||
|
|
v-model="cancelForm.reason"
|
||
|
|
type="textarea"
|
||
|
|
:rows="3"
|
||
|
|
placeholder="请输入取消原因(可选)"
|
||
|
|
/>
|
||
|
|
</el-form-item>
|
||
|
|
</el-form>
|
||
|
|
|
||
|
|
<template #footer>
|
||
|
|
<el-button @click="cancelDialogVisible = false">取消</el-button>
|
||
|
|
<el-button type="danger" @click="confirmCancel">确认取消</el-button>
|
||
|
|
</template>
|
||
|
|
</el-dialog>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||
|
|
import { useOrderStore } from '@/stores/orders'
|
||
|
|
import { createOrderPayment } from '@/api/orders'
|
||
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
|
|
|
||
|
|
const orderStore = useOrderStore()
|
||
|
|
|
||
|
|
// 筛选条件
|
||
|
|
const filters = reactive({
|
||
|
|
status: '',
|
||
|
|
search: ''
|
||
|
|
})
|
||
|
|
|
||
|
|
// 分页信息
|
||
|
|
const pagination = reactive({
|
||
|
|
page: 1,
|
||
|
|
size: 10,
|
||
|
|
total: 0
|
||
|
|
})
|
||
|
|
|
||
|
|
// 排序
|
||
|
|
const sortBy = ref('createdAt')
|
||
|
|
const sortDir = ref('desc')
|
||
|
|
|
||
|
|
// 取消订单对话框
|
||
|
|
const cancelDialogVisible = ref(false)
|
||
|
|
const cancelForm = reactive({
|
||
|
|
reason: ''
|
||
|
|
})
|
||
|
|
const currentCancelOrder = ref(null)
|
||
|
|
|
||
|
|
// 订单状态选项
|
||
|
|
const orderStatuses = [
|
||
|
|
{ value: '', label: '全部状态' },
|
||
|
|
{ value: 'PENDING', label: '待支付' },
|
||
|
|
{ value: 'CONFIRMED', label: '已确认' },
|
||
|
|
{ value: 'PAID', label: '已支付' },
|
||
|
|
{ value: 'PROCESSING', label: '处理中' },
|
||
|
|
{ value: 'SHIPPED', label: '已发货' },
|
||
|
|
{ value: 'DELIVERED', label: '已送达' },
|
||
|
|
{ value: 'COMPLETED', label: '已完成' },
|
||
|
|
{ value: 'CANCELLED', label: '已取消' },
|
||
|
|
{ value: 'REFUNDED', label: '已退款' }
|
||
|
|
]
|
||
|
|
|
||
|
|
// 获取状态类型
|
||
|
|
const getStatusType = (status) => {
|
||
|
|
const statusMap = {
|
||
|
|
'PENDING': 'warning',
|
||
|
|
'CONFIRMED': 'info',
|
||
|
|
'PAID': 'primary',
|
||
|
|
'PROCESSING': '',
|
||
|
|
'SHIPPED': 'success',
|
||
|
|
'DELIVERED': 'success',
|
||
|
|
'COMPLETED': 'success',
|
||
|
|
'CANCELLED': 'danger',
|
||
|
|
'REFUNDED': 'info'
|
||
|
|
}
|
||
|
|
return statusMap[status] || ''
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取状态文本
|
||
|
|
const getStatusText = (status) => {
|
||
|
|
const statusMap = {
|
||
|
|
'PENDING': '待支付',
|
||
|
|
'CONFIRMED': '已确认',
|
||
|
|
'PAID': '已支付',
|
||
|
|
'PROCESSING': '处理中',
|
||
|
|
'SHIPPED': '已发货',
|
||
|
|
'DELIVERED': '已送达',
|
||
|
|
'COMPLETED': '已完成',
|
||
|
|
'CANCELLED': '已取消',
|
||
|
|
'REFUNDED': '已退款'
|
||
|
|
}
|
||
|
|
return statusMap[status] || status
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取订单类型文本
|
||
|
|
const getOrderTypeText = (orderType) => {
|
||
|
|
const typeMap = {
|
||
|
|
'PRODUCT': '商品订单',
|
||
|
|
'SERVICE': '服务订单',
|
||
|
|
'SUBSCRIPTION': '订阅订单',
|
||
|
|
'DIGITAL': '数字商品',
|
||
|
|
'PHYSICAL': '实体商品'
|
||
|
|
}
|
||
|
|
return typeMap[orderType] || orderType
|
||
|
|
}
|
||
|
|
|
||
|
|
// 格式化日期
|
||
|
|
const formatDate = (dateString) => {
|
||
|
|
const date = new Date(dateString)
|
||
|
|
return date.toLocaleDateString('zh-CN', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: '2-digit',
|
||
|
|
day: '2-digit',
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查是否可以支付
|
||
|
|
const canPay = (order) => {
|
||
|
|
return order.status === 'PENDING' || order.status === 'CONFIRMED'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查是否可以取消
|
||
|
|
const canCancel = (order) => {
|
||
|
|
return order.status === 'PENDING' || order.status === 'CONFIRMED'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取订单列表
|
||
|
|
const fetchOrders = async () => {
|
||
|
|
console.log('=== 开始获取订单列表 ===')
|
||
|
|
console.log('当前用户:', userStore.user)
|
||
|
|
console.log('认证状态:', userStore.isAuthenticated)
|
||
|
|
console.log('Token:', sessionStorage.getItem('token'))
|
||
|
|
|
||
|
|
const params = {
|
||
|
|
page: pagination.page - 1,
|
||
|
|
size: pagination.size,
|
||
|
|
sortBy: sortBy.value,
|
||
|
|
sortDir: sortDir.value
|
||
|
|
}
|
||
|
|
|
||
|
|
if (filters.status) {
|
||
|
|
params.status = filters.status
|
||
|
|
}
|
||
|
|
|
||
|
|
if (filters.search) {
|
||
|
|
params.search = filters.search
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('请求参数:', params)
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await orderStore.fetchOrders(params)
|
||
|
|
console.log('API响应:', response)
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
pagination.total = orderStore.pagination.total
|
||
|
|
console.log('订单数据:', orderStore.orders)
|
||
|
|
console.log('分页信息:', orderStore.pagination)
|
||
|
|
} else {
|
||
|
|
console.error('获取订单失败:', response.message)
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取订单异常:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 筛选变化
|
||
|
|
const handleFilterChange = () => {
|
||
|
|
pagination.page = 1
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 搜索
|
||
|
|
const handleSearch = () => {
|
||
|
|
pagination.page = 1
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重置筛选
|
||
|
|
const resetFilters = () => {
|
||
|
|
filters.status = ''
|
||
|
|
filters.search = ''
|
||
|
|
pagination.page = 1
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 排序变化
|
||
|
|
const handleSortChange = ({ prop, order }) => {
|
||
|
|
if (prop) {
|
||
|
|
sortBy.value = prop
|
||
|
|
sortDir.value = order === 'ascending' ? 'asc' : 'desc'
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 分页大小变化
|
||
|
|
const handleSizeChange = (size) => {
|
||
|
|
pagination.size = size
|
||
|
|
pagination.page = 1
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 当前页变化
|
||
|
|
const handleCurrentChange = (page) => {
|
||
|
|
pagination.page = page
|
||
|
|
fetchOrders()
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理支付
|
||
|
|
const handlePayment = async (order, paymentMethod) => {
|
||
|
|
try {
|
||
|
|
const response = await createOrderPayment(order.id, paymentMethod)
|
||
|
|
if (response.success) {
|
||
|
|
ElMessage.success('正在跳转到支付页面...')
|
||
|
|
// 这里应该跳转到支付页面
|
||
|
|
window.open(response.data.paymentUrl, '_blank')
|
||
|
|
} else {
|
||
|
|
ElMessage.error(response.message || '创建支付失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Payment error:', error)
|
||
|
|
ElMessage.error('创建支付失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理取消
|
||
|
|
const handleCancel = (order) => {
|
||
|
|
currentCancelOrder.value = order
|
||
|
|
cancelForm.reason = ''
|
||
|
|
cancelDialogVisible.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
// 确认取消
|
||
|
|
const confirmCancel = async () => {
|
||
|
|
if (!currentCancelOrder.value) return
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await orderStore.cancelOrderById(
|
||
|
|
currentCancelOrder.value.id,
|
||
|
|
cancelForm.reason
|
||
|
|
)
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
ElMessage.success('订单取消成功')
|
||
|
|
cancelDialogVisible.value = false
|
||
|
|
fetchOrders()
|
||
|
|
} else {
|
||
|
|
ElMessage.error(response.message || '取消订单失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Cancel order error:', error)
|
||
|
|
ElMessage.error('取消订单失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
fetchOrders()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.orders {
|
||
|
|
max-width: 1200px;
|
||
|
|
margin: 0 auto;
|
||
|
|
min-height: 100vh;
|
||
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
|
|
position: relative;
|
||
|
|
overflow-x: hidden;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 页面特殊效果 */
|
||
|
|
.orders::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
background:
|
||
|
|
radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||
|
|
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||
|
|
animation: ordersPulse 5s ease-in-out infinite alternate;
|
||
|
|
pointer-events: none;
|
||
|
|
z-index: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes ordersPulse {
|
||
|
|
0% { opacity: 0.3; transform: scale(1); }
|
||
|
|
100% { opacity: 0.6; transform: scale(1.02); }
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 内容层级 */
|
||
|
|
.orders > * {
|
||
|
|
position: relative;
|
||
|
|
z-index: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-title h2 {
|
||
|
|
margin: 0;
|
||
|
|
color: #303133;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-card {
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.orders-card {
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.order-link {
|
||
|
|
color: #409EFF;
|
||
|
|
text-decoration: none;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.order-link:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
|
||
|
|
.amount {
|
||
|
|
font-weight: 600;
|
||
|
|
color: #E6A23C;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pagination-container {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
margin-top: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 确保表格内下拉菜单不被裁剪/遮挡 */
|
||
|
|
:deep(.table-dropdown) {
|
||
|
|
z-index: 3000 !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.page-header {
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 16px;
|
||
|
|
align-items: stretch;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-actions {
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
|