瀏覽代碼

订单模块对接一半

Huanyi 2 周之前
父節點
當前提交
37218d6ec8

+ 8 - 0
api/archieves/customer.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request'
+
+export function getCustomer(id) {
+  return request({
+    url: '/archieves/customer/' + id,
+    method: 'get'
+  })
+}

+ 8 - 0
api/archieves/pet.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request'
+
+export function getPet(id) {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'get'
+  })
+}

+ 9 - 0
api/fulfiller/complaint.js

@@ -0,0 +1,9 @@
+import { request } from '@/utils/request'
+
+export function listComplaintByOrder(orderId) {
+  return request({
+    url: '/fulfiller/complaint/listByOrder',
+    method: 'get',
+    params: { orderId }
+  })
+}

+ 57 - 0
api/order/subOrder.js

@@ -0,0 +1,57 @@
+import { request } from '@/utils/request'
+
+export function listSubOrder(query) {
+  return request({
+    url: '/order/subOrder/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getSubOrderInfo(id) {
+  return request({
+    url: '/order/subOrder/getInfo',
+    method: 'get',
+    params: { id }
+  })
+}
+
+export function dispatchSubOrder(data) {
+  return request({
+    url: '/order/subOrder/dispatch',
+    method: 'post',
+    data: data
+  })
+}
+
+export function cancelSubOrder(data) {
+  return request({
+    url: '/order/subOrder/cancel',
+    method: 'post',
+    data: data
+  })
+}
+
+export function remarkSubOrder(data) {
+  return request({
+    url: '/order/subOrder/remark',
+    method: 'post',
+    data: data
+  })
+}
+
+export function confirmSubOrder(data) {
+  return request({
+    url: '/order/subOrder/confirm',
+    method: 'post',
+    data: data
+  })
+}
+
+export function nursingSummarySubOrder(data) {
+  return request({
+    url: '/order/subOrder/nursingSummary',
+    method: 'post',
+    data: data
+  })
+}

+ 13 - 0
api/order/subOrderLog.js

@@ -0,0 +1,13 @@
+import { request } from '@/utils/request'
+
+export function listSubOrderLog(query) {
+  return request({
+    url: '/order/subOrderLog/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function exportSubOrderLogUrl(orderId) {
+  return '/order/subOrderLog/export/' + orderId
+}

+ 8 - 0
api/service/classification.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request'
+
+export function listAll() {
+  return request({
+    url: '/service/classification/listAll',
+    method: 'get'
+  })
+}

+ 16 - 0
api/system/areaStation.js

@@ -0,0 +1,16 @@
+import { request } from '@/utils/request'
+
+export function listAreaStation(query) {
+  return request({
+    url: '/system/areaStation/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getAreaStation(id) {
+  return request({
+    url: `/system/areaStation/${id}`,
+    method: 'get'
+  })
+}

+ 2 - 1
pages/index/index.vue

@@ -72,7 +72,8 @@
 		</view>
 
 		<view class="recommend-list">
-			<view class="recommend-card" v-for="(item, index) in services" :key="index" @click="goToDetail(item.name)">
+			<view class="recommend-card" v-for="(item, index) in services.slice(0, 5)" :key="index"
+				@click="goToDetail(item.name)">
 				<image :src="item.iconUrl" class="item-img" mode="aspectFill"></image>
 				<view class="item-info">
 					<view class="item-header">

+ 6 - 4
pages/order/apply/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view class="order-apply-page">
+		<nav-bar title="下单预约"></nav-bar>
 		<view class="apply-content">
 			<!-- 01 服务类型 -->
 			<text class="section-title">01 服务类型</text>
@@ -146,10 +147,10 @@
 
 		<!-- 底部操作栏 -->
 		<view class="footer-bar safe-bottom">
-			<view class="quotation-price-box">
+			<view class="quotation-fulfillmentCommission-box">
 				<text class="p-label">总计报价:</text>
 				<text class="p-symbol">¥</text>
-				<text class="p-amount">{{ totalPrice }}</text>
+				<text class="p-amount">{{ totalFulfillmentCommission }}</text>
 			</view>
 			<button class="submit-btn" @click="onSubmit">立即下单</button>
 		</view>
@@ -180,6 +181,7 @@
 <script setup>
 import { ref, reactive, computed } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
+import navBar from '@/components/nav-bar/index.vue'
 
 const activeService = ref('transport')
 const showShopPicker = ref(false)
@@ -214,7 +216,7 @@ const formData = reactive({
 	quoteAmount: ''
 })
 
-const totalPrice = computed(() => {
+const totalFulfillmentCommission = computed(() => {
 	if (formData.quoteAmount && !isNaN(parseFloat(formData.quoteAmount))) return parseFloat(formData.quoteAmount).toFixed(2)
 	return '0.00'
 })
@@ -635,7 +637,7 @@ const onSubmit = () => {
 	z-index: 100;
 }
 
-.quotation-price-box {
+.quotation-fulfillmentCommission-box {
 	flex: 1;
 	display: flex;
 	align-items: baseline;

+ 292 - 91
pages/order/detail/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view class="order-detail-page">
+		<nav-bar title="订单详情"></nav-bar>
 		<!-- 订单号与状态 -->
 		<view class="order-header">
 			<view class="order-id-row">
@@ -83,7 +84,7 @@
 				</view>
 			</view>
 
-			<!-- 订单基础信息 -->
+			<!-- 任务详情扩展板块 -->
 			<view class="tab-content" v-if="activeTab === 'base'">
 				<view class="base-info-grid">
 					<view class="bi-item" v-for="item in baseInfoList" :key="item.label">
@@ -92,40 +93,42 @@
 					</view>
 				</view>
 
-				<template v-if="activeService === 'transport'">
+				<!-- 接送任务详情 -->
+				<block v-if="order.type === 'transport'">
 					<text class="sub-title">接送任务详情</text>
-					<view class="route-block">
-						<view class="route-header">
-							<text class="route-tag pick">宠物接送</text>
-							<text class="route-tag arrow">接</text>
-							<text class="route-time">{{ order.pickTime }}</text>
+					<view class="task-card transport-card">
+						<view class="task-header">
+							<text class="type-tag" :class="getTransportClass(order.subOrderType)">
+								{{ getTransportLabel(order.subOrderType) }}
+							</text>
+							<text class="task-time">{{ order.serviceTime }}</text>
 						</view>
-						<view class="route-addr">
-							<uni-icons type="location" size="14" color="#999"></uni-icons>
-							<text>{{ order.pickAddress }}</text>
+						<view class="task-body">
+							<view class="task-row">
+								<text class="task-label">起点</text>
+								<text class="task-value">{{ order.fromAddress || '获取中...' }}</text>
+							</view>
+							<view class="task-row">
+								<text class="task-label">终点</text>
+								<text class="task-value">{{ order.toAddress || '获取中...' }}</text>
+							</view>
+							<view class="task-row contact-row">
+								<text class="task-value">{{ order.userName }} — {{ order.userPhone }}</text>
+							</view>
 						</view>
 					</view>
-					<view class="route-block">
-						<view class="route-header">
-							<text class="route-tag send">宠物接送</text>
-							<text class="route-tag arrow send-arrow">送</text>
-							<text class="route-time">{{ order.sendTime }}</text>
-						</view>
-						<view class="route-addr">
-							<uni-icons type="location" size="14" color="#999"></uni-icons>
-							<text>{{ order.sendAddress }}</text>
-						</view>
-					</view>
-				</template>
-				<template v-else>
-					<text class="sub-title">上门服务地址</text>
-					<view class="route-block">
-						<view class="route-addr">
-							<uni-icons type="location" size="14" color="#999"></uni-icons>
-							<text>{{ order.address }}</text>
+				</block>
+
+				<!-- 上门服务执行要求 -->
+				<block v-if="['feeding', 'washing'].includes(order.type)">
+					<text class="sub-title">服务执行要求</text>
+					<view class="task-card req-card">
+						<view class="req-item">
+							<text class="req-label">服务地址</text>
+							<text class="req-value">{{ order.address }}</text>
 						</view>
 					</view>
-				</template>
+				</block>
 			</view>
 
 			<!-- 指派履约者 -->
@@ -189,11 +192,19 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue'
+import { ref, reactive, computed, watch } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
+import navBar from '@/components/nav-bar/index.vue'
+import { getSubOrderInfo } from '@/api/order/subOrder'
+import { getPet } from '@/api/archieves/pet'
+import { getCustomer } from '@/api/archieves/customer'
+import { listSubOrderLog } from '@/api/order/subOrderLog'
+import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 
 const activeTab = ref('base')
 const activeService = ref('transport')
+const orderId = ref('')
+const loading = ref(false)
 
 const tabList = [
 	{ title: '基础信息', name: 'base' },
@@ -207,38 +218,178 @@ const currentServiceName = computed(() => {
 	return map[activeService.value]
 })
 
-onLoad((options) => {
-	if (options.service) activeService.value = options.service
-	if (options.status) {
-		const statusTextMap = { wait_dispatch: '待派单', wait_accept: '待接单', serving: '服务中', done: '已完成', cancel: '已取消' }
-		order.statusKey = options.status
-		order.statusText = statusTextMap[options.status] || '服务中'
-	}
-	if (options.cancelTime) order.cancelTime = decodeURIComponent(options.cancelTime)
-})
-
 const order = reactive({
-	id: 'ORD202402048803',
+	id: '',
+	code: '',
 	statusKey: 'serving',
 	statusText: '服务中',
-	petName: 'Cookie',
-	petBreed: '柯基',
-	petAge: 2,
-	petWeight: 15,
-	userName: '王先生',
-	userPhone: '13612345678',
-	address: '北京市朝阳区 某小区5号楼2单元101',
-	shopName: '爱宠生活馆 (中关村店)',
-	createTime: '2024-02-04 12:00',
-	bookTime: '2024-02-04 18:00',
-	packageName: '新春特惠接送套餐',
-	remark: '暂无备注',
-	assigneeName: '张小美',
+	status: 2,
+	petName: '',
+	petBreed: '',
+	petAge: '',
+	petWeight: '',
+	petGender: '',
+	petVaccine: '',
+	petCharacter: '',
+	petHealth: '',
+	userName: '',
+	userPhone: '',
+	address: '',
+	shopName: '',
+	createTime: '',
+	bookTime: '',
+	packageName: '',
+	remark: '',
+	assigneeName: '',
 	cancelTime: '',
-	pickAddress: '北京市朝阳区三里屯SOHO 6号楼',
-	pickTime: '2024-02-04 09:30',
-	sendAddress: '北京市朝阳区某小区5号楼2单元101',
-	sendTime: '2024-02-04 18:00'
+	pickAddress: '',
+	pickTime: '',
+	sendAddress: '',
+	sendTime: '',
+	fromAddress: '',
+	toAddress: '',
+	type: 'transport', // 对应后端 transport/feeding/washing
+	subOrderType: 0,   // 接送子类型
+	service: '',
+	pet: '',
+	customer: '',
+	fulfiller: '',
+	fulfillerName: ''
+})
+
+const orderLogsData = ref([])
+const fulfillerLogsData = ref([])
+const complaintList = ref([])
+
+const loadOrderDetail = async (id) => {
+	if (!id) return
+	loading.value = true
+	try {
+		const res = await getSubOrderInfo(id)
+		console.log('订单详情返回:', res)
+		if (res) {
+			Object.assign(order, res)
+			order.code = res.code || res.id
+			order.status = res.status
+			order.statusKey = getStatusKey(res.status)
+			order.statusText = getStatusName(res.status)
+			order.bookTime = res.serviceTime || ''
+			order.shopName = res.storeName || ''
+			order.userName = res.customerName || ''
+			order.userPhone = res.contactPhoneNumber || ''
+			order.assigneeName = res.fulfillerName || ''
+			order.remark = res.remark || '暂无备注'
+			order.fromAddress = res.fromAddress || ''
+			order.toAddress = res.toAddress || ''
+			order.type = res.type || 'transport'
+			order.subOrderType = res.subOrderType
+
+			if (res.pet) await loadPetInfo(res.pet)
+			if (res.customer) await loadCustomerInfo(res.customer)
+			await loadOrderLogs(id)
+			await loadComplaints(id)
+		}
+	} catch (error) {
+		console.error('加载订单详情失败:', error)
+		uni.showToast({ title: '加载失败', icon: 'none' })
+	} finally {
+		loading.value = false
+	}
+}
+
+const loadPetInfo = async (petId) => {
+	try {
+		const res = await getPet(petId)
+		if (res) {
+			order.petName = res.name || order.petName
+			order.petBreed = res.breed || order.petBreed
+			order.petAge = res.age ? `${res.age}岁` : order.petAge
+			order.petWeight = res.weight ? `${res.weight}kg` : order.petWeight
+			order.petGender = res.gender || order.petGender
+			order.petVaccine = res.vaccineStatus || '未知'
+			order.petCharacter = res.personality || '温顺'
+			order.petHealth = res.healthStatus || '健康'
+		}
+	} catch (error) {
+		console.error('加载宠物信息失败:', error)
+	}
+}
+
+const loadCustomerInfo = async (customerId) => {
+	try {
+		const res = await getCustomer(customerId)
+		if (res) {
+			order.userName = res.name || order.userName
+			order.userPhone = res.phone || order.userPhone
+			order.address = res.address || order.address
+		}
+	} catch (error) {
+		console.error('加载客户信息失败:', error)
+	}
+}
+
+const loadOrderLogs = async (id) => {
+	try {
+		const res = await listSubOrderLog({ orderId: id })
+		const list = res || []
+		orderLogsData.value = list.filter(i => Number(i?.logType) === 0)
+		fulfillerLogsData.value = list.filter(i => Number(i?.logType) === 1)
+	} catch (error) {
+		console.error('加载订单日志失败:', error)
+		orderLogsData.value = []
+		fulfillerLogsData.value = []
+	}
+}
+
+const loadComplaints = async (id) => {
+	try {
+		const res = await listComplaintByOrder(id)
+		complaintList.value = res || []
+	} catch (error) {
+		console.error('加载投诉记录失败:', error)
+		complaintList.value = []
+	}
+}
+
+const getStatusKey = (status) => {
+	const map = { 0: 'wait_dispatch', 1: 'wait_accept', 2: 'serving', 3: 'confirming', 4: 'done', 5: 'cancel' }
+	return map[status] || 'serving'
+}
+
+const getStatusName = (status) => {
+	const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
+	return map[status] || '未知'
+}
+
+const getTransportLabel = (t) => {
+	if (t === 0 || t === '0') return '接'
+	if (t === 1 || t === '1') return '送'
+	if (t === 2 || t === '2') return '单程接'
+	if (t === 3 || t === '3') return '单程送'
+	return '接送'
+}
+
+const getTransportClass = (t) => {
+	if (t === 0 || t === '0' || t === 2 || t === '2') return 'tag-blue'
+	return 'tag-orange'
+}
+
+const getTypeName = (type) => {
+	const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+	return map[type] || '未知服务'
+}
+
+onLoad((options) => {
+	// 防御性校验:id 必须是有效值(非空、非字符串 "undefined")
+	if (options.id && options.id !== 'undefined') {
+		orderId.value = options.id
+		console.log('订单详情页:接收到的订单ID =', options.id)
+		loadOrderDetail(options.id)
+	} else {
+		console.error('订单详情页:缺少有效的订单ID,options =', options)
+		uni.showToast({ title: '订单ID无效', icon: 'none' })
+	}
+	if (options.service) activeService.value = options.service
 })
 
 const progressSteps = computed(() => {
@@ -652,13 +803,17 @@ const onCancelOrder = () => {
 	background: #f8f8f8;
 	border-radius: 16rpx;
 	padding: 16rpx 20rpx;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
 }
 
 .bi-label {
 	display: block;
 	font-size: 20rpx;
 	color: #aaa;
-	margin-bottom: 6rpx;
+	margin-bottom: 8rpx;
 }
 
 .bi-val {
@@ -666,6 +821,8 @@ const onCancelOrder = () => {
 	font-size: 24rpx;
 	color: #333;
 	font-weight: 500;
+	word-break: break-all;
+	line-height: 1.4;
 }
 
 .bi-val.highlight {
@@ -674,65 +831,109 @@ const onCancelOrder = () => {
 
 .sub-title {
 	display: block;
-	font-size: 26rpx;
-	font-weight: 700;
+	font-size: 28rpx;
+	font-weight: bold;
 	color: #333;
-	margin-bottom: 20rpx;
-	padding-left: 12rpx;
-	border-left: 6rpx solid #ff9500;
+	margin: 12rpx 0 24rpx;
 }
 
-.route-block {
-	background: #f9f9f9;
-	border-radius: 20rpx;
-	padding: 20rpx 24rpx;
+/* 任务卡片样式 */
+.task-card {
+	padding: 24rpx;
+	background: #fff;
+	border: 1rpx solid #f0f0f0;
+	border-radius: 12rpx;
 	margin-bottom: 20rpx;
+	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.02);
 }
 
-.route-header {
+.task-header {
 	display: flex;
 	align-items: center;
-	gap: 12rpx;
-	margin-bottom: 16rpx;
+	gap: 16rpx;
+	margin-bottom: 20rpx;
 }
 
-.route-tag {
+.type-tag {
 	font-size: 22rpx;
 	padding: 4rpx 12rpx;
-	border-radius: 8rpx;
+	border-radius: 6rpx;
+	border: 1rpx solid;
 }
 
-.route-tag.pick {
-	background: #e3f2fd;
-	color: #2196f3;
+.tag-blue {
+	color: #007aff;
+	background: #eef6ff;
+	border-color: #007aff;
 }
 
-.route-tag.send {
-	background: #e8f5e9;
-	color: #4caf50;
+.tag-orange {
+	color: #ff9500;
+	background: #fff8f0;
+	border-color: #ff9500;
 }
 
-.route-tag.arrow {
-	background: #2196f3;
-	color: #fff;
+.task-time {
+	font-size: 26rpx;
+	font-weight: bold;
+	color: #f56c6c;
 }
 
-.route-tag.send-arrow {
-	background: #4caf50;
+.task-body {
+	display: flex;
+	flex-direction: column;
+	gap: 12rpx;
 }
 
-.route-time {
-	font-size: 22rpx;
-	color: #ff9500;
-	margin-left: auto;
+.task-row {
+	display: flex;
+	align-items: flex-start;
+	gap: 20rpx;
+}
+
+.task-label {
+	width: 60rpx;
+	font-size: 24rpx;
+	color: #999;
+	flex-shrink: 0;
+}
+
+.task-value {
+	font-size: 26rpx;
+	color: #333;
+	line-height: 1.4;
+}
+
+.contact-row {
+	margin-top: 8rpx;
+	padding-top: 8rpx;
+}
+
+.contact-row .task-value {
+	color: #999;
+}
+
+/* 执行要求卡片 */
+.req-card {
+	background: #f9f9f9;
+	border: none;
 }
 
-.route-addr {
+.req-item {
 	display: flex;
 	align-items: center;
-	gap: 8rpx;
+}
+
+.req-label {
+	width: 140rpx;
 	font-size: 24rpx;
-	color: #555;
+	color: #333;
+}
+
+.req-value {
+	flex: 1;
+	font-size: 26rpx;
+	color: #333;
 }
 
 .assignee-card {

+ 212 - 53
pages/order/list/index.vue

@@ -23,14 +23,14 @@
 				<view class="search-wrap">
 					<uni-icons type="search" size="14" color="#999"></uni-icons>
 					<input class="search-input" v-model="searchValue" placeholder="订单号/商户/宠主/手机号"
-						placeholder-class="placeholder-style" />
+						placeholder-class="placeholder-style" @confirm="onSearch" />
 				</view>
 			</view>
 		</view>
 
 		<!-- 订单列表内容 -->
 		<view class="list-container">
-			<view class="order-card" v-for="order in filteredOrders" :key="order.id" @click="goToDetail(order)">
+			<view class="order-card" v-for="order in orders" :key="order.id" @click="goToDetail(order)">
 				<!-- 头部:订单号与状态 -->
 				<view class="order-head">
 					<text class="order-no">{{ order.id }}</text>
@@ -110,9 +110,14 @@
 			</view>
 
 			<!-- 空状态 -->
-			<view class="empty-state" v-if="filteredOrders.length === 0">
+			<view class="empty-state" v-if="!loading && orders.length === 0">
 				<text class="empty-text">暂无相关订单</text>
 			</view>
+
+			<!-- 加载状态 -->
+			<view class="loading-state" v-if="loading">
+				<text class="loading-text">加载中...</text>
+			</view>
 		</view>
 
 		<custom-tabbar></custom-tabbar>
@@ -120,18 +125,30 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
 import customTabbar from '@/components/custom-tabbar/index.vue'
-import orderMockData from '@/mock/order.json'
 import orderStatusData from '@/json/orderStatus.json'
+import { listAll } from '@/api/service/list'
+import { listSubOrder, cancelSubOrder } from '@/api/order/subOrder'
+import { listAreaStation } from '@/api/system/areaStation'
+
+// 加载状态
+const loading = ref(false)
 
 // 筛选与搜索
 const activeStatus = ref(-1) // -1 表示全部,其他值为枚举值
 const filterType = ref(0)
 const searchValue = ref('')
 
+// 分页参数
+const pagination = ref({
+	current: 1,
+	size: 10,
+	total: 0
+})
+
 // 从 orderStatus.json 生成 tabList
 const tabList = ref([
 	{ title: '全部订单', value: -1 },
@@ -142,25 +159,60 @@ const tabList = ref([
 	}))
 ])
 
-const typeOptions = [
-	{ text: '全部类型', value: 0 },
-	{ text: '宠物接送', value: 1 },
-	{ text: '上门喂遛', value: 2 },
-	{ text: '上门洗护', value: 3 }
-]
-
-const currentTypeName = computed(() => {
-	return typeOptions[filterType.value].text
-})
+const typeOptions = ref([{ text: '全部类型', value: 0 }])
+const serviceList = ref([])
+const areaStationList = ref([])
+const areaStationMap = ref({})
+
+// 加载服务类型列表
+const loadServiceTypes = async () => {
+	try {
+		const services = await listAll()
+		if (services && services.length > 0) {
+			serviceList.value = services
+			const serviceTypes = services.map((service, index) => ({
+				text: service.name,
+				value: index + 1,
+				id: service.id
+			}))
+			typeOptions.value = [
+				{ text: '全部类型', value: 0 },
+				...serviceTypes
+			]
+		}
+	} catch (error) {
+		console.error('加载服务类型失败:', error)
+	}
+}
 
-// 从 URL 参数初始化状态
-onLoad((options) => {
-	if (options.status !== undefined) {
-		const statusValue = parseInt(options.status)
-		if (!isNaN(statusValue)) {
-			activeStatus.value = statusValue
+// 加载区域站点列表
+const loadAreaStations = async () => {
+	try {
+		const res = await listAreaStation()
+		if (res && res.data) {
+			areaStationList.value = res.data
+			const map = {}
+			for (const item of res.data) {
+				if (item && item.id !== undefined && item.id !== null) {
+					map[item.id] = item
+				}
+			}
+			areaStationMap.value = map
 		}
+	} catch (error) {
+		console.error('加载区域站点失败:', error)
 	}
+}
+
+onMounted(() => {
+	loadServiceTypes()
+	loadAreaStations()
+	loadOrders()
+})
+
+const currentTypeName = computed(() => {
+	const option = typeOptions.value.find(opt => opt.value === filterType.value)
+	return option ? option.text : '全部类型'
 })
 
 // 根据枚举值获取状态信息
@@ -168,62 +220,159 @@ const getStatusInfo = (value) => {
 	return orderStatusData.find(item => item.value === value)
 }
 
+// 根据服务ID获取服务名称
+const getServiceName = (serviceId) => {
+	const service = serviceList.value.find(s => s.id === serviceId)
+	return service ? service.name : '未知服务'
+}
+
+// 获取城市/区域文本
+const getCityDistrictText = (row) => {
+	if (!row || !row.site) return ''
+	const station = areaStationMap.value[row.site]
+	if (!station) return ''
+
+	const parent = station.parentId ? areaStationMap.value[station.parentId] : undefined
+	if (!parent) return station.name || ''
+
+	if (parent.type === 0) return parent.name || ''
+	if (parent.type === 1) {
+		const city = parent.parentId ? areaStationMap.value[parent.parentId] : undefined
+		return `${city?.name || ''}/${parent.name || ''}`
+	}
+	return parent.name || ''
+}
+
+// 获取服务模式标签
+const getServiceModeTag = (row) => {
+	const t = row?.type
+	if (t === 0 || t === '0' || t === 1 || t === '1') return '往返'
+	return ''
+}
+
+// 获取服务订单类型标签
+const getServiceOrderTypeTag = (row) => {
+	const t = row?.type
+	if (t === 0 || t === '0') return { label: '接', type: 'blue' }
+	if (t === 1 || t === '1') return { label: '送', type: 'green' }
+	if (t === 2 || t === '2') return { label: '单程接', type: 'blue' }
+	if (t === 3 || t === '3') return { label: '单程送', type: 'green' }
+	return null
+}
+
 const onTabClick = (value) => {
 	activeStatus.value = value
+	pagination.value.current = 1
+	loadOrders()
 }
 
 const onTypeChange = (e) => {
-	filterType.value = Number(e.detail.value)
-}
+	const index = Number(e.detail.value)
+	filterType.value = index
+	pagination.value.current = 1
+	loadOrders()
+}
+
+// 订单列表数据
+const orders = ref([])
+
+// 加载订单列表
+const loadOrders = async () => {
+	loading.value = true
+	try {
+		const selectedType = typeOptions.value.find(opt => opt.value === filterType.value)
+		const params = {
+			pageNum: pagination.value.current,
+			pageSize: pagination.value.size,
+			status: activeStatus.value !== -1 ? activeStatus.value : undefined,
+			service: selectedType && selectedType.id ? selectedType.id : undefined,
+			content: searchValue.value || undefined
+		}
 
-// 搜索与过滤后的订单列表
-const filteredOrders = computed(() => {
-	return orders.value.filter(order => {
-		let statusMatch = true
-		if (activeStatus.value !== -1) {
-			const statusInfo = getStatusInfo(activeStatus.value)
-			statusMatch = statusInfo && order.statusText === statusInfo.label
+		const res = await listSubOrder(params)
+		console.log('后端返回数据:', res)
+		if (res) {
+			const rows = res.rows || []
+			console.log('rows:', rows)
+			orders.value = rows.map(row => transformOrder(row))
+			console.log('转换后的orders:', orders.value)
+			pagination.value.total = res.total || 0
 		}
-		const typeMap = { 1: '宠物接送', 2: '上门喂遛', 3: '上门洗护' }
-		const typeMatch = filterType.value === 0 || order.serviceType === typeMap[filterType.value]
-		const searchLower = searchValue.value.toLowerCase()
-		const searchMatch = !searchValue.value ||
-			order.id.toLowerCase().includes(searchLower) ||
-			order.userName.toLowerCase().includes(searchLower) ||
-			order.petName.toLowerCase().includes(searchLower) ||
-			order.userPhone.includes(searchLower)
-		return statusMatch && typeMatch && searchMatch
-	})
-})
+	} catch (error) {
+		console.error('加载订单列表失败:', error)
+	} finally {
+		loading.value = false
+	}
+}
 
-const orders = ref(orderMockData)
+// 转换订单数据格式
+const transformOrder = (row) => {
+	const statusInfo = getStatusInfo(row.status)
+	const serviceName = getServiceName(row.service)
+	const modeTag = getServiceModeTag(row)
+	const typeTag = getServiceOrderTypeTag(row)
+
+	const serviceTags = []
+	if (modeTag) serviceTags.push(modeTag)
+	if (typeTag) serviceTags.push(typeTag.label)
+
+	return {
+		// 先展开原始字段,后面的手动赋值具有更高优先级
+		...row,
+		// 显示用的单号(优先用业务编号 code,否则用数据库 ID)
+		id: row.code || row.id,
+		rawId: row.id,
+		serviceType: serviceName,
+		serviceTags: serviceTags,
+		petName: row.petName || '未知',
+		petBreed: row.petBreed || '未知',
+		userName: row.customerName || '未知',
+		address: row.toAddress || row.fromAddress || getCityDistrictText(row),
+		shopName: row.storeName || '未知',
+		userPhone: row.contactPhoneNumber || '',
+		bookTime: row.serviceTime || '',
+		createTime: row.createTime || '',
+		statusText: statusInfo ? statusInfo.label : '未知',
+		statusClass: statusInfo ? `text-${statusInfo.color.replace('#', '')}` : 'text-gray',
+		assigneeName: row.fulfillerName || '',
+		cancelTime: row.cancelTime || ''
+	}
+}
+
+// 搜索订单
+const onSearch = () => {
+	pagination.value.current = 1
+	loadOrders()
+}
 
+// 取消订单
 const onCancelOrder = (order) => {
 	uni.showModal({
 		title: '提示',
 		content: `确定要取消订单 [${order.id}] 吗?`,
-		success: (res) => {
+		success: async (res) => {
 			if (res.confirm) {
-				uni.showToast({ title: '订单已取消', icon: 'success' })
-				order.statusText = '已取消'
-				order.statusClass = 'text-gray'
-				activeStatus.value = 5
+				try {
+					await cancelSubOrder({ orderId: order.rawId })
+					uni.showToast({ title: '订单已取消', icon: 'success' })
+					loadOrders()
+				} catch (error) {
+					console.error('取消订单失败:', error)
+					uni.showToast({ title: '取消失败', icon: 'none' })
+				}
 			}
 		}
 	})
 }
 
+// 跳转到订单详情
 const goToDetail = (order) => {
-	const serviceKey = order.serviceType === '宠物接送' ? 'transport' : (order.serviceType === '上门喂遛' ? 'feed' : 'wash')
-	// 根据 statusText 查找对应的枚举值
-	const statusInfo = orderStatusData.find(item => item.label === order.statusText)
-	const statusValue = statusInfo ? statusInfo.value : 3
-	const cancelTime = order.cancelTime ? encodeURIComponent(order.cancelTime) : ''
 	uni.navigateTo({
-		url: `/pages/order/detail/index?service=${serviceKey}&status=${statusValue}${cancelTime ? '&cancelTime=' + cancelTime : ''}`
+		url: `/pages/order/detail/index?id=${order.rawId}`
 	})
 }
 
+// 投诉
 const onComplaint = (order) => {
 	uni.navigateTo({
 		url: `/pages/my/complaint-submit/index?orderId=${order.id}`
@@ -550,4 +699,14 @@ const onComplaint = (order) => {
 	font-size: 28rpx;
 	color: #999;
 }
+
+.loading-state {
+	text-align: center;
+	padding: 100rpx 0;
+}
+
+.loading-text {
+	font-size: 28rpx;
+	color: #999;
+}
 </style>

+ 52 - 72
pages/service/all/index.vue

@@ -3,10 +3,6 @@
 		<nav-bar title="全部分类" :showBack="false"></nav-bar>
 		<!-- 顶部搜索栏 -->
 		<view class="header-search">
-			<view class="location-box">
-				<text>昆明市</text>
-				<uni-icons type="bottom" size="12" color="#333"></uni-icons>
-			</view>
 			<view class="search-input-wrap">
 				<uni-icons type="search" size="14" color="#999"></uni-icons>
 				<input class="search-input" v-model="searchValue" placeholder="搜索服务内容"
@@ -27,7 +23,6 @@
 			<!-- 右侧服务列表 -->
 			<scroll-view scroll-y class="content-view">
 				<view class="category-section" v-for="(cat, index) in currentCategories" :key="index">
-					<text class="cat-section-title">{{ cat.name }}</text>
 					<view class="service-grid">
 						<view class="service-cell" v-for="(service, sIndex) in cat.items" :key="sIndex"
 							@click="onServiceClick(service)">
@@ -39,13 +34,6 @@
 					</view>
 				</view>
 
-				<!-- 底部客服提示 -->
-				<view class="contact-footer">
-					<image src="https://img.icons8.com/?size=100&id=108639&format=png" class="kf-avatar"
-						mode="aspectFit"></image>
-					<text class="footer-msg">没有找到想要的服务?可以联系客服哟~</text>
-					<button size="mini" class="kf-btn">联系客服 ></button>
-				</view>
 			</scroll-view>
 		</view>
 
@@ -54,19 +42,65 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import navBar from '@/components/nav-bar/index.vue'
 import customTabbar from '@/components/custom-tabbar/index.vue'
-import servicesMockData from '@/mock/services.json'
+import { listAll as getClassifications } from '@/api/service/classification'
+import { listAll as getServices } from '@/api/service/list'
 
 const searchValue = ref('')
 const activeSidebar = ref(0)
-const allData = servicesMockData
-const currentCategories = computed(() => allData[activeSidebar.value].categories)
+const allData = ref([])
+const currentCategories = computed(() => {
+	if (allData.value.length === 0 || !allData.value[activeSidebar.value]) return []
+	return allData.value[activeSidebar.value].categories
+})
+
+const loadData = async () => {
+	try {
+		const [classifications, services] = await Promise.all([
+			getClassifications(),
+			getServices()
+		])
+
+		console.log('分类数据:', classifications)
+		console.log('服务数据:', services)
+
+		allData.value = classifications.map(classification => {
+			const categoryServices = services.filter(
+				service => service.classificationId === classification.id
+			)
+
+			console.log(`分类 ${classification.name} 的服务:`, categoryServices)
+
+			return {
+				title: classification.name,
+				categories: [{
+					items: categoryServices.map(service => ({
+						...service,
+						name: service.name,
+						icon: service.iconUrl,
+						type: service.id
+					}))
+				}]
+			}
+		})
+
+		console.log('最终数据结构:', allData.value)
+	} catch (error) {
+		console.error('加载服务数据失败:', error)
+		uni.showToast({ title: '加载失败', icon: 'none' })
+	}
+}
+
+onMounted(() => {
+	loadData()
+})
 
 const onServiceClick = (service) => {
-	if (service.type) {
-		uni.navigateTo({ url: `/pages/service/detail/index?service=${service.type}` })
+	if (service.id) {
+		uni.setStorageSync('currentService', service)
+		uni.navigateTo({ url: `/pages/service/detail/index?serviceId=${service.id}` })
 	} else {
 		uni.showToast({ title: service.name + ' 功能即将上线', icon: 'none' })
 	}
@@ -82,22 +116,11 @@ const onServiceClick = (service) => {
 }
 
 .header-search {
-	display: flex;
-	align-items: center;
 	padding: 16rpx 32rpx;
 	background-color: #fff;
 	border-bottom: 1rpx solid #f2f2f2;
 }
 
-.location-box {
-	display: flex;
-	align-items: center;
-	gap: 8rpx;
-	font-size: 28rpx;
-	color: #333;
-	margin-right: 24rpx;
-}
-
 .search-input-wrap {
 	flex: 1;
 	display: flex;
@@ -164,17 +187,6 @@ const onServiceClick = (service) => {
 	margin-bottom: 48rpx;
 }
 
-.cat-section-title {
-	font-size: 28rpx;
-	font-weight: bold;
-	color: #333;
-	display: block;
-	margin-bottom: 24rpx;
-	background-color: #f9f9f9;
-	padding: 8rpx 20rpx;
-	border-radius: 8rpx;
-}
-
 .service-grid {
 	display: flex;
 	flex-wrap: wrap;
@@ -210,36 +222,4 @@ const onServiceClick = (service) => {
 	color: #666;
 	text-align: center;
 }
-
-.contact-footer {
-	margin-top: 40rpx;
-	background-color: #f0f7ff;
-	border-radius: 24rpx;
-	padding: 32rpx;
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	text-align: center;
-	gap: 20rpx;
-}
-
-.kf-avatar {
-	width: 80rpx;
-	height: 80rpx;
-}
-
-.footer-msg {
-	font-size: 26rpx;
-	color: #333;
-}
-
-.kf-btn {
-	font-size: 24rpx;
-	color: #1989fa;
-	background: transparent;
-	border: 1rpx solid #1989fa;
-	border-radius: 28rpx;
-	padding: 8rpx 32rpx;
-	line-height: 1.5;
-}
 </style>

+ 39 - 63
pages/service/detail/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<view class="service-detail-page">
-		<nav-bar title="服务详情" bgColor="transparent" color="#fff"></nav-bar>
+		<nav-bar title="服务详情"></nav-bar>
 		<!-- 顶部主图区域 -->
 		<view class="hero-section">
 			<image :src="serviceData.heroImg" class="hero-img" mode="aspectFill"></image>
@@ -20,7 +20,6 @@
 					<text class="price-num">{{ serviceData.price }}</text>
 					<text class="price-unit">{{ serviceData.unit }}</text>
 				</view>
-				<text class="bought-count">{{ serviceData.booked }}</text>
 			</view>
 			<text class="service-name-text">{{ serviceData.title }}</text>
 		</view>
@@ -64,72 +63,50 @@ import navBar from '@/components/nav-bar/index.vue'
 import { onLoad } from '@dcloudio/uni-app'
 
 const activeTab = ref('intro')
-const serviceKey = ref('接送')
-
-const serviceConfigs = {
-	'接送': {
-		type: 'transport',
-		title: '宠物接送',
-		heroTitle: '专业宠物托运',
-		heroSubTitle: '专业速度快 安全服务好',
-		heroImg: 'https://images.unsplash.com/photo-1544568100-847a948585b9?q=80&w=600&auto=format&fit=crop',
-		price: '40',
-		unit: '起',
-		booked: '158+ 人已约',
-		intro: '宠物接送对于有出行需求的宠物主们来说,是解决距离难题的最佳选择。<br><br>✨ <b>服务特色:</b><br>专车专送、不拼车、无应激,配有专业航空箱,全天候接单响应。',
-		introImages: ['/static/images/transport-guide.png'],
-		notice: '<div style="background:#fdfaf5;border:1px solid #f9ecec;border-radius:10px;padding:14px;margin-bottom:16px;"><b>📜 服务细节</b><br>1. 请提前至少一天预约出行时间。<br>2. 接送期间发送状态视频及定位。<br>3. 价格按实际公里数计算。</div><div style="background:#fdfaf5;border:1px solid #f9ecec;border-radius:10px;padding:14px;"><b>⚠️ 禁忌项</b><br>1. 孕期宠物严禁长途颠簸。<br>2. 传染病宠物谢绝托运。<br>3. 出发前4小时内请勿大量喂食。</div>'
-	},
-	'托运': {
-		type: 'transport',
-		title: '跨区托运',
-		heroTitle: '跨区宠物托运',
-		heroSubTitle: '全国直达 安全高效',
-		heroImg: 'https://images.unsplash.com/photo-1544568100-847a948585b9?q=80&w=600&auto=format&fit=crop',
-		price: '199',
-		unit: '起',
-		booked: '286+ 人已约',
-		intro: '宠物长途托运包含汽车、航空等多种方式可选。一站式服务。<br><br>✨ <b>服务特色:</b><br>恒温车厢保障、到达后专属客服报平安。',
-		introImages: ['/static/images/transport-guide.png'],
-		notice: '<div style="background:#fdfaf5;border:1px solid #f9ecec;border-radius:10px;padding:14px;margin-bottom:16px;"><b>📜 服务细节</b><br>1. 长途托运需提前3天预约。<br>2. 提供加固航空箱。<br>3. 客服1对1跟踪行程。</div>'
-	},
-	'喂养': {
-		type: 'feed',
-		title: '上门喂遛',
-		heroTitle: '贴心上门喂遛',
-		heroSubTitle: '解放双手 贴心陪伴',
-		heroImg: 'https://images.unsplash.com/photo-1548247416-ec66f4900b2e?q=80&w=600&auto=format&fit=crop',
-		price: '35',
-		unit: ' / 次起',
-		booked: '1240+ 人已约',
-		intro: '专业宠托师上门喂食、铲屎及陪玩/遛狗服务。<br><br>✨ <b>服务特色:</b><br>每次服务30~60分钟,提供手机直播或录像。',
-		introImages: ['https://images.unsplash.com/photo-1583511655857-d19b40a7a54e?w=600&q=80'],
-		notice: '<div style="background:#fdfaf5;border:1px solid #f9ecec;border-radius:10px;padding:14px;"><b>📜 服务细节</b><br>1. 确认入户方式。<br>2. 离场提供照片及视频汇报单。</div>'
-	},
-	'洗护': {
-		type: 'wash',
-		title: '上门洗护SPA',
-		heroTitle: '高端上门洗护',
-		heroSubTitle: '足不出户 尊享SPA',
-		heroImg: 'https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?q=80&w=600&auto=format&fit=crop',
-		price: '98',
-		unit: ' / 次起',
-		booked: '842+ 人已约',
-		intro: '宠物美容师带全套设备上门,提供洗沐、剪甲、清耳道等全方位护理。<br><br>✨ <b>服务特色:</b><br>纯手工精细揉搓洗浴,低敏无泪植物配方。',
-		introImages: ['https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?w=600&q=80'],
-		notice: '<div style="background:#fdfaf5;border:1px solid #f9ecec;border-radius:10px;padding:14px;"><b>📜 服务细节</b><br>1. 请确保家中有热水及洗浴空间。<br>2. 费用依宠物体型及毛长而定。</div>'
-	}
-}
+const serviceInfo = ref(null)
+
+const defaultHeroImg = 'https://images.unsplash.com/photo-1544568100-847a948585b9?q=80&w=600&auto=format&fit=crop'
 
 onLoad((options) => {
-	if (options && options.service) {
-		serviceKey.value = decodeURIComponent(options.service)
+	const storedService = uni.getStorageSync('currentService')
+	if (storedService) {
+		serviceInfo.value = storedService
+		console.log('获取到的服务数据:', storedService)
 	}
 })
 
 const serviceData = computed(() => {
-	const key = Object.keys(serviceConfigs).find(k => serviceKey.value.includes(k)) || '接送'
-	return serviceConfigs[key]
+	if (!serviceInfo.value) {
+		return {
+			heroImg: defaultHeroImg,
+			heroTitle: '服务详情',
+			heroSubTitle: '加载中...',
+			price: '0',
+			unit: '',
+			booked: '0 人已约',
+			title: '服务名称',
+			intro: '加载中...',
+			notice: '加载中...',
+			introImages: []
+		}
+	}
+
+	const service = serviceInfo.value
+	const priceInYuan = service.price ? (service.price / 100).toFixed(2) : '0.00'
+
+	return {
+		type: service.id,
+		title: service.name,
+		heroTitle: service.name,
+		heroSubTitle: service.remark || '专业服务',
+		heroImg: service.iconUrl || defaultHeroImg,
+		price: priceInYuan,
+		unit: '',
+		booked: '0 人已约',
+		intro: service.introduction || '暂无介绍',
+		introImages: [],
+		notice: service.orderInstruction || '暂无须知'
+	}
 })
 
 const goToOrderApply = () => {
@@ -151,7 +128,6 @@ const goToOrderApply = () => {
 	width: 100%;
 	height: 640rpx;
 	overflow: hidden;
-	margin-top: calc(-44px - var(--status-bar-height, 44px));
 }
 
 .hero-img {

+ 8 - 4
utils/request.js

@@ -3,10 +3,14 @@ import { BASE_URL, TIMEOUT, DEFAULT_HEADERS } from './config'
 const request = (options = {}) => {
 	return new Promise((resolve, reject) => {
 		const token = uni.getStorageSync('token') || ''
+		const method = (options.method || 'GET').toUpperCase()
+		const useParams = method === 'GET' || method === 'DELETE'
+		const requestData = useParams ? (options.params || options.data || {}) : (options.data || {})
+		
 		uni.request({
 			url: BASE_URL + options.url,
-			method: options.method || 'GET',
-			data: options.data || {},
+			method: method,
+			data: requestData,
 			header: {
 				...DEFAULT_HEADERS,
 				'Content-Type': 'application/json',
@@ -25,10 +29,10 @@ const request = (options = {}) => {
 				} else if (code === 401) {
 					uni.removeStorageSync('token')
 					uni.reLaunch({ url: '/pages/login/index' })
-					reject(res.data)
+					reject(res.msg)
 				} else {
 					uni.showToast({ title: msg || '操作失败', icon: 'none' })
-					reject(res.data)
+					reject(res.msg)
 				}
 			},
 			fail: (err) => {