Initial commit: AIGC项目完整代码

This commit is contained in:
AIGC Developer
2025-10-21 16:50:33 +08:00
commit 47c8e02ab0
137 changed files with 30676 additions and 0 deletions

View File

@@ -0,0 +1,784 @@
<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 { 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>