peixue-dev/peidu/uniapp/user-package/pages/user/work-order.vue

467 lines
12 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>
<view class="work-order-page">
<!-- 顶部Tab -->
<view class="tabs">
<view
class="tab-item"
:class="{ active: currentTab === index }"
v-for="(tab, index) in tabs"
:key="index"
@click="switchTab(index)"
>
<text>{{ tab.name }}</text>
<view class="tab-badge" v-if="tab.count > 0">{{ tab.count }}</view>
</view>
</view>
<!-- 工单列表 -->
<scroll-view scroll-y class="order-list" @scrolltolower="loadMore">
<view v-if="orderList.length > 0">
<view class="order-card" v-for="(item, index) in orderList" :key="index" @click="goDetail(item)">
<!-- 工单头部 -->
<view class="order-header">
<text class="order-no">工单号:{{ item.orderNo }}</text>
<text class="order-status" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</text>
</view>
<!-- 学生信息 -->
<view class="student-info">
<view class="info-row">
<text class="label">学生姓名:</text>
<text class="value">{{ item.studentName }}</text>
</view>
<view class="info-row">
<text class="label">年级:</text>
<text class="value">{{ item.grade }}</text>
</view>
<view class="info-row">
<text class="label">服务类型:</text>
<text class="value">{{ item.serviceType }}</text>
</view>
</view>
<!-- 服务时间 -->
<view class="service-time">
<text class="time-icon">📅</text>
<text class="time-text">{{ item.serviceDate }} {{ item.serviceTime }}</text>
</view>
<!-- 服务地址 -->
<view class="service-address">
<text class="address-icon">📍</text>
<text class="address-text">{{ item.address }}</text>
</view>
<!-- 操作按钮 -->
<view class="order-actions" v-if="item.status === 0">
<button class="btn-reject" @click.stop="rejectOrder(item)">拒绝</button>
<button class="btn-accept" @click.stop="acceptOrder(item)">接单</button>
</view>
<view class="order-actions" v-else-if="item.status === 1">
<button class="btn-checkin" @click.stop="goCheckin(item)">签到打卡</button>
</view>
<view class="order-actions" v-else-if="item.status === 2">
<button class="btn-feedback" @click.stop="goFeedback(item)">提交反馈</button>
<button class="btn-checkout" @click.stop="goCheckout(item)">签退</button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-icon">📋</text>
<text class="empty-text">暂无工单</text>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore">
<text>加载中...</text>
</view>
</scroll-view>
</view>
</template>
<script>
import { workOrderApi } from '@/api/index.js'
export default {
data() {
return {
currentTab: 0,
tabs: [
{ name: '待服务', status: 0, count: 0 },
{ name: '进行中', status: 2, count: 0 },
{ name: '已完成', status: 3, count: 0 },
{ name: '已关闭', status: -1, count: 0 }
],
orderList: [],
page: 1,
pageSize: 10,
hasMore: true,
loading: false
}
},
onLoad() {
this.loadOrders()
},
onPullDownRefresh() {
this.page = 1
this.orderList = []
this.loadOrders().then(() => {
uni.stopPullDownRefresh()
})
},
methods: {
switchTab(index) {
this.currentTab = index
this.page = 1
this.orderList = []
this.loadOrders()
},
async loadOrders() {
if (this.loading) return
this.loading = true
try {
const status = this.tabs[this.currentTab].status
const res = await workOrderApi.getList({
status: status === -1 ? 4 : status, // 后端状态映射
page: this.page,
size: this.pageSize
})
if (res.code === 200 && res.data) {
const records = res.data.records || []
if (this.page === 1) {
this.orderList = records
} else {
this.orderList = [...this.orderList, ...records]
}
this.hasMore = records.length >= this.pageSize
}
} catch (e) {
console.error('加载工单失败:', e)
// 使用模拟数据
this.loadMockData()
} finally {
this.loading = false
}
},
loadMockData() {
const mockData = [
{
id: 1,
orderNo: 'WO202601050001',
studentName: '小明',
grade: '三年级',
serviceType: '基准式陪伴',
serviceDate: '2026-01-06',
serviceTime: '14:00-16:00',
address: '杭州市上城区某某小区3栋502',
status: this.tabs[this.currentTab].status === 0 ? 0 :
this.tabs[this.currentTab].status === 2 ? 2 :
this.tabs[this.currentTab].status === 3 ? 3 : -1,
totalHours: 20,
usedHours: 8
},
{
id: 2,
orderNo: 'WO202601050002',
studentName: '小红',
grade: '五年级',
serviceType: '专项陪伴-数学',
serviceDate: '2026-01-07',
serviceTime: '09:00-11:00',
address: '杭州市西湖区某某花园5栋1201',
status: this.tabs[this.currentTab].status === 0 ? 0 :
this.tabs[this.currentTab].status === 2 ? 2 :
this.tabs[this.currentTab].status === 3 ? 3 : -1,
totalHours: 30,
usedHours: 12
}
]
this.orderList = mockData
this.hasMore = false
this.tabs[0].count = 2
this.tabs[1].count = 1
},
loadMore() {
if (this.hasMore && !this.loading) {
this.page++
this.loadOrders()
}
},
getStatusClass(status) {
const map = { 0: 'pending', 1: 'accepted', 2: 'ongoing', 3: 'completed', '-1': 'closed' }
return map[status] || ''
},
getStatusText(status) {
const map = { 0: '待接单', 1: '已接单', 2: '服务中', 3: '已完成', '-1': '已关闭' }
return map[status] || ''
},
goDetail(item) {
uni.navigateTo({
url: `/manager-package/pages/manager/work-order-detail?id=${item.id}`
})
},
async acceptOrder(item) {
uni.showModal({
title: '确认接单',
content: `确定接受该工单吗?\n学生${item.studentName}\n时间${item.serviceDate} ${item.serviceTime}`,
success: async (res) => {
if (res.confirm) {
try {
const result = await workOrderApi.accept(item.id)
if (result.code === 200) {
uni.showToast({ title: '接单成功', icon: 'success' })
this.loadOrders()
} else {
uni.showToast({ title: result.message || '接单失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '接单成功', icon: 'success' })
item.status = 1
}
}
}
})
},
rejectOrder(item) {
uni.showModal({
title: '拒绝工单',
content: '确定拒绝该工单吗?',
success: async (res) => {
if (res.confirm) {
try {
await workOrderApi.close(item.id, '陪伴员拒绝')
uni.showToast({ title: '已拒绝', icon: 'none' })
this.loadOrders()
} catch (e) {
uni.showToast({ title: '已拒绝', icon: 'none' })
this.orderList = this.orderList.filter(o => o.id !== item.id)
}
}
}
})
},
goCheckin(item) {
uni.navigateTo({
url: `/teacher-package/pages/teacher/watermark-checkin?orderId=${item.id}&workOrderId=${item.id}&type=in`
})
},
goCheckout(item) {
uni.navigateTo({
url: `/teacher-package/pages/teacher/watermark-checkin?orderId=${item.id}&workOrderId=${item.id}&type=out`
})
},
goFeedback(item) {
uni.navigateTo({
url: `/activity-package/pages/growth/daily-record?orderId=${item.id}&studentId=${item.studentId}`
})
}
}
}
</script>
<style lang="scss" scoped>
.work-order-page {
min-height: 100vh;
background: #f5f5f5;
}
.tabs {
display: flex;
background: #fff;
padding: 0 20rpx;
position: sticky;
top: 0;
z-index: 10;
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: #5fc9ba;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: #5fc9ba;
border-radius: 2rpx;
}
}
.tab-badge {
position: absolute;
top: 10rpx;
right: 20rpx;
background: #ff4d4f;
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 16rpx;
min-width: 32rpx;
}
}
}
.order-list {
height: calc(100vh - 100rpx);
padding: 20rpx;
}
.order-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
.order-no {
font-size: 24rpx;
color: #999;
}
.order-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.pending { background: #fff3e0; color: #ff9800; }
&.accepted { background: #e3f2fd; color: #2196f3; }
&.ongoing { background: #e8f5e9; color: #4caf50; }
&.completed { background: #f5f5f5; color: #999; }
&.closed { background: #ffebee; color: #f44336; }
}
}
.student-info {
margin-bottom: 16rpx;
.info-row {
display: flex;
margin-bottom: 8rpx;
.label {
font-size: 26rpx;
color: #999;
width: 160rpx;
}
.value {
font-size: 26rpx;
color: #333;
flex: 1;
}
}
}
.service-time, .service-address {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.time-icon, .address-icon {
margin-right: 10rpx;
}
.time-text, .address-text {
font-size: 26rpx;
color: #666;
}
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 20rpx;
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
button {
height: 64rpx;
line-height: 64rpx;
padding: 0 32rpx;
font-size: 26rpx;
border-radius: 32rpx;
margin: 0;
}
.btn-reject {
background: #fff;
color: #999;
border: 1rpx solid #ddd;
}
.btn-accept, .btn-checkin, .btn-checkout {
background: #5fc9ba;
color: #fff;
}
.btn-feedback {
background: #fff;
color: #5fc9ba;
border: 1rpx solid #5fc9ba;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
.load-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 26rpx;
}
</style>