feat: add orders

This commit is contained in:
yanghao05
2025-04-10 21:42:13 +08:00
parent 5a63eee1ce
commit 8baab46132
20 changed files with 979 additions and 1 deletions

View File

@@ -27,6 +27,11 @@ const navItems = ref([
icon: 'pi pi-users',
command: () => router.push('/users')
},
{
label: 'Orders',
icon: 'pi pi-shopping-cart',
command: () => router.push('/orders')
},
{
label: 'Settings',
icon: 'pi pi-cog',

View File

@@ -0,0 +1,16 @@
import httpClient from './httpClient';
export const orderService = {
getOrders({ page = 1, limit = 10, keyword = '' } = {}) {
return httpClient.get('/admin/orders', {
params: {
page,
limit,
keyword: keyword.trim()
}
});
},
deleteOrder(id) {
return httpClient.delete(`/admin/orders/${id}`);
}
}

View File

@@ -0,0 +1,24 @@
{
"page": 1,
"limit": 10,
"total": 1,
"items": [
{
"id": 1,
"created_at": "2025-04-10T21:30:27.585874Z",
"updated_at": "2025-04-10T21:30:27.585877Z",
"order_no": "20250410213027",
"sub_order_no": "20250410213027",
"transaction_id": "",
"refund_transaction_id": "",
"price": 325,
"discount": 58,
"currency": "",
"payment_method": "",
"post_id": 1,
"user_id": 1,
"status": 0,
"meta": {}
}
]
}

View File

@@ -0,0 +1,207 @@
<script setup>
import { orderService } from '@/api/orderService';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import Badge from 'primevue/badge';
import Button from 'primevue/button';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import InputText from 'primevue/inputtext';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
const toast = useToast();
const confirm = useConfirm();
const globalFilterValue = ref('');
const loading = ref(false);
const searchTimeout = ref(null);
const filters = ref({
global: { value: null, matchMode: 'contains' },
status: { value: null, matchMode: 'equals' }
});
const orders = ref({
items: [],
total: 0,
page: 1,
limit: 10
});
const first = ref(0);
const rows = ref(10);
dayjs.extend(utc);
dayjs.extend(timezone);
const orderStatusMap = {
0: { label: '待支付', severity: 'warning' },
1: { label: '已支付', severity: 'success' },
2: { label: '已退款', severity: 'info' },
3: { label: '已取消', severity: 'danger' }
};
const formatPrice = (price) => {
return (price / 100).toFixed(2);
};
const getDiscountAmount = (price, discount) => {
return (price * discount / 100);
};
const getFinalPrice = (price, discount) => {
return price - getDiscountAmount(price, discount);
};
const fetchOrders = async () => {
loading.value = true;
try {
const currentPage = (first.value / rows.value) + 1;
const response = await orderService.getOrders({
page: currentPage,
limit: rows.value,
keyword: globalFilterValue.value
});
orders.value = response.data;
} catch (error) {
console.error('Failed to fetch orders:', error);
toast.add({ severity: 'error', summary: '错误', detail: '加载订单数据失败', life: 3000 });
} finally {
loading.value = false;
}
};
const onPage = (event) => {
first.value = event.first;
rows.value = event.rows;
fetchOrders();
};
const onSearch = (event) => {
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
}
searchTimeout.value = setTimeout(() => {
first.value = 0;
fetchOrders();
}, 300);
};
const formatDate = (date) => {
return dayjs.tz(date, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
};
const handleDelete = (order) => {
confirm.require({
message: `确定要删除订单 "${order.id}" 吗?`,
header: '确认删除',
icon: 'pi pi-exclamation-triangle',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await orderService.deleteOrder(order.id);
toast.add({ severity: 'success', summary: '成功', detail: '订单已删除', life: 3000 });
fetchOrders();
} catch (error) {
console.error('Failed to delete order:', error);
toast.add({ severity: 'error', summary: '错误', detail: '删除订单失败', life: 3000 });
}
}
});
};
onMounted(() => {
fetchOrders();
});
</script>
<template>
<Toast />
<ConfirmDialog />
<div class="w-full">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-800">订单列表</h1>
</div>
<div class="card">
<div class="pb-10 flex">
<InputText v-model="globalFilterValue" placeholder="搜索订单..." class="flex-1" @input="onSearch" />
</div>
<DataTable v-model:filters="filters" :value="orders.items" :paginator="true" :rows="rows"
:totalRecords="orders.total" :loading="loading" :lazy="true" :first="first" @page="onPage"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[10, 25, 50]"
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
:globalFilterFields="['order_no', 'user_id']" stripedRows removableSort class="p-datatable-sm"
responsiveLayout="scroll">
<template #empty>
<div class="text-center p-4">未找到订单</div>
</template>
<template #loading>
<div class="flex flex-col items-center justify-center p-4">
<ProgressSpinner style="width:50px;height:50px" />
<span class="mt-2">加载订单数据...</span>
</div>
</template>
<Column field="id" header="ID" sortable></Column>
<Column field="order_no" header="订单号" sortable>
<template #body="{ data }">
<div class="flex flex-col">
<span class="text-gray-700">系统订单号: {{ data.order_no }}</span>
<span class="text-gray-500">商户订单号: {{ data.transaction_id || '-' }}</span>
<span class="text-orange-500">退款订单号: {{ data.refund_transaction_id || '-' }}</span>
</div>
</template>
</Column>
<Column field="status" header="状态" sortable>
<template #body="{ data }">
<Badge :value="orderStatusMap[data.status]?.label"
:severity="orderStatusMap[data.status]?.severity" />
</template>
</Column>
<Column field="user_id" header="用户ID" sortable></Column>
<Column field="post_id" header="文章ID" sortable></Column>
<Column field="price" header="价格信息" sortable>
<template #body="{ data }">
<div class="flex flex-col">
<span class="text-gray-500">原价: ¥{{ formatPrice(data.price) }}</span>
<span class="text-orange-500">优惠: -¥{{ formatPrice(getDiscountAmount(data.price,
data.discount)) }}</span>
<span class="font-bold">实付: ¥{{ formatPrice(getFinalPrice(data.price, data.discount))
}}</span>
</div>
</template>
</Column>
<Column field="updated_at" header="时间信息" sortable>
<template #body="{ data }">
<div class="flex flex-col">
<span class="text-gray-500">更新: {{ formatDate(data.updated_at) }}</span>
<span class="text-gray-400">创建: {{ formatDate(data.created_at) }}</span>
</div>
</template>
</Column>
<Column header="操作" :exportable="false" style="min-width:8rem">
<template #body="{ data }">
<div class="flex justify-center space-x-2">
<Button icon="pi pi-trash" rounded text severity="danger" @click="handleDelete(data)"
aria-label="删除" />
</div>
</template>
</Column>
</DataTable>
</div>
</div>
</template>

View File

@@ -39,6 +39,11 @@ const routes = [
path: '/users',
name: 'Users',
component: () => import('./pages/UserPage.vue'),
},
{
path: '/orders',
name: 'Orders',
component: () => import('./pages/OrderPage.vue'),
}
];