Files
AIGC/demo/frontend/src/views/AdminOrders.vue

802 lines
22 KiB
Vue
Raw Normal View History

2025-10-21 16:50:33 +08:00
<template>
<div class="admin-orders">
<!-- 页面标题 -->
<div class="page-header">
<h2>
<el-icon><Management /></el-icon>
订单管理 - 管理员
</h2>
</div>
<!-- 统计面板 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('all')">
<div class="stat-content">
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
<div class="stat-label">总订单数</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><List /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PENDING')">
<div class="stat-content">
<div class="stat-number">{{ stats.pendingOrders || 0 }}</div>
<div class="stat-label">待支付</div>
</div>
<el-icon class="stat-icon" color="#E6A23C"><Clock /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('COMPLETED')">
<div class="stat-content">
<div class="stat-number">{{ stats.completedOrders || 0 }}</div>
<div class="stat-label">已完成</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><Check /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('today')">
<div class="stat-content">
<div class="stat-number">{{ stats.todayOrders || 0 }}</div>
<div class="stat-label">今日订单</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Calendar /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 第二行统计卡片 -->
<el-row :gutter="20" class="stats-row" style="margin-top: 20px;">
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PAID')">
<div class="stat-content">
<div class="stat-number">{{ stats.paidOrders || 0 }}</div>
<div class="stat-label">已支付</div>
</div>
<el-icon class="stat-icon" color="#409EFF"><CreditCard /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('PROCESSING')">
<div class="stat-content">
<div class="stat-number">{{ stats.processingOrders || 0 }}</div>
<div class="stat-label">处理中</div>
</div>
<el-icon class="stat-icon" color="#909399"><Loading /></el-icon>
</el-card>
</el-col>
<!-- 只有存在实体商品时才显示发货统计 -->
<el-col v-if="hasPhysicalOrders" :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('SHIPPED')">
<div class="stat-content">
<div class="stat-number">{{ stats.shippedOrders || 0 }}</div>
<div class="stat-label">已发货</div>
</div>
<el-icon class="stat-icon" color="#67C23A"><Truck /></el-icon>
</el-card>
</el-col>
<!-- 如果没有实体商品显示已退款统计 -->
<el-col v-else :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('REFUNDED')">
<div class="stat-content">
<div class="stat-number">{{ stats.refundedOrders || 0 }}</div>
<div class="stat-label">已退款</div>
</div>
<el-icon class="stat-icon" color="#909399"><Money /></el-icon>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card class="stat-card clickable" @click="handleStatClick('CANCELLED')">
<div class="stat-content">
<div class="stat-number">{{ stats.cancelledOrders || 0 }}</div>
<div class="stat-label">已取消</div>
</div>
<el-icon class="stat-icon" color="#F56C6C"><Close /></el-icon>
</el-card>
</el-col>
</el-row>
<!-- 筛选和搜索 -->
<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 label="全部状态" value="" />
<el-option label="待支付" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="已支付" value="PAID" />
<el-option label="处理中" value="PROCESSING" />
<el-option label="已发货" value="SHIPPED" />
<el-option label="已送达" value="DELIVERED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
<el-option label="已退款" value="REFUNDED" />
</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="orders"
v-loading="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="user.username" label="用户" width="120">
<template #default="{ row }">
<div class="user-info">
<el-avatar :size="24">{{ row.user.username.charAt(0).toUpperCase() }}</el-avatar>
<span class="username">{{ row.user.username }}</span>
</div>
</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 trigger="click" :teleported="true" popper-class="table-dropdown" @command="(command) => handleAdminAction(row, command)">
<el-button size="small" type="primary">
管理<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canShip(row)" command="ship">
<el-icon><Truck /></el-icon>
发货
</el-dropdown-item>
<el-dropdown-item v-if="canComplete(row)" command="complete">
<el-icon><Check /></el-icon>
完成订单
</el-dropdown-item>
<el-dropdown-item v-if="canCancel(row)" command="cancel">
<el-icon><Close /></el-icon>
取消订单
</el-dropdown-item>
<el-dropdown-item divided command="updateStatus">
<el-icon><Edit /></el-icon>
更新状态
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</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="shipDialogVisible"
title="订单发货"
width="400px"
>
<el-form :model="shipForm" label-width="80px">
<el-form-item label="物流单号">
<el-input
v-model="shipForm.trackingNumber"
placeholder="请输入物流单号(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="shipDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmShip">确认发货</el-button>
</template>
</el-dialog>
<!-- 更新状态对话框 -->
<el-dialog
v-model="statusDialogVisible"
title="更新订单状态"
width="400px"
>
<el-form :model="statusForm" label-width="80px">
<el-form-item label="新状态">
<el-select v-model="statusForm.status" placeholder="选择新状态">
<el-option label="待支付" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="已支付" value="PAID" />
<el-option label="处理中" value="PROCESSING" />
<!-- 实体商品才显示发货相关状态 -->
<template v-if="isPhysicalOrder(currentStatusOrder)">
<el-option label="已发货" value="SHIPPED" />
<el-option label="已送达" value="DELIVERED" />
</template>
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
<el-option label="已退款" value="REFUNDED" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="statusForm.notes"
type="textarea"
:rows="3"
placeholder="请输入备注(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUpdateStatus">确认更新</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
User as ArrowDown,
User as Truck,
Check,
Close,
User as Refresh,
User as Search,
User as Filter,
User as Download,
User as Plus,
User as Edit,
User as Delete,
User as View,
Money,
CreditCard,
Wallet
} from '@element-plus/icons-vue'
2025-10-21 16:50:33 +08:00
import { getOrders, getOrderStats } from '@/api/orders'
const loading = ref(false)
const orders = ref([])
// 统计数据
const stats = ref({
totalOrders: 0,
pendingOrders: 0,
completedOrders: 0,
todayOrders: 0
})
// 筛选条件
const filters = reactive({
status: '',
search: ''
})
// 分页信息
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
// 排序
const sortBy = ref('createdAt')
const sortDir = ref('desc')
// 发货对话框
const shipDialogVisible = ref(false)
const shipForm = reactive({
trackingNumber: ''
})
const currentShipOrder = ref(null)
// 状态更新对话框
const statusDialogVisible = ref(false)
const statusForm = reactive({
status: '',
notes: ''
})
const currentStatusOrder = ref(null)
// 获取状态类型
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 canShip = (order) => {
// 只有实体商品才需要发货
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
return physicalOrderTypes.includes(order.orderType) &&
(order.status === 'PAID' || order.status === 'CONFIRMED')
}
// 检查是否可以完成
const canComplete = (order) => {
// 实体商品需要先发货才能完成
if (['PRODUCT', 'PHYSICAL'].includes(order.orderType)) {
return order.status === 'SHIPPED'
}
// 虚拟商品支付后可以直接完成
return ['PAID', 'CONFIRMED'].includes(order.status)
}
// 检查是否可以取消
const canCancel = (order) => {
return order.status === 'PENDING' || order.status === 'CONFIRMED'
}
// 检查是否为实体商品
const isPhysicalOrder = (order) => {
if (!order) return false
const physicalOrderTypes = ['PRODUCT', 'PHYSICAL']
return physicalOrderTypes.includes(order.orderType)
}
// 检查是否有实体商品订单
const hasPhysicalOrders = computed(() => {
return orders.value.some(order => isPhysicalOrder(order))
})
// 处理统计卡片点击
const handleStatClick = (type) => {
if (type === 'all') {
// 显示所有订单
filters.status = ''
filters.search = ''
} else if (type === 'today') {
// 显示今日订单(这里可以添加日期筛选逻辑)
filters.status = ''
filters.search = ''
// 可以添加日期筛选逻辑
} else {
// 按状态筛选
filters.status = type
filters.search = ''
}
// 重置分页并重新加载数据
pagination.page = 1
fetchOrders()
}
// 获取订单列表
const fetchOrders = async () => {
try {
loading.value = true
// 调用真实API获取订单数据
const response = await getOrders({
page: pagination.page - 1,
size: pagination.size,
status: filters.status,
search: filters.search
})
if (response.success) {
orders.value = response.data.content || []
pagination.total = response.data.totalElements || 0
} else {
ElMessage.error('获取订单列表失败')
}
// 获取统计数据
const statsResponse = await getOrderStats()
if (statsResponse.success) {
stats.value = statsResponse.data
}
} catch (error) {
console.error('Fetch orders error:', error)
ElMessage.error('获取订单列表失败')
} finally {
loading.value = false
}
}
// 筛选变化
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 handleAdminAction = (order, command) => {
switch (command) {
case 'ship':
currentShipOrder.value = order
shipForm.trackingNumber = ''
shipDialogVisible.value = true
break
case 'complete':
handleCompleteOrder(order)
break
case 'cancel':
handleCancelOrder(order)
break
case 'updateStatus':
currentStatusOrder.value = order
statusForm.status = order.status
statusForm.notes = ''
statusDialogVisible.value = true
break
}
}
// 完成订单
const handleCompleteOrder = async (order) => {
try {
await ElMessageBox.confirm('确定要完成此订单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.success('订单完成成功')
fetchOrders()
} catch (error) {
// 用户取消
}
}
// 更新统计数据
const updateStats = () => {
const today = new Date().toISOString().split('T')[0]
stats.value = {
totalOrders: orders.value.length,
pendingOrders: orders.value.filter(order => order.status === 'PENDING').length,
paidOrders: orders.value.filter(order => order.status === 'PAID').length,
processingOrders: orders.value.filter(order => order.status === 'PROCESSING').length,
shippedOrders: orders.value.filter(order => order.status === 'SHIPPED').length,
completedOrders: orders.value.filter(order => order.status === 'COMPLETED').length,
cancelledOrders: orders.value.filter(order => order.status === 'CANCELLED').length,
refundedOrders: orders.value.filter(order => order.status === 'REFUNDED').length,
todayOrders: orders.value.filter(order => order.createdAt.startsWith(today)).length
}
}
// 取消订单
const handleCancelOrder = async (order) => {
try {
await ElMessageBox.confirm('确定要取消此订单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 更新订单状态为已取消
const orderIndex = orders.value.findIndex(o => o.id === order.id)
if (orderIndex !== -1) {
orders.value[orderIndex].status = 'CANCELLED'
}
// 更新统计数据
updateStats()
ElMessage.success('订单取消成功')
} catch (error) {
// 用户取消
}
}
// 确认发货
const confirmShip = async () => {
if (!currentShipOrder.value) return
try {
ElMessage.success('发货成功')
shipDialogVisible.value = false
fetchOrders()
} catch (error) {
ElMessage.error('发货失败')
}
}
// 确认更新状态
const confirmUpdateStatus = async () => {
if (!currentStatusOrder.value) return
try {
// 更新订单状态
const orderIndex = orders.value.findIndex(o => o.id === currentStatusOrder.value.id)
if (orderIndex !== -1) {
orders.value[orderIndex].status = statusForm.status
}
// 更新统计数据
updateStats()
ElMessage.success('状态更新成功')
statusDialogVisible.value = false
} catch (error) {
ElMessage.error('状态更新失败')
}
}
onMounted(() => {
fetchOrders()
})
</script>
<style scoped>
.admin-orders {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.stats-row {
margin-bottom: 20px;
}
.stat-card {
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.stat-card.clickable {
cursor: pointer;
}
.stat-card.clickable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #303133;
margin-bottom: 4px;
}
.stat-label {
color: #909399;
font-size: 14px;
}
.stat-icon {
font-size: 2rem;
opacity: 0.8;
}
.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;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.username {
font-size: 14px;
}
.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 {
text-align: center;
}
.stat-card {
margin-bottom: 16px;
}
}
</style>