petstore-frontend/src/pages/mine/MyOrders.vue

210 lines
7.7 KiB
Vue

<template>
<div class="page-shell orders-page">
<!-- 顶部导航 -->
<view class="orders-nav nav-gradient" :style="navSafeStyle">
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
<text class="nav-title">我的订单</text>
<view class="nav-placeholder"></view>
</view>
<view class="page-section orders-hero">
<view class="hero-title">服务进度一目了然</view>
<view v-if="userInfo.role === 'customer'" class="hero-sub">查看您的预约状态,随时掌握服务进度。</view>
<view v-else class="hero-sub">按状态查看订单,待确认可快速开始服务,进行中可直接填写报告。</view>
</view>
<!-- 状态 Tab -->
<view class="van-tabs">
<view
v-for="tab in statusTabs"
:key="tab.name"
:class="['van-tabs__tab', { active: currentStatus === tab.name }]"
@click="currentStatus = tab.name"
>{{ tab.title }}</view>
</view>
<!-- 订单列表 -->
<view class="page-section section-gap">
<div v-for="item in filteredOrders" :key="item.id" class="order-item" @click="goAppointmentDetail(item)">
<div class="order-head">
<div class="order-title">{{ item.title }}</div>
<view :class="`van-tag van-tag--${statusTagBg(item.status)}`">{{ item.statusText }}</view>
</div>
<div class="order-desc">
<span class="desc-icon"><AppIcon name="profile" :size="12" /></span>
<span>{{ item.desc }}</span>
</div>
<div class="order-footer">
<span class="order-time">
<AppIcon name="orders" :size="12" color="#94a3b8" />
<text>{{ item.time }}</text>
</span>
</div>
<div v-if="userInfo.role !== 'customer'">
<div v-if="item.status === 'new'" class="action-btns">
<button class="van-button van-button--small van-button--primary" @click.stop="startService(item)">开始服务</button>
<button class="van-button van-button--small" @click.stop="cancelService(item)">取消</button>
</div>
<button v-else-if="item.status === 'doing'" class="van-button van-button--small btn-mt" @click.stop="goReport(item)">填写报告</button>
</div>
<div v-else>
<div v-if="item.status === 'new'" class="action-btns">
<button class="van-button van-button--small" @click.stop="cancelService(item)">取消预约</button>
</div>
</div>
</div>
</view>
<view v-if="filteredOrders.length === 0" class="empty"><text>暂无数据</text></view>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import AppIcon from '../../components/AppIcon.vue'
import { navigateTo } from '../../utils/globalState.js'
import { getAppointmentList, startAppointment, cancelAppointment } from '../../api/index.js'
import { getUserSession } from '../../utils/session.js'
import { formatDateTimeCN } from '../../utils/datetime.js'
import { getAppointmentStatusText, getAppointmentTagType } from '../../utils/appointment.js'
const emit = defineEmits(['change-page'])
const userInfo = getUserSession()
const currentUserId = userInfo.id
const goBackToMine = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
return
}
navigateTo('mine')
}
const navSafeStyle = (() => {
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
let navHeight = statusBarHeight + 44
const menuRect = uni.getMenuButtonBoundingClientRect?.()
if (menuRect && menuRect.top && menuRect.height) {
const verticalGap = Math.max(menuRect.top - statusBarHeight, 4)
navHeight = statusBarHeight + verticalGap * 2 + menuRect.height
}
return `padding-top:${statusBarHeight}px;height:${navHeight}px;`
})()
const currentStatus = ref('new')
const orders = ref([])
const statusTabs = [
{ title: '待确认', name: 'new' },
{ title: '进行中', name: 'doing' },
{ title: '已完成', name: 'done' }
]
const filteredOrders = computed(() => orders.value.filter(o => {
if (currentStatus.value === 'new') return o.status === 'new'
if (currentStatus.value === 'doing') return o.status === 'doing'
if (currentStatus.value === 'done') return o.status === 'done' || o.status === 'cancel'
return true
}))
const statusTagBg = (status) => {
return getAppointmentTagType(status)
}
const fetchOrders = async () => {
if (!currentUserId) return
const res = await getAppointmentList(currentUserId, null, { page: 1, pageSize: 50 })
if (res.code === 200) {
orders.value = res.data.map(appt => ({
id: appt.id,
title: appt.serviceType || '洗澡美容预约',
desc: `${appt.petType || ''} - ${appt.petName || ''}`,
time: formatDateTimeCN(appt.appointmentTime),
status: appt.status || 'new',
statusText: getAppointmentStatusText(appt.status),
petName: appt.petName,
petType: appt.petType,
serviceType: appt.serviceType,
appointmentTime: appt.appointmentTime
}))
}
}
const startService = async (item) => {
const res = await startAppointment(item.id, userInfo.id)
if (res.code === 200) {
uni.showToast({ title: '已开始服务', icon: 'success' })
fetchOrders()
} else {
uni.showToast({ title: res.message || '操作失败', icon: 'none' })
}
}
const cancelService = async (item) => {
uni.showModal({
title: '提示',
content: '确定取消该预约?',
success: async (res) => {
if (!res.confirm) return
const r = await cancelAppointment(item.id)
if (r.code === 200) {
uni.showToast({ title: '已取消', icon: 'success' })
fetchOrders()
}
}
})
}
const goAppointmentDetail = (item) => {
if (!item?.id) return
uni.navigateTo({ url: `/pages/appointment/AppointmentDetail?id=${item.id}` })
}
const goReport = (item) => {
uni.setStorageSync('petstore_report_prefill', JSON.stringify({
appointmentId: item.id, petName: item.petName, serviceType: item.serviceType, appointmentTime: item.appointmentTime
}))
uni.navigateTo({ url: '/pages/report/Report' })
}
onMounted(() => fetchOrders())
</script>
<style scoped>
.orders-page { padding-bottom: 120rpx; }
.nav-placeholder { width: 32px; }
.btn-mt { margin-top: 10px; }
.orders-hero {
margin-top: 12px;
padding: 14px 16px;
border: 1px solid #dcefe3;
border-radius: 14px;
background: linear-gradient(135deg, #f3fff7 0%, #ecfbf3 100%);
}
.hero-title { font-size: 16px; font-weight: 700; color: #166534; }
.hero-sub { margin-top: 4px; font-size: 12px; color: #4b5563; line-height: 1.45; }
.orders-nav {
padding: 14px 16px;
display: flex; align-items: center; justify-content: space-between;
position: sticky; top: 0; z-index: 10;
}
.nav-back { font-size: 20px; color: #fff; }
.nav-title { font-size: 18px; font-weight: 700; color: #fff; }
.order-item { background: #fff; margin-bottom: 12px; border-radius: 14px; padding: 16px; border: 1px solid #e8edf4; box-shadow: 0 6px 16px rgba(15, 23, 42, 0.05); cursor: pointer; }
.order-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.order-title { font-size: 15px; font-weight: 700; color: #1f2937; margin-bottom: 6px; }
.order-desc { font-size: 13px; color: #6b7280; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
.desc-icon {
width: 18px;
height: 18px;
border-radius: 6px;
background: #eef2f7;
display: inline-flex;
align-items: center;
justify-content: center;
}
.order-footer { display: flex; justify-content: space-between; align-items: center; }
.order-time { font-size: 12px; color: #94a3b8; display: inline-flex; align-items: center; gap: 4px; }
.action-btns { display: flex; gap: 8px; margin-top: 10px; }
</style>