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

1235 lines
31 KiB
Vue
Raw Normal View History

2025-10-21 16:50:33 +08:00
<template>
<div class="admin-orders">
<!-- 左侧导航栏 -->
<aside class="sidebar">
<div class="logo">
<img src="/images/backgrounds/logo-admin.svg" alt="Logo" />
2025-11-13 17:01:39 +08:00
</div>
<nav class="nav-menu">
<div v-if="isAdminMode" class="nav-item" @click="goToDashboard">
<el-icon><Grid /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.dashboard') }}</span>
2025-10-21 16:50:33 +08:00
</div>
<div v-if="isAdminMode" class="nav-item" @click="goToMembers">
<el-icon><User /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.members') }}</span>
2025-10-21 16:50:33 +08:00
</div>
<div class="nav-item active">
<el-icon><ShoppingCart /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.orders') }}</span>
2025-10-21 16:50:33 +08:00
</div>
<div v-if="isAdminMode" class="nav-item" @click="goToAPI">
<el-icon><Document /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.apiManagement') }}</span>
2025-10-21 16:50:33 +08:00
</div>
<div v-if="isAdminMode" class="nav-item" @click="goToTasks">
<el-icon><Document /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.tasks') }}</span>
</div>
<div v-if="isAdminMode" class="nav-item" @click="goToSettings">
<el-icon><Setting /></el-icon>
2025-11-13 17:01:39 +08:00
<span>{{ $t('nav.systemSettings') }}</span>
2025-10-21 16:50:33 +08:00
</div>
<!-- 普通用户模式的导航 -->
<div v-if="!isAdminMode" class="nav-item" @click="goToProfile">
<el-icon><User /></el-icon>
<span>{{ $t('nav.profile') || '个人主页' }}</span>
</div>
<div v-if="!isAdminMode" class="nav-item" @click="goToWorks">
<el-icon><Document /></el-icon>
<span>{{ $t('nav.myWorks') || '我的作品' }}</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="online-users">
{{ $t('nav.todayVisitors') }}: <span class="highlight">{{ onlineUsers }}</span>
</div>
<div class="system-uptime">
2025-11-13 17:01:39 +08:00
{{ $t('nav.systemUptime') }}: <span class="highlight">{{ systemUptime }}</span>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部搜索栏 -->
<header class="top-header">
<div class="search-bar">
<el-icon class="search-icon"><Search /></el-icon>
2025-11-13 17:01:39 +08:00
<input type="text" :placeholder="$t('common.searchPlaceholder')" class="search-input" v-model="searchText" />
2025-10-21 16:50:33 +08:00
</div>
<div class="header-actions">
2025-11-13 17:01:39 +08:00
<LanguageSwitcher />
<el-dropdown v-if="isAdminMode" @command="handleUserCommand">
<div class="user-avatar">
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
<el-icon class="arrow-down"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="exitAdmin">
{{ $t('admin.exitAdmin') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else class="user-avatar" @click="goToProfile">
2025-11-13 17:01:39 +08:00
<img src="/images/backgrounds/avatar-default.svg" alt="用户头像" />
2025-10-21 16:50:33 +08:00
</div>
</div>
</header>
<!-- 订单列表内容 -->
<section class="order-content">
<div class="content-header">
2025-11-13 17:01:39 +08:00
<h2>{{ $t('orders.title') }}</h2>
<div class="selection-info" v-if="selectedOrders.length > 0">
2025-11-13 17:01:39 +08:00
{{ $t('orders.selected', { count: selectedOrders.length }) }}
2025-10-21 16:50:33 +08:00
</div>
</div>
<div class="table-toolbar">
<div class="toolbar-left">
2025-11-13 17:01:39 +08:00
<el-select v-model="filters.status" :placeholder="$t('orders.allStatus')" size="small" @change="handleFilterChange">
<el-option :label="$t('orders.allStatus')" value="" />
<el-option :label="$t('orders.pending')" value="PENDING" />
<el-option :label="$t('orders.paid')" value="PAID" />
<el-option :label="$t('orders.completed')" value="COMPLETED" />
<el-option :label="$t('orders.cancelled')" value="CANCELLED" />
<el-option :label="$t('orders.refunded')" value="REFUNDED" />
2025-10-21 16:50:33 +08:00
</el-select>
<el-select v-model="filters.paymentMethod" :placeholder="$t('orders.allPaymentMethods') || '全部支付方式'" size="small" @change="handleFilterChange">
<el-option :label="$t('orders.allPaymentMethods') || '全部支付方式'" value="" />
<el-option :label="$t('orders.alipay') || '支付宝'" value="ALIPAY" />
<el-option :label="$t('orders.paypal') || 'PayPal'" value="PAYPAL" />
</el-select>
2025-10-21 16:50:33 +08:00
</div>
<div class="toolbar-right">
<el-button type="danger" size="small" @click="deleteSelected" :disabled="selectedOrders.length === 0">
<el-icon><Delete /></el-icon>
2025-11-13 17:01:39 +08:00
{{ $t('common.delete') }}
2025-10-21 16:50:33 +08:00
</el-button>
</div>
</div>
<div class="table-container">
<table class="order-table">
<thead>
<tr>
<th class="checkbox-col">
<input type="checkbox" @change="toggleAllSelection" :checked="isAllSelected" />
</th>
2025-11-13 17:01:39 +08:00
<th>{{ $t('orders.orderNumber') }}</th>
<th>{{ $t('orders.username') }}</th>
<th>{{ $t('orders.amount') }}</th>
<th>{{ $t('orders.paymentMethod') }}</th>
<th>{{ $t('orders.status') }}</th>
<th>{{ $t('orders.createTime') }}</th>
<th>{{ $t('orders.operation') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="order in orders" :key="order.id" class="table-row">
<td class="checkbox-col">
2025-11-13 17:01:39 +08:00
<input
type="checkbox"
:checked="selectedOrders.some(o => o.id === order.id)"
@change="toggleOrderSelection(order)" />
</td>
<td>{{ order.orderNumber || order.id }}</td>
2025-11-13 17:01:39 +08:00
<td>{{ order.user?.username || $t('orders.unpaid') }}</td>
<td>{{ order.currency || '¥' }}{{ order.totalAmount || 0 }}</td>
<td>
2025-11-13 17:01:39 +08:00
<span v-if="order.paymentMethod === 'ALIPAY'">
<el-icon><CreditCard /></el-icon> {{ $t('orders.alipay') }}
</span>
<span v-else-if="order.paymentMethod === 'WECHAT'">
<el-icon><CreditCard /></el-icon> {{ $t('orders.wechat') }}
</span>
<span v-else-if="order.paymentMethod === 'PAYPAL'">
<el-icon><Wallet /></el-icon> {{ $t('orders.paypal') }}
</span>
<span v-else class="text-muted">{{ $t('orders.unpaid') }}</span>
</td>
<td>
<span class="status-tag" :class="getStatusClass(order.status)">
{{ getStatusText(order.status) }}
</span>
</td>
<td>{{ formatDate(order.createdAt) }}</td>
<td>
2025-11-13 17:01:39 +08:00
<el-link type="primary" class="action-link" @click="viewOrder(order)">{{ $t('common.view') }}</el-link>
<el-link type="danger" class="action-link" @click="deleteOrder(order)">{{ $t('common.delete') }}</el-link>
</td>
</tr>
</tbody>
</table>
</div>
2025-10-21 16:50:33 +08:00
<!-- 分页 -->
<div class="pagination-container">
<div class="pagination">
<el-icon class="page-arrow" @click="prevPage" :class="{ disabled: currentPage === 1 }"><ArrowLeft /></el-icon>
<button
v-for="page in visiblePages"
:key="page"
class="page-btn"
:class="{ active: page === currentPage }"
@click="goToPage(page)">
{{ page }}
</button>
<template v-if="totalPages > 7 && currentPage < totalPages - 2">
<span class="page-ellipsis">...</span>
<button
class="page-btn"
:class="{ active: totalPages === currentPage }"
@click="goToPage(totalPages)">
{{ totalPages }}
</button>
2025-10-21 16:50:33 +08:00
</template>
<el-icon class="page-arrow" @click="nextPage" :class="{ disabled: currentPage === totalPages }"><ArrowRight /></el-icon>
</div>
</div>
</section>
</main>
<!-- 订单详情弹窗 -->
<el-dialog
v-model="orderDetailVisible"
:title="$t('orders.orderDetail') || '订单详情'"
width="600px"
class="order-detail-dialog"
>
<div v-if="currentOrderDetail" class="order-detail-content">
<div class="detail-section">
<h4>{{ $t('orders.basicInfo') || '基本信息' }}</h4>
<div class="detail-grid">
<div class="detail-item">
<span class="label">{{ $t('orders.orderNumber') }}:</span>
<span class="value">{{ currentOrderDetail.orderNumber || currentOrderDetail.id }}</span>
</div>
<div class="detail-item">
<span class="label">{{ $t('orders.username') }}:</span>
<span class="value">{{ currentOrderDetail.user?.username || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">{{ $t('orders.orderType') || '订单类型' }}:</span>
<span class="value">{{ getOrderTypeText(currentOrderDetail.orderType) }}</span>
</div>
<div class="detail-item">
<span class="label">{{ $t('orders.status') }}:</span>
<span class="status-tag" :class="getStatusClass(currentOrderDetail.status)">
{{ getStatusText(currentOrderDetail.status) }}
</span>
</div>
</div>
</div>
<div class="detail-section">
<h4>{{ $t('orders.paymentInfo') || '支付信息' }}</h4>
<div class="detail-grid">
<div class="detail-item">
<span class="label">{{ $t('orders.amount') }}:</span>
<span class="value amount">{{ currentOrderDetail.currency || '¥' }}{{ currentOrderDetail.totalAmount || 0 }}</span>
</div>
<div class="detail-item">
<span class="label">{{ $t('orders.paymentMethod') }}:</span>
<span class="value">
<template v-if="currentOrderDetail.paymentMethod === 'ALIPAY'">{{ $t('orders.alipay') }}</template>
<template v-else-if="currentOrderDetail.paymentMethod === 'WECHAT'">{{ $t('orders.wechat') }}</template>
<template v-else-if="currentOrderDetail.paymentMethod === 'PAYPAL'">{{ $t('orders.paypal') }}</template>
<template v-else>{{ $t('orders.unpaid') }}</template>
</span>
</div>
<div class="detail-item">
<span class="label">{{ $t('orders.createTime') }}:</span>
<span class="value">{{ formatDate(currentOrderDetail.createdAt) }}</span>
</div>
<div class="detail-item" v-if="currentOrderDetail.paidAt">
<span class="label">{{ $t('orders.paidTime') || '支付时间' }}:</span>
<span class="value">{{ formatDate(currentOrderDetail.paidAt) }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="currentOrderDetail.contactEmail || currentOrderDetail.contactPhone">
<h4>{{ $t('orders.contactInfo') || '联系信息' }}</h4>
<div class="detail-grid">
<div class="detail-item" v-if="currentOrderDetail.contactEmail">
<span class="label">{{ $t('orders.email') || '邮箱' }}:</span>
<span class="value">{{ currentOrderDetail.contactEmail }}</span>
</div>
<div class="detail-item" v-if="currentOrderDetail.contactPhone">
<span class="label">{{ $t('orders.phone') || '电话' }}:</span>
<span class="value">{{ currentOrderDetail.contactPhone }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="currentOrderDetail.description">
<h4>{{ $t('orders.description') || '订单描述' }}</h4>
<p class="description-text">{{ currentOrderDetail.description }}</p>
</div>
</div>
<div v-else class="loading-container">
<el-icon class="is-loading"><Loading /></el-icon>
<span>{{ $t('common.loading') || '加载中...' }}</span>
</div>
<template #footer>
<el-button @click="orderDetailVisible = false">{{ $t('common.close') || '关闭' }}</el-button>
</template>
</el-dialog>
2025-10-21 16:50:33 +08:00
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
2025-10-21 16:50:33 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import {
Grid,
User,
ShoppingCart,
Document,
Setting,
Search,
ArrowDown,
ArrowLeft,
ArrowRight,
Delete,
CreditCard,
Wallet,
Loading
} from '@element-plus/icons-vue'
import { getOrders, getAdminOrders, getOrderStats, deleteOrder as deleteOrderAPI, deleteOrders } from '@/api/orders'
2025-11-13 17:01:39 +08:00
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
// 判断是否为管理员模式(基于路由路径)
const isAdminMode = computed(() => route.path.includes('/admin/'))
2025-10-21 16:50:33 +08:00
const loading = ref(false)
const orders = ref([])
const searchText = ref('')
const selectedOrders = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const totalOrders = ref(0)
2025-10-21 16:50:33 +08:00
2025-11-13 17:01:39 +08:00
// 系统状态数据
const onlineUsers = ref('0/500')
const systemUptime = ref(t('nav.loading'))
2025-11-13 17:01:39 +08:00
2025-10-21 16:50:33 +08:00
// 筛选条件
const filters = reactive({
status: '',
paymentMethod: '',
2025-10-21 16:50:33 +08:00
search: ''
})
// 导航功能
const goToDashboard = () => {
router.push('/admin/dashboard')
}
const goToMembers = () => {
router.push('/member-management')
}
2025-10-21 16:50:33 +08:00
const goToAPI = () => {
router.push('/api-management')
}
2025-10-21 16:50:33 +08:00
const goToTasks = () => {
router.push('/generate-task-record')
}
2025-10-21 16:50:33 +08:00
const goToSettings = () => {
router.push('/system-settings')
}
2025-10-21 16:50:33 +08:00
// 普通用户模式的导航函数
const goToProfile = () => {
router.push('/profile')
}
const goToWorks = () => {
router.push('/works')
}
// 处理用户头像下拉菜单
const handleUserCommand = (command) => {
if (command === 'exitAdmin') {
// 退出后台,返回个人首页
router.push('/profile')
}
}
// 获取状态样式类
const getStatusClass = (status) => {
2025-10-21 16:50:33 +08:00
const statusMap = {
'PENDING': 'pending',
'CONFIRMED': 'confirmed',
'PAID': 'paid',
'PROCESSING': 'processing',
'SHIPPED': 'shipped',
'DELIVERED': 'delivered',
'COMPLETED': 'completed',
'CANCELLED': 'cancelled',
'REFUNDED': 'refunded'
2025-10-21 16:50:33 +08:00
}
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 isAllSelected = computed(() => {
return orders.value.length > 0 && selectedOrders.value.length === orders.value.length
})
const totalPages = computed(() => {
return Math.ceil(totalOrders.value / pageSize.value)
})
const visiblePages = computed(() => {
const pages = []
const total = totalPages.value
const current = currentPage.value
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i)
}
} else {
if (current <= 3) {
for (let i = 1; i <= 5; i++) {
pages.push(i)
}
} else if (current >= total - 2) {
for (let i = total - 4; i <= total; i++) {
pages.push(i)
}
} else {
for (let i = current - 2; i <= current + 2; i++) {
pages.push(i)
}
}
}
return pages
})
const toggleAllSelection = () => {
if (isAllSelected.value) {
selectedOrders.value = []
} else {
selectedOrders.value = [...orders.value]
}
}
const toggleOrderSelection = (order) => {
const index = selectedOrders.value.findIndex(o => o.id === order.id)
if (index > -1) {
selectedOrders.value.splice(index, 1)
} else {
selectedOrders.value.push(order)
}
2025-10-21 16:50:33 +08:00
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
fetchOrders()
2025-10-21 16:50:33 +08:00
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
fetchOrders()
}
2025-10-21 16:50:33 +08:00
}
const goToPage = (page) => {
currentPage.value = page
fetchOrders()
2025-10-21 16:50:33 +08:00
}
// 订单详情弹窗相关
const orderDetailVisible = ref(false)
const currentOrderDetail = ref(null)
const viewOrder = async (order) => {
currentOrderDetail.value = order
orderDetailVisible.value = true
}
2025-10-21 16:50:33 +08:00
const deleteOrder = async (order) => {
try {
await ElMessageBox.confirm(
t('orders.confirmDeleteOrder', { orderNumber: order.orderNumber || order.id }),
t('orders.confirmDeleteTitle'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning',
}
)
const response = await deleteOrderAPI(order.id)
console.log('Delete order response:', response)
// 检查响应状态
if (response.data?.success) {
const index = orders.value.findIndex(o => o.id === order.id)
if (index > -1) {
orders.value.splice(index, 1)
totalOrders.value--
}
ElMessage.success(t('orders.deleteSuccess'))
} else {
ElMessage.error(response.data?.message || t('orders.deleteFailed'))
}
} catch (error) {
if (error !== 'cancel') {
console.error('Delete failed:', error)
ElMessage.error(t('orders.deleteFailed') + ': ' + (error.message || t('dashboard.unknownError')))
}
}
}
const deleteSelected = async () => {
if (selectedOrders.value.length === 0) {
ElMessage.warning(t('orders.pleaseSelectOrders'))
return
2025-10-21 16:50:33 +08:00
}
try {
await ElMessageBox.confirm(
t('orders.confirmBatchDelete', { count: selectedOrders.value.length }),
t('orders.batchDeleteTitle'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning',
}
)
const ids = selectedOrders.value.map(o => o.id)
const response = await deleteOrders(ids)
console.log('Batch delete orders response:', response)
if (response.data?.success) {
orders.value = orders.value.filter(o => !ids.includes(o.id))
totalOrders.value -= response.data?.deletedCount || ids.length
selectedOrders.value = []
ElMessage.success(response.data?.message || t('orders.batchDeleteSuccess'))
} else {
ElMessage.error(response.data?.message || t('orders.batchDeleteFailed'))
}
} catch (error) {
if (error !== 'cancel') {
console.error('Batch delete failed:', error)
ElMessage.error(t('orders.batchDeleteFailed') + ': ' + (error.message || t('dashboard.unknownError')))
}
}
2025-10-21 16:50:33 +08:00
}
// 获取订单列表
const fetchOrders = async () => {
try {
loading.value = true
// 根据模式调用不同的 API
const apiFunction = isAdminMode.value ? getAdminOrders : getOrders
const response = await apiFunction({
page: currentPage.value - 1,
size: pageSize.value,
status: filters.status || undefined,
paymentMethod: filters.paymentMethod || undefined,
search: filters.search || searchText.value || undefined
2025-10-21 16:50:33 +08:00
})
console.log('获取订单列表响应:', response)
// 后端返回格式: { success: true, data: { content: [...], totalElements: 100, ... } }
// axios会将响应包装在response.data中
const responseData = response?.data || response || {}
console.log('解析后的响应数据:', responseData)
if (responseData.success && responseData.data) {
const pageData = responseData.data
if (pageData.content) {
// Spring Data Page格式
orders.value = pageData.content || []
totalOrders.value = pageData.totalElements || 0
console.log('设置后的订单列表:', orders.value)
} else if (Array.isArray(pageData)) {
// 直接数组格式
orders.value = pageData
totalOrders.value = pageData.length
} else {
console.error('API data format error: data is not Page object or array', pageData)
ElMessage.error(t('orders.apiDataFormatError'))
}
} else if (responseData.content) {
// 直接返回Page对象没有success包装
orders.value = responseData.content || []
totalOrders.value = responseData.totalElements || 0
} else if (responseData.list) {
// 列表格式
orders.value = responseData.list || []
totalOrders.value = responseData.total || 0
} else {
console.error('API data format error:', responseData)
ElMessage.error(t('orders.apiDataFormatError'))
2025-10-21 16:50:33 +08:00
}
2025-10-21 16:50:33 +08:00
} catch (error) {
console.error('Get orders list failed:', error)
ElMessage.error(t('orders.loadOrdersFailed') + ': ' + (error.message || t('dashboard.unknownError')))
2025-10-21 16:50:33 +08:00
} finally {
loading.value = false
}
}
// 筛选变化
const handleFilterChange = () => {
currentPage.value = 1
2025-10-21 16:50:33 +08:00
fetchOrders()
}
onMounted(() => {
2025-10-21 16:50:33 +08:00
fetchOrders()
2025-11-13 17:01:39 +08:00
fetchSystemStats()
})
2025-11-13 17:01:39 +08:00
// 获取系统统计数据(当天访问人数和系统运行时间)
2025-11-13 17:01:39 +08:00
const fetchSystemStats = async () => {
try {
const response = await fetch('/api/admin/online-stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
const data = await response.json()
if (data.success) {
onlineUsers.value = data.todayVisitors || 0
systemUptime.value = data.uptime || t('systemSettings.unknown')
} else {
onlineUsers.value = '0'
systemUptime.value = t('systemSettings.unknown')
}
2025-11-13 17:01:39 +08:00
} catch (error) {
console.error('Get online stats failed:', error)
onlineUsers.value = '0'
systemUptime.value = t('systemSettings.unknown')
2025-11-13 17:01:39 +08:00
}
}
</script>
<style scoped>
.admin-orders {
display: flex;
min-height: 100vh;
background: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2025-10-21 16:50:33 +08:00
}
/* 左侧导航栏 */
.sidebar {
width: 240px;
background: #ffffff;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
padding: 24px 0;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
2025-10-21 16:50:33 +08:00
}
.logo {
2025-11-13 17:01:39 +08:00
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
margin-bottom: 32px;
2025-10-21 16:50:33 +08:00
}
.logo img {
2025-11-13 17:01:39 +08:00
width: 100%;
height: auto;
max-width: 180px;
2025-11-13 17:01:39 +08:00
object-fit: contain;
2025-10-21 16:50:33 +08:00
}
.nav-menu {
flex: 1;
padding: 0 16px;
2025-10-21 16:50:33 +08:00
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #4b5563;
font-size: 14px;
font-weight: 500;
2025-10-21 16:50:33 +08:00
}
.nav-item:hover {
background: #f3f4f6;
color: #1f2937;
2025-10-21 16:50:33 +08:00
}
.nav-item.active {
background: rgba(64, 158, 255, 0.15);
color: #409EFF;
2025-10-21 16:50:33 +08:00
}
.nav-item .el-icon {
margin-right: 12px;
font-size: 18px;
2025-10-21 16:50:33 +08:00
}
.nav-item span {
font-size: 14px;
font-weight: 500;
2025-10-21 16:50:33 +08:00
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
margin-top: auto;
color: #4b5563;
}
2025-10-21 16:50:33 +08:00
.online-users,
.system-uptime {
font-size: 13px;
color: #4b5563;
margin-bottom: 8px;
line-height: 1.5;
2025-10-21 16:50:33 +08:00
}
.sidebar-footer .highlight {
color: #409EFF;
font-weight: 600;
}
.highlight {
color: #409EFF;
font-weight: 600;
2025-10-21 16:50:33 +08:00
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
/* 顶部搜索栏 */
.top-header {
background: white;
border-bottom: 1px solid #e9ecef;
padding: 16px 24px;
2025-10-21 16:50:33 +08:00
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-bar {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #9ca3af;
font-size: 16px;
z-index: 1;
}
.search-input {
width: 300px;
padding: 10px 12px 10px 40px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
background: white;
outline: none;
transition: border-color 0.2s ease;
}
.search-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
2025-10-21 16:50:33 +08:00
}
.search-input::placeholder {
color: #9ca3af;
2025-10-21 16:50:33 +08:00
}
.header-actions {
2025-10-21 16:50:33 +08:00
display: flex;
align-items: center;
gap: 20px;
}
.user-avatar {
display: flex;
align-items: center;
gap: 8px;
2025-10-21 16:50:33 +08:00
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: background 0.2s ease;
}
.user-avatar:hover {
background: #f3f4f6;
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
2025-10-21 16:50:33 +08:00
}
.user-avatar .arrow-down {
font-size: 12px;
color: #6b7280;
2025-10-21 16:50:33 +08:00
}
/* 订单内容区域 */
.order-content {
padding: 24px;
2025-10-21 16:50:33 +08:00
flex: 1;
background: white;
margin: 24px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
2025-10-21 16:50:33 +08:00
}
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.content-header h2 {
font-size: 24px;
font-weight: 600;
color: #1e293b;
margin: 0;
2025-10-21 16:50:33 +08:00
}
.selection-info {
2025-10-21 16:50:33 +08:00
font-size: 14px;
color: #64748b;
background: #f1f5f9;
padding: 8px 16px;
border-radius: 6px;
2025-10-21 16:50:33 +08:00
}
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
2025-10-21 16:50:33 +08:00
}
.toolbar-left {
display: flex;
align-items: center;
gap: 16px;
2025-10-21 16:50:33 +08:00
}
/* 确保下拉框选中值可见 */
.toolbar-left :deep(.el-select) {
min-width: 120px;
}
.toolbar-left :deep(.el-select__wrapper) {
color: #374151 !important;
font-size: 14px;
min-height: 32px;
}
.toolbar-left :deep(.el-select__wrapper *) {
color: #374151 !important;
}
.toolbar-left :deep(.el-select__selection) {
display: flex !important;
align-items: center !important;
}
.toolbar-left :deep(.el-select__selected-item) {
color: #374151 !important;
display: inline-flex !important;
visibility: visible !important;
opacity: 1 !important;
}
.toolbar-left :deep(.el-select__placeholder) {
color: #9ca3af !important;
opacity: 1 !important;
visibility: visible !important;
}
.table-container {
background: white;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
margin-bottom: 24px;
2025-10-21 16:50:33 +08:00
}
.order-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.order-table thead {
background: #f9fafb;
}
.order-table th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.order-table td {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
color: #374151;
}
.table-row:hover {
background: #f9fafb;
}
.checkbox-col {
width: 50px;
text-align: center;
}
.checkbox-col input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
2025-10-21 16:50:33 +08:00
font-weight: 500;
color: white;
}
.status-tag.completed {
background: #10b981;
}
.status-tag.cancelled {
background: #ef4444;
}
.status-tag.processing {
background: #3b82f6;
}
.status-tag.pending {
background: #f59e0b;
}
.status-tag.paid {
background: #6366f1;
}
.action-link {
margin-right: 12px;
font-size: 14px;
text-decoration: none;
}
.action-link:last-child {
margin-right: 0;
2025-10-21 16:50:33 +08:00
}
.pagination-container {
display: flex;
justify-content: flex-end;
margin-top: 24px;
2025-10-21 16:50:33 +08:00
}
.pagination {
2025-10-21 16:50:33 +08:00
display: flex;
align-items: center;
gap: 4px;
2025-10-21 16:50:33 +08:00
}
.page-arrow {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d1d5db;
background: white;
color: #374151;
cursor: pointer;
border-radius: 4px;
2025-10-21 16:50:33 +08:00
font-size: 14px;
transition: all 0.2s ease;
2025-10-21 16:50:33 +08:00
}
.page-arrow:hover:not(.disabled) {
background: #f3f4f6;
border-color: #9ca3af;
2025-10-21 16:50:33 +08:00
}
.page-arrow.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-btn {
min-width: 32px;
height: 32px;
padding: 0 12px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
2025-10-21 16:50:33 +08:00
display: flex;
align-items: center;
2025-10-21 16:50:33 +08:00
justify-content: center;
}
.page-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
}
.page-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
2025-10-21 16:50:33 +08:00
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-ellipsis {
padding: 0 8px;
color: #6b7280;
font-size: 14px;
}
2025-11-13 17:01:39 +08:00
/* 未支付文本样式 */
.text-muted {
color: #9ca3af;
font-size: 13px;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.admin-orders {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
}
.nav-menu {
display: flex;
overflow-x: auto;
padding: 0 16px;
}
.nav-item {
white-space: nowrap;
margin-right: 16px;
margin-bottom: 0;
}
.sidebar-footer {
display: none;
}
.search-input {
width: 200px;
2025-10-21 16:50:33 +08:00
}
.order-content {
padding: 16px;
2025-10-21 16:50:33 +08:00
}
}
/* 订单详情弹窗样式 */
.order-detail-content {
padding: 0 10px;
}
.detail-section {
margin-bottom: 24px;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-section h4 {
font-size: 15px;
font-weight: 600;
color: #1e293b;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 24px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
}
.detail-item .label {
color: #64748b;
font-size: 14px;
min-width: 80px;
}
.detail-item .value {
color: #1e293b;
font-size: 14px;
font-weight: 500;
}
.detail-item .value.amount {
color: #f59e0b;
font-weight: 600;
font-size: 16px;
}
.description-text {
color: #475569;
font-size: 14px;
line-height: 1.6;
margin: 0;
padding: 12px;
background: #f8fafc;
border-radius: 6px;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #64748b;
gap: 12px;
}
.loading-container .el-icon {
font-size: 32px;
color: #3b82f6;
}
:deep(.order-detail-dialog .el-dialog__header) {
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
margin-right: 0;
}
:deep(.order-detail-dialog .el-dialog__body) {
padding: 20px;
}
:deep(.order-detail-dialog .el-dialog__footer) {
padding: 12px 20px;
border-top: 1px solid #e5e7eb;
}
2025-10-21 16:50:33 +08:00
</style>