467 lines
12 KiB
Vue
467 lines
12 KiB
Vue
<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>
|