deotalandAi/apps/frontend/src/views/OrderManagement/OrderManagement.vue

364 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-scrollbar height="100%" @end-reached="loadMore">
<div class="order-management">
<div class="orders-section">
<div class="filter-section">
<div class="filter-row">
<div class="filter-group">
<label class="filter-label">{{ t('orderManagement.filters.status') }}</label>
<div class="status-filter">
<button
v-for="status in statusFilters"
:key="status.key"
:class="['status-tab', { active: selectedStatus === status.key }]"
@click="selectStatus(status.key)"
>
{{ t(status.label) }}
<!-- <el-badge
v-if="status.count !== undefined"
:value="status.count"
:max="99"
:type="selectedStatus === status.key ? 'primary' : 'info'"
/> -->
</button>
</div>
</div>
<div class="filter-group">
<label class="filter-label">{{ t('orderManagement.filters.search') }}</label>
<el-input
v-model="order_no"
:placeholder="t('orderManagement.searchPlaceholder')"
class="search-input"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="filter-group">
<label class="filter-label">{{ t('orderManagement.filters.sort') }}</label>
<el-select v-model="sort_by" class="sort-select">
<el-option v-for="opt in sortOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</div>
</div>
</div>
<div class="orders-grid">
<OrderCard
v-for="order in order_list"
:key="order.id"
:order="order"
@view-details="viewOrderDetails"
@pay-order="handlePayOrder"
@cancel-order="handleCancelOrder"
@expired-order="handleExpiredOrder"
@confirm-order="handleConfirmOrder"
/>
</div>
<div v-if="order_list.length === 0" class="empty-state">
<div class="empty-icon">
<el-icon><DocumentDelete /></el-icon>
</div>
<h3 class="empty-title">{{ t('orderManagement.empty.title') }}</h3>
<p class="empty-description">{{ t('orderManagement.empty.description') }}</p>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { DocumentDelete, Search } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import OrderCard from '@/components/OrderCard.vue'
import { useOrderStore } from '@/stores/orders'
import {OrderManagement} from './OrderManagement'
import {orderStatus,isWeChatBrowser,WechatBus} from '@deotaland/utils'
const orderPlug = new OrderManagement()
const { t } = useI18n()
const router = useRouter()
const orderStore = useOrderStore()
const selectedStatus = ref('all')
const statusFilters = orderStatus.selectList('1');
const sortOptions = ref([
{ label: t('orderManagement.sort.created_at'), value: 'created_at' },
{ label: t('orderManagement.sort.total'), value: 'amount' },
])
const viewOrderDetails = (orderData) => {
console.log(orderData);
router.push({ name: 'order-detail', params: { orderId:orderData.id } })
}
const handlePayOrder = (orderData) => {//处理支付订单
console.log(orderData);
if(isWeChatBrowser()){
const order_info = orderData.order_info
if(!order_info.pay_params){
ElMessage.error('当前订单不属于小程序订单')
return
}
const token = window.localStorage.getItem('token')
WechatBus.BusWechartForNavigate('/pages/pay/pay',{
method:'pay',
payData:JSON.stringify({
...orderData.order_info,
token:token,
amount:orderData.order_info.amount,
order_no:orderData.order_no,
})
})
return
}
window.location.href = orderData.stripe_url
}
//确认收货
const handleConfirmOrder = (id)=>{
ElMessageBox.confirm(
t('orderManagement.confirm.message'),
t('orderManagement.confirm.title'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
).then(() => {
orderPlug.receiveAddress(id).then(res => {
init()
})
})
}
const handleCancelOrder = (orderData) => {
ElMessageBox.confirm(
t('orderManagement.cancelConfirm.message'),
t('orderManagement.cancelConfirm.title'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
).then(() => {
orderPlug.orderCancel({
id: orderData.id
}).then(res => {
if (res.code === 0) {
ElMessage.success(t('orderManagement.cancelSuccess'))
init()
} else {
ElMessage.error(res.message || t('orderManagement.cancelFail'))
}
})
}).catch(() => {
// 用户点击取消,无需处理
})
}
const handleExpiredOrder = (orderId) => {
orderStore.updateOrder(orderId, { status: 'expired' })
}
const page = ref(1);
const page_size = ref(10);
//订单筛选
const order_no = ref('');
const loading = ref(false);
const finished = ref(false);
//排序方式
const sort_by = ref('created_at')
const sort_order = ref('desc');//asc/desc
const order_list = ref([]);
const total = ref(0);
//剩余筛选条件
const sxtjjson = ref({});
const loadMore = ()=>{
getOrderList();
}
//获取订单列表
const getOrderList = ()=>{
if (loading.value || finished.value) return;
loading.value = true;
orderPlug.getOrderList({
page: page.value,
page_size: page_size.value,
order_no: order_no.value,
sort_by: sort_by.value,
sort_order: sort_order.value,
...sxtjjson.value
}).then(res=>{
if(res.code==0){
let data = res.data;
if (page.value === 1) {
order_list.value = data.items;
} else {
order_list.value = [...order_list.value, ...data.items];
}
total.value = data.total;
finished.value = order_list.value.length >= total.value;
page.value++;
}
})
}
const init = ()=>{
loading.value = false;
finished.value = false;
order_list.value = [];
page.value = 1;
page_size.value = 10;
getOrderList();
}
watch(order_no, () => {
// 防抖300ms 内无新输入再触发
clearTimeout(window._orderNoDebounce)
window._orderNoDebounce = setTimeout(() => {
init()
}, 300)
})
onMounted(() => {
init();
})
const selectStatus = (status) => {
const json = orderStatus.selectOrderStatusOptions(status);
sxtjjson.value = json;
selectedStatus.value = status;
init()
}
watch(sort_by, () => {
init();
})
</script>
<style scoped>
.order-management {
padding: 24px;
height: 100%;
}
.page-header { margin-bottom: 24px; }
.header-content { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.page-title { font-size: 24px; font-weight: 700; margin: 0; }
.page-description { font-size: 14px; color: var(--text-secondary, #6b7280); margin: 4px 0 0; }
.create-order-btn { height: 40px; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section-title { font-size: 18px; font-weight: 600; margin: 0; }
.filter-section {
background: var(--card-bg, #fff);
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
/* Dark theme styles for filter-section */
html.dark .filter-section {
background: var(--card-bg, #1f2937);
border-color: var(--border-color, #374151);
}
.filter-row { display: flex; gap: 24px; align-items: flex-end; flex-wrap: wrap; }
.filter-group { display: flex; flex-direction: column; gap: 8px; min-width: 220px; }
.filter-label { font-size: 12px; font-weight: 600; color: var(--text-secondary, #6b7280); }
/* Dark theme styles for filter-label */
html.dark .filter-label {
color: var(--text-secondary, #d1d5db);
}
.status-filter { display: flex; gap: 8px; flex-wrap: wrap; }
.status-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid var(--border-color, #d1d5db);
border-radius: 6px;
background: var(--card-bg, #fff);
color: var(--text-secondary, #6b7280);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
/* Dark theme styles for status-tab */
html.dark .status-tab {
background: var(--bg-secondary, #374151);
border-color: var(--border-color, #4b5563);
color: var(--text-secondary, #9ca3af);
opacity: 0.8;
}
/* Dark theme hover state for status-tab */
html.dark .status-tab:hover {
border-color: #8b5cf6;
color: #c4b5fd;
background: rgba(139, 92, 246, 0.15);
opacity: 1;
transform: translateY(-1px);
}
/* Dark theme active state for status-tab */
html.dark .status-tab.active {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
border-color: #8b5cf6;
color: white;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4), 0 0 0 2px rgba(139, 92, 246, 0.2);
opacity: 1;
font-weight: 600;
transform: translateY(0);
}
.status-tab:hover {
border-color: var(--el-color-primary, #6B46C1);
color: var(--el-color-primary, #6B46C1);
}
.status-tab.active {
background: var(--el-color-primary, #6B46C1);
border-color: var(--el-color-primary, #6B46C1);
color: white;
}
.search-input { width: 280px; }
.sort-select { width: 180px; }
.orders-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 16px; }
.pay-dialog :deep(.el-dialog) { width: 560px; max-width: 90vw; }
.empty-state { text-align: center; padding: 48px 24px; }
.empty-icon { font-size: 48px; color: #d1d5db; margin-bottom: 12px; }
/* Dark theme styles for empty-icon */
html.dark .empty-icon {
color: var(--text-secondary, #d1d5db);
}
.empty-title { font-size: 18px; font-weight: 600; margin: 0 0 8px 0; }
/* Dark theme styles for empty-title */
html.dark .empty-title {
color: var(--text-primary, #f9fafb);
}
.empty-description { font-size: 14px; color: var(--text-secondary, #6b7280); margin: 0 0 16px 0; }
/* Dark theme styles for empty-description */
html.dark .empty-description {
color: var(--text-secondary, #d1d5db);
}
@media (max-width: 768px) {
.order-management { padding: 16px; }
.orders-grid { grid-template-columns: 1fr; }
.filter-row { flex-direction: column; gap: 16px; }
.filter-group { min-width: auto; }
.search-input, .sort-select { width: 100%; }
}
</style>