deotalandAi/apps/FrontendDesigner/src/views/admin/AdminOrders copy.vue

1001 lines
27 KiB
Vue

<template>
<div class="admin-orders">
<!-- 统计卡片 -->
<div class="order-stats">
<div class="stat-card">
<div class="stat-icon total">
<el-icon><ShoppingCart /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ orderStats.total }}</div>
<div class="stat-label">{{ t('admin.orders.stats.total') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon pending">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ orderStats.pending }}</div>
<div class="stat-label">{{ t('admin.orders.stats.pending') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon completed">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ orderStats.completed }}</div>
<div class="stat-label">{{ t('admin.orders.stats.completed') }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon revenue">
<el-icon><Money /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">¥{{ orderStats.revenue.toLocaleString() }}</div>
<div class="stat-label">{{ t('admin.orders.stats.revenue') }}</div>
</div>
</div>
</div>
<!-- 筛选和搜索 -->
<div class="orders-filters">
<div class="filter-group">
<el-select
v-model="selectedStatus"
:placeholder="t('admin.orders.status')"
clearable
>
<el-option
:label="t('admin.orders.statusOptions.pendingConfirmation')"
value="pendingConfirmation"
/>
<el-option
:label="t('admin.orders.statusOptions.confirmed')"
value="confirmed"
/>
<el-option
:label="t('admin.orders.statusOptions.rejected')"
value="rejected"
/>
<el-option
:label="t('admin.orders.statusOptions.processing')"
value="processing"
/>
<el-option
:label="t('admin.orders.statusOptions.readyToShip')"
value="readyToShip"
/>
<el-option
:label="t('admin.orders.statusOptions.shipped')"
value="shipped"
/>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
:start-placeholder="t('admin.orders.dateRange')"
:end-placeholder="t('admin.orders.dateRange')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</div>
<div class="search-group">
<el-input
v-model="searchQuery"
:placeholder="t('admin.orders.search')"
prefix-icon="Search"
clearable
/>
</div>
<div class="filter-actions">
<el-button
type="primary"
@click="handleExportOrders"
>
<el-icon><Download /></el-icon>
{{ t('admin.orders.export') }}
</el-button>
<el-button
@click="refresh"
>
<el-icon><Refresh /></el-icon>
{{ t('admin.common.refresh') }}
</el-button>
</div>
</div>
<!-- 订单列表 -->
<div class="orders-list">
<!-- stripe -->
<el-table
:data="filteredOrdersList"
style="width: 100%"
v-loading="loading"
@row-click="handleOrderDetail"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="orderNumber" :label="t('admin.orders.orderNumber')" width="150" />
<el-table-column prop="customerName" :label="t('admin.orders.customer')" width="120" />
<el-table-column prop="totalAmount" :label="t('admin.orders.total')" width="120">
<template #default="{ row }">
¥{{ row.totalAmount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="status" :label="t('admin.orders.status')" width="120">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ t(`admin.orders.statusOptions.${row.status}`) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="paymentMethod" :label="t('admin.orders.payment')" width="100">
<template #default="{ row }">
{{ t(`admin.orders.paymentOptions.${row.paymentMethod}`) }}
</template>
</el-table-column>
<el-table-column prop="orderDate" :label="t('admin.orders.date')" width="180">
<template #default="{ row }">
{{ formatDate(row.orderDate) }}
</template>
</el-table-column>
<el-table-column :label="t('admin.orders.actions')" width="200" fixed="right">
<template #default="{ row }">
<div class="actions-container">
<!-- 待确认状态:显示"去确认"按钮 -->
<el-button
v-if="row.status === 'pendingConfirmation'"
size="small"
type="primary"
@click.stop="handleConfirmOrder(row)"
>
{{ t('admin.orders.confirm') }}
</el-button>
<!-- 已拒绝状态:显示退款提示 -->
<el-tag v-if="row.status === 'rejected'" type="danger" size="small">
{{ t('admin.orders.refundNotice') }}
</el-tag>
<!-- 待处理状态:显示"去处理"按钮 -->
<el-button
v-if="row.status === 'processing'"
size="small"
type="primary"
@click.stop="handleProcessOrder(row)"
>
{{ t('admin.orders.process') }}
</el-button>
<!-- 待发货状态:显示"发货"按钮 -->
<el-button
v-if="row.status === 'readyToShip'"
size="small"
type="primary"
@click.stop="handleShipOrder(row)"
>
{{ t('admin.orders.ship') }}
</el-button>
<!-- 已发货状态:显示"查看物流"按钮 -->
<el-button
v-if="row.status === 'shipped'"
size="small"
type="success"
@click.stop="handleViewLogistics(row)"
>
{{ t('admin.orders.viewLogistics') }}
</el-button>
<!-- 所有状态都显示"查看"按钮 -->
<el-button size="small" @click.stop="handleViewOrder(row)">
{{ t('admin.orders.view') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalOrders"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 订单详情对话框 -->
<el-dialog
:title="t('admin.orders.detail')"
v-model="detailDialogVisible"
width="800px"
>
<div v-if="selectedOrder" class="order-detail">
<div class="detail-section">
<h4>{{ t('admin.orders.basicInfo') }}</h4>
<el-descriptions :column="2" border>
<el-descriptions-item :label="t('admin.orders.orderNumber')">
{{ selectedOrder.orderNumber }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.customer')">
{{ selectedOrder.customerName }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.total')">
¥{{ selectedOrder.totalAmount.toFixed(2) }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.status')">
<el-tag :type="getStatusTagType(selectedOrder.status)">
{{ t(`admin.orders.statusOptions.${selectedOrder.status}`) }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="detail-section">
<h4>{{ t('admin.orders.items') }}</h4>
<!-- stripe -->
<el-table :data="selectedOrder.items" >
<el-table-column prop="name" :label="t('admin.orders.itemName')" />
<el-table-column prop="quantity" :label="t('admin.orders.quantity')" width="100" />
<el-table-column prop="price" :label="t('admin.orders.price')" width="120">
<template #default="{ row }">
¥{{ row.price.toFixed(2) }}
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
<!-- 更新状态对话框 -->
<el-dialog
:title="t('admin.orders.updateStatus')"
v-model="statusDialogVisible"
width="400px"
>
<el-form v-if="selectedOrderForStatus" :model="statusForm" label-width="100px">
<el-form-item :label="t('admin.orders.currentStatus')">
<el-tag :type="getStatusTagType(selectedOrderForStatus.status)">
{{ t(`admin.orders.statusOptions.${selectedOrderForStatus.status}`) }}
</el-tag>
</el-form-item>
<el-form-item :label="t('admin.orders.newStatus')" prop="status">
<el-select v-model="statusForm.status" :placeholder="t('admin.orders.selectStatus')">
<el-option
v-for="status in availableStatuses"
:key="status.value"
:label="status.label"
:value="status.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleStatusUpdate">{{ t('common.confirm') }}</el-button>
</template>
</el-dialog>
<!-- 发货对话框 -->
<el-dialog
v-model="shippingDialogVisible"
:title="t('admin.orders.ship')"
width="40%"
>
<div v-if="selectedOrder">
<el-form :model="shippingForm" label-width="120px">
<el-form-item :label="t('admin.orders.trackingNumber')" required>
<el-input v-model="shippingForm.trackingNumber" placeholder="请输入物流单号"></el-input>
</el-form-item>
<el-form-item :label="t('admin.orders.carrier')" required>
<el-select v-model="shippingForm.carrier" placeholder="请选择物流公司">
<el-option label="顺丰速运" value="sf"></el-option>
<el-option label="圆通速递" value="yto"></el-option>
<el-option label="中通快递" value="zto"></el-option>
<el-option label="申通快递" value="sto"></el-option>
<el-option label="韵达速递" value="yd"></el-option>
<el-option label="邮政EMS" value="ems"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="t('admin.orders.shippingNote')">
<el-input
v-model="shippingForm.note"
type="textarea"
rows="3"
placeholder="发货备注(可选)">
</el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="shippingDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="confirmShipOrder">{{ t('common.confirm') }}</el-button>
</template>
</el-dialog>
<!-- 物流对话框 -->
<el-dialog
v-model="logisticsDialogVisible"
:title="t('admin.orders.viewLogistics')"
width="50%"
>
<div v-if="selectedOrder">
<el-descriptions :column="2" border>
<el-descriptions-item :label="t('admin.orders.trackingNumber')">
{{ selectedOrder.trackingNumber || 'SF1234567890' }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.carrier')">
{{ selectedOrder.carrier || '顺丰速运' }}
</el-descriptions-item>
</el-descriptions>
<el-divider>{{ t('admin.orders.logisticsTimeline') }}</el-divider>
<el-timeline>
<el-timeline-item
v-for="(activity, index) in logisticsActivities"
:key="index"
:timestamp="activity.timestamp"
:type="activity.type"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
<template #footer>
<el-button @click="logisticsDialogVisible = false">{{ t('common.close') }}</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import {
Download,
ShoppingCart,
Clock,
Check,
Money,
Search,
Refresh
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
name: 'AdminOrders',
components: {
Download,
ShoppingCart,
Clock,
Check,
Money,
Search,
Refresh
},
setup() {
const { t } = useI18n()
const router = useRouter()
// 响应式数据
const loading = ref(false)
const searchQuery = ref('')
const selectedStatus = ref('')
const dateRange = ref([])
const currentPage = ref(1)
const pageSize = ref(20)
const totalOrders = ref(0)
const detailDialogVisible = ref(false)
const statusDialogVisible = ref(false)
const shippingDialogVisible = ref(false)
const logisticsDialogVisible = ref(false)
const selectedOrder = ref(null)
const selectedOrderForStatus = ref(null)
const selectedOrders = ref([]) // 多选选中的订单
const statusForm = reactive({
status: ''
})
// 发货表单
const shippingForm = reactive({
trackingNumber: '',
carrier: '',
note: ''
})
// 物流活动时间线
const logisticsActivities = ref([
{
content: '您的订单已发货',
timestamp: '2023-12-01 10:00:00',
type: 'primary'
},
{
content: '您的订单已到达【北京转运中心】',
timestamp: '2023-12-01 14:30:00',
type: 'success'
},
{
content: '您的订单正在派送中',
timestamp: '2023-12-02 09:00:00',
type: 'warning'
},
{
content: '您的订单已签收',
timestamp: '2023-12-02 16:30:00',
type: 'success'
}
])
// 模拟订单数据
const ordersList = ref([
{
id: 1,
orderNumber: 'ORD-2024-001',
customerName: '张三',
totalAmount: 1299.00,
status: 'shipped',
paymentMethod: 'alipay',
orderDate: '2024-01-15 10:30:00',
items: [
{ name: 'AI智能音箱', quantity: 1, price: 799.00 },
{ name: '智能插座', quantity: 2, price: 250.00 }
]
},
{
id: 2,
orderNumber: 'ORD-2024-002',
customerName: '李四',
totalAmount: 2599.00,
status: 'readyToShip',
paymentMethod: 'wechat',
orderDate: '2024-01-14 16:20:00',
items: [
{ name: '智能门锁', quantity: 1, price: 1999.00 },
{ name: '智能摄像头', quantity: 1, price: 600.00 }
]
},
{
id: 3,
orderNumber: 'ORD-2024-003',
customerName: '王五',
totalAmount: 899.00,
status: 'processing',
paymentMethod: 'credit',
orderDate: '2024-01-13 09:15:00',
items: [
{ name: '智能灯泡', quantity: 3, price: 299.00 }
]
},
{
id: 4,
orderNumber: 'ORD-2024-004',
customerName: '赵六',
totalAmount: 1599.00,
status: 'pendingConfirmation',
paymentMethod: 'alipay',
orderDate: '2024-01-12 14:45:00',
items: [
{ name: '智能扫地机器人', quantity: 1, price: 1599.00 }
]
},
{
id: 5,
orderNumber: 'ORD-2024-005',
customerName: '孙七',
totalAmount: 799.00,
status: 'processing',
paymentMethod: 'wechat',
orderDate: '2024-01-11 11:30:00',
items: [
{ name: '智能体重秤', quantity: 1, price: 799.00 }
]
},
{
id: 6,
orderNumber: 'ORD-2024-006',
customerName: '周八',
totalAmount: 1299.00,
status: 'rejected',
paymentMethod: 'credit',
orderDate: '2024-01-10 15:20:00',
items: [
{ name: '智能手环', quantity: 1, price: 1299.00 }
]
}
])
// 统计数据
const orderStats = ref({
total: 156,
pending: 23,
completed: 128,
revenue: 156789
})
// 计算属性
const filteredOrdersList = computed(() => {
let result = ordersList.value
if (searchQuery.value) {
result = result.filter(item =>
item.orderNumber.includes(searchQuery.value) ||
item.customerName.includes(searchQuery.value)
)
}
if (selectedStatus.value) {
result = result.filter(item => item.status === selectedStatus.value)
}
if (dateRange.value && dateRange.value.length === 2) {
result = result.filter(item => {
const orderDate = new Date(item.orderDate)
const startDate = new Date(dateRange.value[0])
const endDate = new Date(dateRange.value[1])
return orderDate >= startDate && orderDate <= endDate
})
}
return result
})
const availableStatuses = computed(() => {
const allStatuses = [
{ value: 'pendingConfirmation', label: t('admin.orders.statusOptions.pendingConfirmation') },
{ value: 'rejected', label: t('admin.orders.statusOptions.rejected') },
{ value: 'processing', label: t('admin.orders.statusOptions.processing') },
{ value: 'readyToShip', label: t('admin.orders.statusOptions.readyToShip') },
{ value: 'shipped', label: t('admin.orders.statusOptions.shipped') }
]
return allStatuses
})
// 方法
const getStatusTagType = (status) => {
const statusMap = {
pendingConfirmation: 'info', // 待确认 - 蓝色
rejected: 'danger', // 已拒绝 - 红色
processing: 'warning', // 待处理 - 橙色
readyToShip: 'primary', // 待发货 - 主色调
shipped: 'success' // 已发货 - 绿色
}
return statusMap[status] || 'info'
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
const handleExportOrders = () => {
ElMessage.success('Orders exported successfully')
}
const refresh = () => {
console.log('Refresh data')
ElMessage.success('Data refreshed successfully')
}
const handleOrderDetail = (row) => {
selectedOrder.value = row
detailDialogVisible.value = true
}
const handleViewOrder = (row) => {
selectedOrder.value = row
detailDialogVisible.value = true
}
const handleUpdateStatus = (row) => {
selectedOrderForStatus.value = row
statusForm.value.status = row.status
statusDialogVisible.value = true
}
const handleStatusUpdate = async () => {
try {
await ElMessageBox.confirm(
'确定要更新订单状态吗?',
'确认更新',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
ElMessage.success('订单状态更新成功')
statusDialogVisible.value = false
} catch {
// 用户取消更新
}
}
// 确认订单
const handleConfirmOrder = (row) => {
// 跳转到订单审核页面
router.push('/admin/content-review')
}
// 处理订单
const handleProcessOrder = (row) => {
// 跳转到拆解订单页面
router.push('/admin/disassembly-orders')
}
// 发货订单
const handleShipOrder = (row) => {
selectedOrder.value = row
shippingDialogVisible.value = true
}
// 查看物流
const handleViewLogistics = (row) => {
selectedOrder.value = row
logisticsDialogVisible.value = true
}
// 确认发货
const confirmShipOrder = () => {
if (!shippingForm.trackingNumber || !shippingForm.carrier) {
ElMessage.warning('请填写必要的发货信息')
return
}
// 更新订单状态
const orderIndex = orders.value.findIndex(order => order.id === selectedOrder.value.id)
if (orderIndex !== -1) {
orders.value[orderIndex].status = 'shipped'
orders.value[orderIndex].trackingNumber = shippingForm.trackingNumber
orders.value[orderIndex].carrier = shippingForm.carrier
}
ElMessage.success('发货成功')
shippingDialogVisible.value = false
// 重置表单
shippingForm.trackingNumber = ''
shippingForm.carrier = ''
shippingForm.note = ''
}
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
}
const handleCurrentChange = (val) => {
currentPage.value = val
}
// 多选处理函数
const handleSelectionChange = (selection) => {
selectedOrders.value = selection
}
// 生命周期
onMounted(() => {
totalOrders.value = ordersList.value.length
})
return {
t,
loading,
searchQuery,
selectedStatus,
dateRange,
currentPage,
pageSize,
totalOrders,
detailDialogVisible,
statusDialogVisible,
shippingDialogVisible,
logisticsDialogVisible,
selectedOrder,
selectedOrderForStatus,
selectedOrders,
statusForm,
shippingForm,
logisticsActivities,
ordersList,
orderStats,
filteredOrdersList,
availableStatuses,
getStatusTagType,
formatDate,
handleExportOrders,
refresh,
handleOrderDetail,
handleViewOrder,
handleUpdateStatus,
handleStatusUpdate,
handleConfirmOrder,
handleProcessOrder,
handleShipOrder,
handleViewLogistics,
confirmShipOrder,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
}
}
}
</script>
<style scoped>
.admin-orders {
padding: 20px;
background-color: #f8fafc;
min-height: calc(100vh - 60px);
}
/* 统计卡片样式 */
.order-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 16px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.stat-icon.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-icon.pending {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-icon.completed {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-icon.revenue {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-number {
font-size: 32px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.stat-label {
color: #6b7280;
font-size: 14px;
margin-top: 4px;
}
/* 筛选器样式 */
.orders-filters {
padding: 20px;
border-radius: 12px;
margin-bottom: 24px;
display: flex;
gap: 12px;
align-items: flex-end;
justify-content: flex-end;
overflow-x: auto;
white-space: nowrap;
}
.filter-group {
display: flex;
gap: 8px;
align-items: flex-end;
flex-shrink: 0;
}
.search-group {
flex: 1;
min-width: 200px;
max-width: 300px;
flex-shrink: 0;
}
.filter-actions {
display: flex;
align-items: flex-end;
flex-shrink: 0;
gap: 8px;
}
/* 订单列表样式 */
.orders-list {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
margin-bottom: 24px;
}
/* 表格样式优化 */
.orders-list :deep(.el-table) {
width: 100% !important;
table-layout: auto;
}
.orders-list :deep(.el-table__header) {
width: 100% !important;
}
.orders-list :deep(.el-table__body) {
width: 100% !important;
}
.orders-list :deep(.el-table__cell) {
padding: 8px 12px;
white-space: nowrap;
}
.orders-list :deep(.el-table__header-wrapper) {
background-color: #f8fafc;
}
.orders-list :deep(.el-table__header th) {
background-color: #f8fafc;
color: #374151;
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
}
/* 操作按钮容器 */
.actions-container {
display: flex;
gap: 6px;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
overflow: hidden;
}
.actions-container .el-button {
flex-shrink: 0;
white-space: nowrap;
}
/* 分页样式 */
.pagination {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
/* 订单详情样式 */
.order-detail {
max-height: 60vh;
overflow-y: auto;
}
.detail-section {
margin-bottom: 24px;
}
.detail-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.order-stats {
grid-template-columns: repeat(2, 1fr);
}
.orders-filters {
flex-wrap: wrap;
}
}
@media (max-width: 768px) {
.admin-orders {
padding: 16px;
}
.order-stats {
grid-template-columns: 1fr;
}
.orders-filters {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.filter-group {
flex-direction: column;
align-items: stretch;
}
.search-group {
min-width: auto;
max-width: none;
}
.filter-actions {
justify-content: center;
}
/* 移动端表格优化 */
.orders-list {
overflow-x: auto;
}
.orders-list :deep(.el-table) {
min-width: 800px;
}
.actions-container {
flex-direction: column;
gap: 6px;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.order-stats {
flex-wrap: wrap;
}
.stat-card {
min-width: calc(50% - 8px);
}
.orders-filters {
flex-wrap: wrap;
}
}
</style>