415 lines
12 KiB
Plaintext
415 lines
12 KiB
Plaintext
<template>
|
|
<AdminLayout :currentPage="currentPage">
|
|
<view class="order-management-page">
|
|
<!-- 1. 顶部筛选卡片 -->
|
|
<view class="filter-card">
|
|
<view class="filter-row">
|
|
<view class="filter-item">
|
|
<text class="label">订单号:</text>
|
|
<input class="filter-input" v-model="searchOrderNo" placeholder="请输入订单号" @confirm="onSearch" />
|
|
</view>
|
|
<view class="filter-item">
|
|
<text class="label">用户信息:</text>
|
|
<input class="filter-input" v-model="searchUser" placeholder="姓名/手机号" @confirm="onSearch" />
|
|
</view>
|
|
<view class="filter-item">
|
|
<text class="label">下单时间:</text>
|
|
<AnalyticsDateRangePicker
|
|
:initialStartDate="startDate"
|
|
:initialEndDate="endDate"
|
|
@apply="onApplyRange"
|
|
@clear="onClearRange"
|
|
/>
|
|
</view>
|
|
<view class="filter-btns">
|
|
<button class="btn primary" @click="onSearch">查询</button>
|
|
<button class="btn reset" @click="onReset">重置</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 2. 状态选项卡 -->
|
|
<view class="status-tabs-card">
|
|
<view class="status-tabs">
|
|
<view
|
|
v-for="tab in statusTabs"
|
|
:key="tab.value"
|
|
class="tab-item"
|
|
:class="{ active: activeStatus === tab.value }"
|
|
@click="onStatusChange(tab.value)"
|
|
>
|
|
<text class="tab-text">{{ tab.label }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 3. 订单列表表格 -->
|
|
<view class="content-card">
|
|
<view class="table-container">
|
|
<!-- Loading 遮罩 -->
|
|
<view v-if="isLoading" class="loading-mask">
|
|
<text class="loading-text">订单数据加载中...</text>
|
|
</view>
|
|
|
|
<view class="table-header">
|
|
<view class="col col-no"><text>订单号</text></view>
|
|
<view class="col col-user"><text>用户信息</text></view>
|
|
<view class="col col-amount"><text>支付金额</text></view>
|
|
<view class="col col-status"><text>订单状态</text></view>
|
|
<view class="col col-time"><text>下单时间</text></view>
|
|
<view class="col col-ops"><text>操作</text></view>
|
|
</view>
|
|
|
|
<view class="table-body">
|
|
<view v-if="orderList.length === 0 && !isLoading" class="empty-row">
|
|
<text>未找到相关订单</text>
|
|
</view>
|
|
<view v-for="item in orderList" :key="item.id" class="table-row">
|
|
<view class="col col-no">
|
|
<text class="order-no">{{ item.order_no }}</text>
|
|
</view>
|
|
<view class="col col-user">
|
|
<view class="user-info-box">
|
|
<text class="u-name">{{ item.username || '匿名用户' }}</text>
|
|
<text class="u-phone">{{ item.phone || '-' }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="col col-amount">
|
|
<text class="amount-val">¥{{ item.paid_amount?.toFixed(2) || '0.00' }}</text>
|
|
</view>
|
|
<view class="col col-status">
|
|
<text class="status-tag" :class="getStatusClass(item.order_status)">
|
|
{{ getStatusLabel(item.order_status) }}
|
|
</text>
|
|
</view>
|
|
<view class="col col-time">
|
|
<text class="time-text">{{ formatDateTime(item.created_at) }}</text>
|
|
</view>
|
|
<view class="col col-ops">
|
|
<text class="op-link" @click="goDetail(item.id)">详情</text>
|
|
<text class="op-divider">|</text>
|
|
<text class="op-link">备注</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 4. 分页控制 -->
|
|
<view class="pagination">
|
|
<view class="pager-btns">
|
|
<button class="btn small" :disabled="page <= 1" @click="onPrevPage">上一页</button>
|
|
<text class="page-num">第 {{ page }} 页</text>
|
|
<button class="btn small" :disabled="orderList.length < pageSize" @click="onNextPage">下一页</button>
|
|
</view>
|
|
<text class="page-info">当前页 {{ orderList.length }} 条 / 总计 {{ total }} 条</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</AdminLayout>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, onMounted } from 'vue'
|
|
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
|
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
|
import { fetchOrderPage } from '@/services/orderService.uts'
|
|
import AkReq from '@/uni_modules/ak-req/ak-req.uts'
|
|
|
|
const currentPage = ref<string>('OrderList')
|
|
|
|
// --- 数据状态 ---
|
|
const orderList = ref<any[]>([])
|
|
const total = ref(0)
|
|
const isLoading = ref(false)
|
|
|
|
// --- 筛选条件 ---
|
|
const searchOrderNo = ref('')
|
|
const searchUser = ref('')
|
|
const activeStatus = ref<number | null>(null)
|
|
const startDate = ref('')
|
|
const endDate = ref('')
|
|
|
|
const page = ref(1)
|
|
const pageSize = 20
|
|
|
|
const statusTabs = [
|
|
{ label: '全部', value: null },
|
|
{ label: '待付款', value: 1 },
|
|
{ label: '待发货', value: 2 },
|
|
{ label: '待收货', value: 3 },
|
|
{ label: '已完成', value: 4 },
|
|
{ label: '已取消', value: 5 },
|
|
{ label: '退款中', value: 6 },
|
|
{ label: '已退款', value: 7 }
|
|
]
|
|
|
|
onMounted(() => {
|
|
if (AkReq.getToken() == null || AkReq.getToken() === '') return
|
|
loadOrders()
|
|
})
|
|
|
|
async function loadOrders() {
|
|
isLoading.value = true
|
|
try {
|
|
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
|
|
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
|
|
|
|
// 合并搜索词
|
|
const combinedSearch = (searchOrderNo.value || searchUser.value)
|
|
? `${searchOrderNo.value} ${searchUser.value}`.trim()
|
|
: null
|
|
|
|
const res = await fetchOrderPage(
|
|
page.value,
|
|
pageSize,
|
|
activeStatus.value,
|
|
combinedSearch,
|
|
st,
|
|
et
|
|
)
|
|
|
|
orderList.value = res.items
|
|
total.value = res.total
|
|
} catch (e) {
|
|
uni.showToast({ title: '加载订单失败', icon: 'none' })
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// --- 事件处理 ---
|
|
|
|
function onSearch() {
|
|
page.value = 1
|
|
loadOrders()
|
|
}
|
|
|
|
function onReset() {
|
|
searchOrderNo.value = ''
|
|
searchUser.value = ''
|
|
activeStatus.value = null
|
|
startDate.value = ''
|
|
endDate.value = ''
|
|
page.value = 1
|
|
loadOrders()
|
|
}
|
|
|
|
function onStatusChange(status: number | null) {
|
|
activeStatus.value = status
|
|
page.value = 1
|
|
loadOrders()
|
|
}
|
|
|
|
function onApplyRange(payload: any) {
|
|
startDate.value = payload?.start ?? ''
|
|
endDate.value = payload?.end ?? ''
|
|
page.value = 1
|
|
loadOrders()
|
|
}
|
|
|
|
function onClearRange() {
|
|
startDate.value = ''
|
|
endDate.value = ''
|
|
page.value = 1
|
|
loadOrders()
|
|
}
|
|
|
|
function onPrevPage() {
|
|
if (page.value <= 1) return
|
|
page.value--
|
|
loadOrders()
|
|
}
|
|
|
|
function onNextPage() {
|
|
if (orderList.value.length < pageSize) return
|
|
page.value++
|
|
loadOrders()
|
|
}
|
|
|
|
// --- 辅助方法 ---
|
|
|
|
function getStatusLabel(status: number): string {
|
|
const found = statusTabs.find(t => t.value === status)
|
|
return found ? found.label : '未知'
|
|
}
|
|
|
|
function getStatusClass(status: number): string {
|
|
switch (status) {
|
|
case 1: return 'status-pending'
|
|
case 2: return 'status-shipping'
|
|
case 3: return 'status-delivery'
|
|
case 4: return 'status-completed'
|
|
case 5: return 'status-cancelled'
|
|
case 6: case 7: return 'status-refund'
|
|
default: return ''
|
|
}
|
|
}
|
|
|
|
function formatDateTime(iso: string | null): string {
|
|
if (!iso) return '-'
|
|
return iso.replace('T', ' ').split('.')[0]
|
|
}
|
|
|
|
function goDetail(id: string) {
|
|
uni.showToast({ title: '详情页开发中', icon: 'none' })
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.order-management-page {
|
|
padding: 20px;
|
|
background-color: #f5f7f9;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.filter-card {
|
|
background: #fff;
|
|
padding: 24px;
|
|
border-radius: 4px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.filter-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
}
|
|
|
|
.filter-item {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
.label { font-size: 14px; color: #333; margin-right: 8px; }
|
|
}
|
|
|
|
.filter-input {
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
height: 32px;
|
|
width: 180px;
|
|
padding: 0 12px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.filter-btns {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-tabs-card {
|
|
background: #fff;
|
|
margin-bottom: 16px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.status-tabs {
|
|
display: flex;
|
|
flex-direction: row;
|
|
padding: 0 24px;
|
|
}
|
|
|
|
.tab-item {
|
|
padding: 16px 20px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
.tab-text { font-size: 14px; color: #666; }
|
|
&.active {
|
|
.tab-text { color: #1890ff; font-weight: bold; }
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0; left: 0; right: 0;
|
|
height: 2px; background: #1890ff;
|
|
}
|
|
}
|
|
}
|
|
|
|
.content-card {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
position: relative;
|
|
}
|
|
|
|
.table-container {
|
|
padding: 24px;
|
|
min-height: 400px;
|
|
position: relative;
|
|
}
|
|
|
|
.table-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
background-color: #f8faff;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
padding: 12px 0;
|
|
}
|
|
|
|
.table-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
padding: 16px 0;
|
|
align-items: center;
|
|
&:hover { background: #fafafa; }
|
|
}
|
|
|
|
.col { padding: 0 12px; font-size: 14px; color: #333; display: flex; align-items: center; }
|
|
.col-no { width: 200px; }
|
|
.col-user { flex: 1; }
|
|
.col-amount { width: 120px; justify-content: center; }
|
|
.col-status { width: 120px; justify-content: center; }
|
|
.col-time { width: 180px; justify-content: center; }
|
|
.col-ops { width: 150px; justify-content: flex-end; }
|
|
|
|
.order-no { font-family: monospace; color: #1890ff; }
|
|
.user-info-box { display: flex; flex-direction: column; gap: 4px; }
|
|
.u-name { font-weight: 500; }
|
|
.u-phone { font-size: 12px; color: #999; }
|
|
.amount-val { color: #f5222d; font-weight: bold; }
|
|
|
|
.status-tag {
|
|
padding: 2px 10px; border-radius: 4px; font-size: 12px;
|
|
&.status-pending { background: #fff7e6; color: #faad14; border: 1px solid #ffe58f; }
|
|
&.status-shipping { background: #e6f7ff; color: #1890ff; border: 1px solid #bae7ff; }
|
|
&.status-completed { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
|
&.status-cancelled { background: #f5f5f5; color: #999; border: 1px solid #d9d9d9; }
|
|
&.status-refund { background: #fff1f0; color: #f5222d; border: 1px solid #ffa39e; }
|
|
}
|
|
|
|
.op-link { color: #1890ff; cursor: pointer; }
|
|
.op-divider { margin: 0 8px; color: #eee; }
|
|
|
|
.pagination {
|
|
padding: 16px 24px;
|
|
border-top: 1px solid #f0f0f0;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.pager-btns { display: flex; flex-direction: row; align-items: center; gap: 12px; }
|
|
.page-num { font-size: 14px; color: #333; }
|
|
.page-info { font-size: 14px; color: #999; }
|
|
|
|
.loading-mask {
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(255, 255, 255, 0.7);
|
|
display: flex; align-items: center; justify-content: center;
|
|
z-index: 10;
|
|
}
|
|
.empty-row { padding: 60px 0; text-align: center; color: #999; }
|
|
|
|
.btn {
|
|
height: 32px; padding: 0 16px; font-size: 14px; border-radius: 4px; cursor: pointer;
|
|
&.primary { background: #1890ff; color: #fff; border: none; }
|
|
&.reset { background: #fff; color: #666; border: 1px solid #d9d9d9; }
|
|
&.small { height: 28px; padding: 0 12px; font-size: 13px; }
|
|
&[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
}
|
|
</style>
|