refactor: 重构订单管理模块,拆分合并订单页面
1. 删除旧的统一订单列表页,拆分为景点和商品订单面板组件 2. 新增用户个人中心的订单导航,支持通过query参数跳转对应订单页 3. 后端补全商品订单分页查询的关联商品项逻辑 4. 新增商品订单相关API和支付、取消、收货接口 5. 调整下单和支付成功后的跳转路径,重定向到对应订单列表页 6. 添加未登录状态的全局拦截处理
This commit is contained in:
parent
1671215071
commit
966c458348
|
|
@ -242,7 +242,9 @@ public class ProductOrderServiceImpl extends ServiceImpl<ProductOrderMapper, Pro
|
|||
wrapper.eq(ProductOrder::getStatus, status);
|
||||
}
|
||||
wrapper.orderByDesc(ProductOrder::getCreateTime);
|
||||
return baseMapper.selectPage(page, wrapper).convert(this::convertToVO);
|
||||
IPage<ProductOrderVO> result = baseMapper.selectPage(page, wrapper).convert(this::convertToVO);
|
||||
result.getRecords().forEach(vo -> vo.setItems(getOrderItems(vo.getId())));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ http.interceptors.request.use((config) => {
|
|||
http.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const res = response.data
|
||||
if (res.code === 401) {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/user/login'
|
||||
return Promise.reject(new Error(res.msg || '未登录'))
|
||||
}
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg || '请求失败')
|
||||
return Promise.reject(new Error(res.msg))
|
||||
|
|
|
|||
|
|
@ -46,3 +46,50 @@ export function cancelAttractionOrder(data: { id: number; cancelReason?: string
|
|||
export function getMyAttractionOrders(params: { current?: number; size?: number; status?: number }) {
|
||||
return http.get<{ records: AttractionOrder[]; total: number }>('/order/attraction/my', { params })
|
||||
}
|
||||
|
||||
export interface ProductOrderItem {
|
||||
id: number
|
||||
productId: number
|
||||
productName: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export interface ProductOrder {
|
||||
id: number
|
||||
orderNo: string
|
||||
totalQuantity: number
|
||||
totalPrice: number
|
||||
actualAmount: number
|
||||
consignee: string
|
||||
phone: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
detailAddress: string
|
||||
status: number
|
||||
statusText: string
|
||||
createTime: string
|
||||
deliveryTime: string
|
||||
receiveTime: string
|
||||
items: ProductOrderItem[]
|
||||
}
|
||||
|
||||
export function getMyProductOrders(params: { current?: number; size?: number; status?: number }) {
|
||||
return http.get<{ records: ProductOrder[]; total: number }>('/order/product/my', { params })
|
||||
}
|
||||
|
||||
export function payProductOrder(id: number) {
|
||||
return http.put<void>(`/order/product/${id}/pay`)
|
||||
}
|
||||
|
||||
export function cancelProductOrder(data: { id: number; cancelReason?: string }) {
|
||||
return http.put<void>('/order/product/cancel', data)
|
||||
}
|
||||
|
||||
export function receiveProductOrder(id: number) {
|
||||
return http.put<void>(`/order/product/${id}/receive`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,12 +78,6 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('@/views/user/AddressView.vue'),
|
||||
meta: { title: '地址管理' },
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
name: 'OrderList',
|
||||
component: () => import('@/views/order/OrderList.vue'),
|
||||
meta: { title: '我的订单' },
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
name: 'Cart',
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ function submitOrder() {
|
|||
}
|
||||
ElMessage.success('下单成功')
|
||||
cartStore.clear()
|
||||
setTimeout(() => { router.push('/user/order') }, 1000)
|
||||
setTimeout(() => { router.push('/user/profile?orders=product') }, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
<template>
|
||||
<div class="order-list-page" style="padding: 100px 20px 80px;">
|
||||
<div class="container">
|
||||
<h1 class="page-title">我的订单</h1>
|
||||
|
||||
<el-tabs v-model="activeTab" class="order-tabs">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="待支付" name="unpaid" />
|
||||
<el-tab-pane label="已支付" name="paid" />
|
||||
<el-tab-pane label="已取消" name="cancelled" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-if="filteredOrders.length === 0" class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#CBD5E1" stroke-width="1.5"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/></svg>
|
||||
<p>{{ loading ? '加载中...' : '暂无门票订单' }}</p>
|
||||
</div>
|
||||
|
||||
<div v-for="order in filteredOrders" :key="order.id" class="order-card">
|
||||
<div class="order-header">
|
||||
<span class="order-no">订单号:{{ order.orderNo }}</span>
|
||||
<span class="order-status" :class="statusClass(order.status)">{{ statusText(order.status) }}</span>
|
||||
</div>
|
||||
<div class="order-item">
|
||||
<div class="item-img" style="background: linear-gradient(135deg, #38bdf8, #0ea5e9)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"><path d="M20 13c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3a3 3 0 013-3z"/><path d="M4 6h16v4.5a3 3 0 010 3V18a2 2 0 01-2 2H6a2 2 0 01-2-2v-4.5a3 3 0 010-3V6z"/></svg>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4>{{ order.attractionName }} - {{ order.ticketName }}</h4>
|
||||
<p>游玩日期:{{ order.visitDate || '不限' }} · 联系人:{{ order.contactName }} {{ order.contactPhone }}</p>
|
||||
</div>
|
||||
<span class="item-price">¥{{ order.unitPrice }} x {{ order.quantity }}</span>
|
||||
</div>
|
||||
<div class="order-footer">
|
||||
<span class="order-total">合计:<strong>¥{{ Number(order.totalPrice).toFixed(2) }}</strong></span>
|
||||
<div class="order-actions">
|
||||
<el-button v-if="order.status === 1" type="primary" class="btn-action" @click="goPay(order)">去支付</el-button>
|
||||
<el-button v-if="order.status === 1" text @click="handleCancel(order)">取消订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getMyAttractionOrders, cancelAttractionOrder } from '@/api/order'
|
||||
|
||||
const router = useRouter()
|
||||
const activeTab = ref('all')
|
||||
const orders = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const statusTextMap: Record<number, string> = {
|
||||
1: '待支付',
|
||||
2: '已支付',
|
||||
3: '已使用',
|
||||
4: '已取消',
|
||||
5: '退款中',
|
||||
6: '已退款',
|
||||
}
|
||||
|
||||
const filteredOrders = computed(() => {
|
||||
if (activeTab.value === 'all') return orders.value
|
||||
if (activeTab.value === 'unpaid') return orders.value.filter((o) => o.status === 1)
|
||||
if (activeTab.value === 'paid') return orders.value.filter((o) => o.status === 2 || o.status === 3)
|
||||
if (activeTab.value === 'cancelled') return orders.value.filter((o) => o.status === 4)
|
||||
return orders.value
|
||||
})
|
||||
|
||||
function statusText(status: number) {
|
||||
return statusTextMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
function statusClass(status: number) {
|
||||
const map: Record<number, string> = { 1: 'unpaid', 2: 'paid', 3: 'used', 4: 'cancelled', 5: 'refunding', 6: 'refunded' }
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
function goPay(order: any) {
|
||||
router.push({
|
||||
path: '/payment',
|
||||
query: { type: 'attraction', ids: String(order.id), amount: Number(order.totalPrice).toFixed(2) },
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCancel(order: any) {
|
||||
try {
|
||||
await cancelAttractionOrder({ id: order.id, cancelReason: '用户取消' })
|
||||
ElMessage.success('订单已取消')
|
||||
fetchOrders()
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getMyAttractionOrders({ current: 1, size: 100 })
|
||||
orders.value = res.data.records || []
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchOrders)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
svg { margin-bottom: 16px; }
|
||||
p { color: var(--text-muted); font-size: 15px; }
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-no { color: var(--text-muted); }
|
||||
.order-status { font-weight: 500;
|
||||
|
||||
&.unpaid { color: var(--cta); }
|
||||
&.paid, &.used { color: var(--primary); }
|
||||
&.cancelled { color: #94a3b8; }
|
||||
&.refunding, &.refunded { color: #f59e0b; }}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
h4 { font-size: 14px; font-weight: 500; }
|
||||
p { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
|
||||
}
|
||||
|
||||
.item-price { font-size: 14px; color: var(--text-light); white-space: nowrap; }
|
||||
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.order-total { font-size: 14px; strong { color: var(--cta); font-size: 16px; }}
|
||||
|
||||
.order-actions { display: flex; gap: 8px; }
|
||||
|
||||
.btn-action {
|
||||
background: linear-gradient(135deg, var(--cta), var(--cta-hover));
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
&:hover { box-shadow: 0 4px 16px rgba(249,115,22,0.3); }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { payAttractionOrder } from '@/api/order'
|
||||
import { payAttractionOrder, payProductOrder } from '@/api/order'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
|
@ -60,9 +60,14 @@ async function handlePay() {
|
|||
for (const id of orderIds.value) {
|
||||
await payAttractionOrder(id)
|
||||
}
|
||||
} else if (orderType.value === 'product' && orderIds.value.length > 0) {
|
||||
for (const id of orderIds.value) {
|
||||
await payProductOrder(id)
|
||||
}
|
||||
}
|
||||
ElMessage.success('支付成功(沙箱环境)')
|
||||
setTimeout(() => { router.push('/user/order') }, 1000)
|
||||
const target = orderType.value === 'product' ? '/user/profile?orders=product' : '/user/profile?orders=attraction'
|
||||
setTimeout(() => { router.push(target) }, 1000)
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,311 @@
|
|||
<template>
|
||||
<div class="order-panel">
|
||||
<el-tabs v-model="activeStatus" class="order-tabs">
|
||||
<el-tab-pane v-for="tab in statusTabs" :key="tab.value" :label="tab.label" :name="tab.value" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-if="filteredOrders.length === 0" class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#CBD5E1" stroke-width="1.5"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/></svg>
|
||||
<p>{{ loading ? '加载中...' : '暂无订单' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 景点门票订单 -->
|
||||
<template v-if="isAttraction">
|
||||
<div v-for="order in filteredOrders" :key="order.id" class="order-card">
|
||||
<div class="order-header">
|
||||
<span class="order-no">订单号:{{ order.orderNo }}</span>
|
||||
<span class="order-status" :class="attractionStatusClass(order.status)">{{ attractionStatusText(order.status) }}</span>
|
||||
</div>
|
||||
<div class="order-item">
|
||||
<div class="item-img" style="background: linear-gradient(135deg, #38bdf8, #0ea5e9)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"><path d="M20 13c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3a3 3 0 013-3z"/><path d="M4 6h16v4.5a3 3 0 010 3V18a2 2 0 01-2 2H6a2 2 0 01-2-2v-4.5a3 3 0 010-3V6z"/></svg>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4>{{ order.attractionName }} - {{ order.ticketName }}</h4>
|
||||
<p>游玩日期:{{ order.visitDate || '不限' }} · 联系人:{{ order.contactName }} {{ order.contactPhone }}</p>
|
||||
</div>
|
||||
<span class="item-price">¥{{ order.unitPrice }} x {{ order.quantity }}</span>
|
||||
</div>
|
||||
<div class="order-footer">
|
||||
<span class="order-total">合计:<strong>¥{{ Number(order.totalPrice).toFixed(2) }}</strong></span>
|
||||
<div class="order-actions">
|
||||
<el-button v-if="order.status === 1" type="primary" class="btn-action" @click="goPay(order)">去支付</el-button>
|
||||
<el-button v-if="order.status === 1" text @click="handleCancel(order)">取消订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 商品订单 -->
|
||||
<template v-else>
|
||||
<div v-for="order in filteredOrders" :key="order.id" class="order-card">
|
||||
<div class="order-header">
|
||||
<span class="order-no">订单号:{{ order.orderNo }}</span>
|
||||
<span class="order-status" :class="productStatusClass(order.status)">{{ productStatusText(order.status) }}</span>
|
||||
</div>
|
||||
<div v-for="item in order.items || []" :key="item.id" class="order-item">
|
||||
<div class="item-img" style="background: linear-gradient(135deg, #667eea, #764ba2)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"><rect x="3" y="7" width="18" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-4-2h-2a2 2 0 00-2 2v2"/></svg>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4>{{ item.productName }}</h4>
|
||||
<p>{{ item.skuName || '默认规格' }}</p>
|
||||
</div>
|
||||
<span class="item-price">¥{{ item.unitPrice }} x {{ item.quantity }}</span>
|
||||
</div>
|
||||
<div class="order-item address-item">
|
||||
<div class="item-info">
|
||||
<p>收货人:{{ order.consignee }} {{ order.phone }}</p>
|
||||
<p>{{ order.province }}{{ order.city }}{{ order.district }}{{ order.detailAddress }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-footer">
|
||||
<span class="order-total">实付:<strong>¥{{ Number(order.actualAmount ?? order.totalPrice).toFixed(2) }}</strong></span>
|
||||
<div class="order-actions">
|
||||
<el-button v-if="order.status === 1" type="primary" class="btn-action" @click="goPay(order)">去支付</el-button>
|
||||
<el-button v-if="order.status === 1" text @click="handleCancel(order)">取消订单</el-button>
|
||||
<el-button v-if="order.status === 4" type="primary" class="btn-action" @click="handleReceive(order)">确认收货</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
getMyAttractionOrders,
|
||||
cancelAttractionOrder,
|
||||
getMyProductOrders,
|
||||
cancelProductOrder,
|
||||
receiveProductOrder,
|
||||
} from '@/api/order'
|
||||
|
||||
const props = defineProps<{ orderType: 'attraction' | 'product' }>()
|
||||
|
||||
const router = useRouter()
|
||||
const activeStatus = ref('all')
|
||||
const attractionOrders = ref<any[]>([])
|
||||
const productOrders = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const isAttraction = computed(() => props.orderType === 'attraction')
|
||||
|
||||
const attractionStatusMap: Record<number, string> = {
|
||||
1: '待支付',
|
||||
2: '已支付',
|
||||
3: '已使用',
|
||||
4: '已取消',
|
||||
5: '退款中',
|
||||
6: '已退款',
|
||||
}
|
||||
|
||||
const productStatusMap: Record<number, string> = {
|
||||
1: '待支付',
|
||||
2: '已支付',
|
||||
3: '待发货',
|
||||
4: '已发货',
|
||||
5: '待收货',
|
||||
6: '已完成',
|
||||
7: '已取消',
|
||||
8: '退款中',
|
||||
9: '已退款',
|
||||
}
|
||||
|
||||
const statusTabs = computed(() => {
|
||||
if (isAttraction.value) {
|
||||
return [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '已使用', value: 'used' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '已支付', value: 'paid' },
|
||||
{ label: '已发货', value: 'delivered' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
]
|
||||
})
|
||||
|
||||
const filteredOrders = computed(() => {
|
||||
const source = isAttraction.value ? attractionOrders.value : productOrders.value
|
||||
if (activeStatus.value === 'all') return source
|
||||
if (activeStatus.value === 'unpaid') return source.filter((o) => o.status === 1)
|
||||
if (activeStatus.value === 'paid') {
|
||||
return isAttraction.value
|
||||
? source.filter((o) => o.status === 2)
|
||||
: source.filter((o) => o.status === 2 || o.status === 3)
|
||||
}
|
||||
if (activeStatus.value === 'used') return source.filter((o) => o.status === 3)
|
||||
if (activeStatus.value === 'delivered') return source.filter((o) => o.status === 4)
|
||||
if (activeStatus.value === 'completed') return source.filter((o) => o.status === 6)
|
||||
if (activeStatus.value === 'cancelled') return source.filter((o) => o.status === 7 || o.status === 4)
|
||||
return source
|
||||
})
|
||||
|
||||
function attractionStatusText(status: number) {
|
||||
return attractionStatusMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
function productStatusText(status: number) {
|
||||
return productStatusMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
function attractionStatusClass(status: number) {
|
||||
const map: Record<number, string> = { 1: 'unpaid', 2: 'paid', 3: 'used', 4: 'cancelled', 5: 'refunding', 6: 'refunded' }
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
function productStatusClass(status: number) {
|
||||
const map: Record<number, string> = { 1: 'unpaid', 2: 'paid', 3: 'pending', 4: 'delivered', 5: 'received', 6: 'completed', 7: 'cancelled', 8: 'refunding', 9: 'refunded' }
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
function goPay(order: any) {
|
||||
const amount = isAttraction.value
|
||||
? Number(order.totalPrice).toFixed(2)
|
||||
: Number(order.actualAmount ?? order.totalPrice).toFixed(2)
|
||||
router.push({
|
||||
path: '/payment',
|
||||
query: { type: props.orderType, ids: String(order.id), amount },
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCancel(order: any) {
|
||||
try {
|
||||
if (isAttraction.value) {
|
||||
await cancelAttractionOrder({ id: order.id, cancelReason: '用户取消' })
|
||||
} else {
|
||||
await cancelProductOrder({ id: order.id, cancelReason: '用户取消' })
|
||||
}
|
||||
ElMessage.success('订单已取消')
|
||||
fetchOrders()
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReceive(order: any) {
|
||||
try {
|
||||
await receiveProductOrder(order.id)
|
||||
ElMessage.success('已确认收货')
|
||||
fetchOrders()
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrders() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (isAttraction.value) {
|
||||
const res: any = await getMyAttractionOrders({ current: 1, size: 100 })
|
||||
attractionOrders.value = res.data.records || []
|
||||
} else {
|
||||
const res: any = await getMyProductOrders({ current: 1, size: 100 })
|
||||
productOrders.value = res.data.records || []
|
||||
}
|
||||
} catch {
|
||||
// 错误提示由 axios 拦截器统一处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchOrders)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
svg { margin-bottom: 12px; }
|
||||
p { color: var(--text-muted); font-size: 14px; }
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-no { color: var(--text-muted); }
|
||||
.order-status { font-weight: 500;
|
||||
|
||||
&.unpaid { color: var(--cta); }
|
||||
&.paid, &.used, &.completed { color: var(--primary); }
|
||||
&.delivered, &.received { color: #0ea5e9; }
|
||||
&.pending { color: #64748b; }
|
||||
&.cancelled { color: #94a3b8; }
|
||||
&.refunding, &.refunded { color: #f59e0b; }}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.address-item {
|
||||
background: var(--bg);
|
||||
p { font-size: 13px; color: var(--text-muted); line-height: 1.7; }
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
h4 { font-size: 14px; font-weight: 500; }
|
||||
p { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
|
||||
}
|
||||
|
||||
.item-price { font-size: 14px; color: var(--text-light); white-space: nowrap; }
|
||||
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.order-total { font-size: 14px; strong { color: var(--cta); font-size: 16px; }}
|
||||
|
||||
.order-actions { display: flex; gap: 8px; }
|
||||
|
||||
.btn-action {
|
||||
background: linear-gradient(135deg, var(--cta), var(--cta-hover));
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
&:hover { box-shadow: 0 4px 16px rgba(249,115,22,0.3); }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<div class="container page-body">
|
||||
<aside class="profile-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a v-for="item in navItems" :key="item.path" class="nav-item" :class="{ active: currentNav === item.path }" @click="currentNav = item.path" @keydown.enter="currentNav = item.path" tabindex="0">
|
||||
<a v-for="item in navItems" :key="item.path" class="nav-item" :class="{ active: currentNav === item.path }" @click="handleNav(item)" @keydown.enter="handleNav(item)" tabindex="0">
|
||||
<span class="nav-icon" v-html="item.icon"></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</a>
|
||||
|
|
@ -127,6 +127,22 @@
|
|||
<router-link to="/user/travel/create" class="btn-primary">发布游记</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentNav === '/user/orders/attraction'" class="content-card">
|
||||
<div class="card-header">
|
||||
<h3>景点门票订单</h3>
|
||||
<span class="card-desc">查询你的门票订单</span>
|
||||
</div>
|
||||
<OrderPanel order-type="attraction" />
|
||||
</div>
|
||||
|
||||
<div v-if="currentNav === '/user/orders/product'" class="content-card">
|
||||
<div class="card-header">
|
||||
<h3>商品订单</h3>
|
||||
<span class="card-desc">查询你的商品订单</span>
|
||||
</div>
|
||||
<OrderPanel order-type="product" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -134,12 +150,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getTravelNoteList } from '@/api/travel'
|
||||
import OrderPanel from '@/views/user/OrderPanel.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const saving = ref(false)
|
||||
|
|
@ -149,8 +167,14 @@ const navItems = [
|
|||
{ path: '/user/profile', label: '个人资料', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>' },
|
||||
{ path: '/user/security', label: '账号安全', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>' },
|
||||
{ path: '/user/activity', label: '最近动态', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>' },
|
||||
{ path: '/user/orders/attraction', label: '景点门票订单', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 13c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3a3 3 0 013-3z"/><path d="M4 6h16v4.5a3 3 0 010 3V18a2 2 0 01-2 2H6a2 2 0 01-2-2v-4.5a3 3 0 010-3V6z"/></svg>' },
|
||||
{ path: '/user/orders/product', label: '商品订单', icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-4-2h-2a2 2 0 00-2 2v2"/></svg>' },
|
||||
]
|
||||
|
||||
function handleNav(item: any) {
|
||||
currentNav.value = item.path
|
||||
}
|
||||
|
||||
const genderOptions = [
|
||||
{ value: 0, label: '保密', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>' },
|
||||
{ value: 1, label: '男', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' },
|
||||
|
|
@ -223,6 +247,11 @@ onMounted(async () => {
|
|||
router.replace('/user/login')
|
||||
return
|
||||
}
|
||||
if (route.query.orders === 'attraction') {
|
||||
currentNav.value = '/user/orders/attraction'
|
||||
} else if (route.query.orders === 'product') {
|
||||
currentNav.value = '/user/orders/product'
|
||||
}
|
||||
try {
|
||||
await userStore.fetchUserInfo()
|
||||
initForm()
|
||||
|
|
|
|||
1937
tourism_platform.sql
1937
tourism_platform.sql
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue