210 lines
7.7 KiB
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>
|