Huanyi 21 цаг өмнө
parent
commit
394c7b2267
46 өөрчлөгдсөн 1537 нэмэгдсэн , 476 устгасан
  1. 12 0
      api/order/subOrderAppeal.js
  2. 22 25
      api/system/oss.js
  3. 22 23
      api/system/user.js
  4. 26 3
      components/center-select/index.vue
  5. 261 0
      components/page-select/index.vue
  6. 3 1
      json/orderStatus.json
  7. 5 2
      manifest.json
  8. 84 1
      pages/index/index.vue
  9. 9 7
      pages/login/index.vue
  10. 270 135
      pages/my/complaint/submit/index.vue
  11. 51 16
      pages/my/index.vue
  12. 109 1
      pages/my/user/add/index.vue
  13. 126 1
      pages/my/user/edit/index.vue
  14. 59 25
      pages/order/apply/index.vue
  15. 397 223
      pages/order/detail/index.vue
  16. 1 1
      pages/order/list/index.vue
  17. 9 9
      pages/service/all/index.vue
  18. 69 2
      pages/service/detail/index.vue
  19. BIN
      static/images/index-hand.png
  20. BIN
      static/images/index-header.png
  21. BIN
      static/images/index-middle.png
  22. BIN
      static/images/index-symbol.png
  23. BIN
      unpackage/cache/apk/__UNI__F19BBAD_cm.apk
  24. 1 1
      unpackage/cache/apk/apkurl
  25. 0 0
      unpackage/cache/apk/cmManifestCache.json
  26. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/app-service.js
  27. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/manifest.json
  28. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/index/index.css
  29. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/login/index.css
  30. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/submit/index.css
  31. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/index.css
  32. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/add/index.css
  33. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/edit/index.css
  34. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/add/index.css
  35. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/edit/index.css
  36. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/apply/index.css
  37. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/detail/index.css
  38. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/list/index.css
  39. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/service/all/index.css
  40. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/service/detail/index.css
  41. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-hand.png
  42. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-header.png
  43. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-middle.png
  44. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-symbol.png
  45. BIN
      unpackage/release/apk/__UNI__F19BBAD__20260427180258.apk
  46. 1 0
      utils/config.js

+ 12 - 0
api/order/subOrderAppeal.js

@@ -0,0 +1,12 @@
+import { request } from '@/utils/request'
+
+/**
+ * 根据订单ID查询服务变更记录列表
+ * @param orderId 订单ID
+ */
+export function listSubOrderAppealByOrderId(orderId) {
+  return request({
+    url: '/order/subOrderAppeal/listByOrderId/' + orderId,
+    method: 'get'
+  })
+}

+ 22 - 25
api/system/oss.js

@@ -1,34 +1,31 @@
 import { request } from '@/utils/request'
+import { BASE_URL, DEFAULT_HEADERS } from '@/utils/config'
 
 // 上传文件到 OSS
-// @Author: Antigravity
 export function uploadFile(filePath) {
   return new Promise((resolve, reject) => {
     const token = uni.getStorageSync('token') || ''
-    // 使用 import() 动态获取配置,避免循环依赖或加载顺序问题
-    import('@/utils/config').then(({ BASE_URL, DEFAULT_HEADERS }) => {
-      uni.uploadFile({
-        url: BASE_URL + '/resource/oss/upload',
-        filePath: filePath,
-        name: 'file',
-        header: {
-          'Authorization': token ? `Bearer ${token}` : '',
-          ...DEFAULT_HEADERS
-        },
-        success: (res) => {
-          const resData = JSON.parse(res.data)
-          if (resData.code === 200) {
-            resolve(resData.data)
-          } else {
-            uni.showToast({ title: resData.msg || '上传失败', icon: 'none' })
-            reject(resData.msg)
-          }
-        },
-        fail: (err) => {
-          uni.showToast({ title: '网络异常', icon: 'none' })
-          reject(err)
+    uni.uploadFile({
+      url: BASE_URL + '/resource/oss/upload',
+      filePath: filePath,
+      name: 'file',
+      header: {
+        'Authorization': token ? `Bearer ${token}` : '',
+        ...DEFAULT_HEADERS
+      },
+      success: (res) => {
+        const resData = JSON.parse(res.data)
+        if (resData.code === 200) {
+          resolve(resData.data)
+        } else {
+          uni.showToast({ title: resData.msg || '上传失败', icon: 'none' })
+          reject(resData.msg)
         }
-      })
-    }).catch(reject)
+      },
+      fail: (err) => {
+        uni.showToast({ title: '网络异常', icon: 'none' })
+        reject(err)
+      }
+    })
   })
 }

+ 22 - 23
api/system/user.js

@@ -1,4 +1,5 @@
 import { request } from '@/utils/request'
+import { BASE_URL } from '@/utils/config'
 
 export function getInfo() {
   return request({
@@ -45,30 +46,28 @@ export function cancelUser() {
 export function uploadAvatar(filePath) {
   return new Promise((resolve, reject) => {
     const token = uni.getStorageSync('token') || ''
-    import('@/utils/config').then(({ BASE_URL }) => {
-      uni.uploadFile({
-        url: BASE_URL + '/system/user/profile/avatar',
-        filePath: filePath,
-        name: 'avatarfile',
-        header: {
-          'Authorization': token ? `Bearer ${token}` : '',
-          'clientid': 'fe63fea7be31b0200b496d08bc6b517d',
-          'X-Platform-Code': 'MfJkMNMW2JKXBuPcbP2rxkD3ynXmReAZZFm4fN7cAGwGJdKCmd'
-        },
-        success: (res) => {
-          const resData = JSON.parse(res.data)
-          if (resData.code === 200) {
-            resolve(resData.data)
-          } else {
-            uni.showToast({ title: resData.msg || '上传失败', icon: 'none' })
-            reject(resData.msg)
-          }
-        },
-        fail: (err) => {
-          uni.showToast({ title: '网络异常', icon: 'none' })
-          reject(err)
+    uni.uploadFile({
+      url: BASE_URL + '/system/user/profile/avatar',
+      filePath: filePath,
+      name: 'avatarfile',
+      header: {
+        'Authorization': token ? `Bearer ${token}` : '',
+        'clientid': 'fe63fea7be31b0200b496d08bc6b517d',
+        'X-Platform-Code': 'MfJkMNMW2JKXBuPcbP2rxkD3ynXmReAZZFm4fN7cAGwGJdKCmd'
+      },
+      success: (res) => {
+        const resData = JSON.parse(res.data)
+        if (resData.code === 200) {
+          resolve(resData.data)
+        } else {
+          uni.showToast({ title: resData.msg || '上传失败', icon: 'none' })
+          reject(resData.msg)
         }
-      })
+      },
+      fail: (err) => {
+        uni.showToast({ title: '网络异常', icon: 'none' })
+        reject(err)
+      }
     })
   })
 }

+ 26 - 3
components/center-select/index.vue

@@ -9,7 +9,7 @@
 					<view class="line line2"></view>
 				</view>
 			</view>
-			<scroll-view scroll-y class="select-content">
+			<scroll-view scroll-y class="select-content" @scrolltolower="onScrollBottom" lower-threshold="80">
 				<view 
 					v-for="(item, index) in options" 
 					:key="index" 
@@ -21,7 +21,9 @@
 					<!-- CSS 绘制的选中对勾 @Author: Antigravity -->
 					<view class="checkmark" v-if="isSelected(item)"></view>
 				</view>
-				<view class="empty-tip" v-if="options.length === 0">暂无选项</view>
+				<view class="loading-tip" v-if="loading">加载中...</view>
+				<view class="no-more-tip" v-else-if="finished && options.length > 0">没有更多了</view>
+				<view class="empty-tip" v-if="options.length === 0 && !loading">暂无选项</view>
 			</scroll-view>
 		</view>
 	</view>
@@ -54,10 +56,18 @@ const props = defineProps({
 	valueKey: {
 		type: String,
 		default: 'value'
+	},
+	loading: {
+		type: Boolean,
+		default: false
+	},
+	finished: {
+		type: Boolean,
+		default: true
 	}
 })
 
-const emit = defineEmits(['update:modelValue', 'select'])
+const emit = defineEmits(['update:modelValue', 'select', 'loadMore'])
 
 const getLabel = (item) => {
 	if (typeof item === 'string' || typeof item === 'number') return item
@@ -81,6 +91,12 @@ const onSelect = (item) => {
 const onClose = () => {
 	emit('update:modelValue', false)
 }
+
+const onScrollBottom = () => {
+	if (!props.loading && !props.finished) {
+		emit('loadMore')
+	}
+}
 </script>
 
 <style lang="scss" scoped>
@@ -184,4 +200,11 @@ const onClose = () => {
 	color: #999;
 	font-size: 26rpx;
 }
+
+.loading-tip, .no-more-tip {
+	padding: 30rpx 0;
+	text-align: center;
+	color: #bbb;
+	font-size: 24rpx;
+}
 </style>

+ 261 - 0
components/page-select/index.vue

@@ -0,0 +1,261 @@
+<template>
+	<view class="page-select-mask" v-if="modelValue" @click="onClose" @touchmove.stop.prevent>
+		<view class="page-select-container" @click.stop @touchmove.stop>
+			<!-- 标题栏 -->
+			<view class="select-header">
+				<text class="select-title">{{ title }}</text>
+				<view class="close-btn" @click="onClose">
+					<view class="line line1"></view>
+					<view class="line line2"></view>
+				</view>
+			</view>
+
+			<!-- 搜索栏(可选) -->
+			<view class="search-bar" v-if="searchable">
+				<view class="search-box">
+					<view class="search-icon"></view>
+					<input class="search-input" v-model="localSearchKey" :placeholder="searchPlaceholder" @confirm="onSearch" confirm-type="search" />
+					<view class="search-btn" @click="onSearch">查询</view>
+				</view>
+			</view>
+
+			<!-- 列表区 -->
+			<view class="select-list-wrapper" @touchmove.stop>
+				<scroll-view scroll-y class="select-list" @scrolltolower="onScrollBottom" lower-threshold="80">
+					<view class="select-list-inner">
+						<view
+							v-for="(item, index) in options"
+							:key="index"
+							class="select-item"
+							:class="{ 'active': isSelected(item) }"
+							@click="onSelect(item)"
+						>
+							<!-- 默认插槽:自定义每行内容 -->
+							<slot name="item" :item="item" :index="index">
+								<text class="item-label">{{ getLabel(item) }}</text>
+							</slot>
+							<!-- 选中对勾 -->
+							<view class="checkmark" v-if="isSelected(item)"></view>
+						</view>
+						<view class="loading-tip" v-if="loading">加载中...</view>
+						<view class="no-more-tip" v-else-if="finished && options.length > 0">没有更多了</view>
+						<view class="empty-tip" v-if="options.length === 0 && !loading">{{ emptyText }}</view>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+/**
+ * 分页选择器组件(纯 UI,不涉及网络调用)
+ * 支持搜索 + 滑动到底部触发分页
+ * @Author: Antigravity
+ */
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+	modelValue: Boolean,
+	title: { type: String, default: '请选择' },
+	options: { type: Array, default: () => [] },
+	value: { type: [String, Number], default: '' },
+	labelKey: { type: String, default: 'label' },
+	valueKey: { type: String, default: 'value' },
+	loading: { type: Boolean, default: false },
+	finished: { type: Boolean, default: true },
+	searchable: { type: Boolean, default: false },
+	searchKey: { type: String, default: '' },
+	searchPlaceholder: { type: String, default: '请输入关键词搜索' },
+	emptyText: { type: String, default: '暂无选项' }
+})
+
+const emit = defineEmits(['update:modelValue', 'select', 'loadMore', 'search'])
+
+const localSearchKey = ref(props.searchKey)
+
+watch(() => props.searchKey, (val) => { localSearchKey.value = val })
+
+const getLabel = (item) => {
+	if (typeof item === 'string' || typeof item === 'number') return item
+	return item[props.labelKey]
+}
+
+const getValue = (item) => {
+	if (typeof item === 'string' || typeof item === 'number') return item
+	return item[props.valueKey]
+}
+
+const isSelected = (item) => getValue(item) === props.value
+
+const onSelect = (item) => {
+	emit('select', item)
+	emit('update:modelValue', false)
+}
+
+const onClose = () => {
+	emit('update:modelValue', false)
+}
+
+const onScrollBottom = () => {
+	if (!props.loading && !props.finished) {
+		emit('loadMore')
+	}
+}
+
+const onSearch = () => {
+	emit('search', localSearchKey.value)
+}
+</script>
+
+<style lang="scss" scoped>
+.page-select-mask {
+	position: fixed;
+	top: 0; left: 0; right: 0; bottom: 0;
+	background: rgba(0, 0, 0, 0.6);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	z-index: 9999;
+	backdrop-filter: blur(2px);
+}
+
+.page-select-container {
+	width: 620rpx;
+	background: #fff;
+	border-radius: 32rpx;
+	display: flex;
+	flex-direction: column;
+	max-height: 70vh;
+	overflow: hidden;
+	animation: popIn 0.25s ease-out;
+}
+
+@keyframes popIn {
+	from { transform: scale(0.8); opacity: 0; }
+	to { transform: scale(1); opacity: 1; }
+}
+
+.select-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 32rpx 40rpx;
+	border-bottom: 2rpx solid #f2f2f2;
+
+	.select-title {
+		font-size: 32rpx;
+		font-weight: bold;
+		color: #333;
+	}
+}
+
+.close-btn {
+	width: 40rpx;
+	height: 40rpx;
+	position: relative;
+
+	.line {
+		position: absolute;
+		top: 50%; left: 50%;
+		width: 30rpx; height: 4rpx;
+		background: #999;
+		border-radius: 4rpx;
+	}
+	.line1 { transform: translate(-50%, -50%) rotate(45deg); }
+	.line2 { transform: translate(-50%, -50%) rotate(-45deg); }
+}
+
+.search-bar {
+	padding: 20rpx 32rpx;
+	border-bottom: 2rpx solid #f2f2f2;
+}
+
+.search-box {
+	display: flex;
+	align-items: center;
+	background: #f5f5f5;
+	border-radius: 36rpx;
+	padding: 0 24rpx;
+	height: 72rpx;
+
+	.search-icon {
+		width: 20rpx; height: 20rpx;
+		border: 3rpx solid #999;
+		border-radius: 50%;
+		margin-right: 12rpx;
+		position: relative;
+
+		&::after {
+			content: '';
+			width: 10rpx; height: 3rpx;
+			background: #999;
+			position: absolute;
+			bottom: -4rpx; right: -4rpx;
+			transform: rotate(45deg);
+		}
+	}
+
+	.search-input {
+		flex: 1;
+		font-size: 26rpx;
+	}
+
+	.search-btn {
+		font-size: 26rpx;
+		color: #ff9500;
+		font-weight: bold;
+		margin-left: 20rpx;
+	}
+}
+
+.select-list-wrapper {
+	flex: 1;
+	min-height: 0;
+	overflow: hidden;
+}
+
+.select-list {
+	max-height: 55vh;
+}
+
+.select-list-inner {
+	padding: 0 32rpx;
+}
+
+.select-item {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 30rpx 0;
+	border-bottom: 2rpx solid #f9f9f9;
+
+	&:active { background: #f7f8fa; }
+
+	&.active .item-label { color: #ff9500; font-weight: bold; }
+
+	.item-label { font-size: 28rpx; color: #333; flex: 1; }
+}
+
+.checkmark {
+	width: 12rpx; height: 22rpx;
+	border-right: 4rpx solid #ff9500;
+	border-bottom: 4rpx solid #ff9500;
+	transform: rotate(45deg);
+	flex-shrink: 0;
+}
+
+.loading-tip, .no-more-tip {
+	padding: 30rpx 0;
+	text-align: center;
+	color: #bbb;
+	font-size: 24rpx;
+}
+
+.empty-tip {
+	padding: 80rpx 0;
+	text-align: center;
+	color: #ccc;
+	font-size: 26rpx;
+}
+</style>

+ 3 - 1
json/orderStatus.json

@@ -4,5 +4,7 @@
 	{ "value": 2, "label": "待服务", "color": "#49a3ff" },
 	{ "value": 3, "label": "服务中", "color": "#49a3ff" },
 	{ "value": 4, "label": "已完成", "color": "#67c23a" },
-	{ "value": 5, "label": "已取消", "color": "#909399" }
+	{ "value": 5, "label": "已取消", "color": "#909399" },
+	{ "value": 6, "label": "已拒绝", "color": "#f56c6c" },
+	{ "value": 7, "label": "已关闭", "color": "#909399" }
 ]

+ 5 - 2
manifest.json

@@ -2,8 +2,8 @@
     "name" : "好萌友(测试版)",
     "appid" : "__UNI__F19BBAD",
     "description" : "宠物服务商家端",
-    "versionName" : "1.0.29t",
-    "versionCode" : 30,
+    "versionName" : "1.0.36t",
+    "versionCode" : 37,
     "transformPx" : false,
     "app-plus" : {
         "usingComponents" : true,
@@ -16,6 +16,7 @@
             "delay" : 0
         },
         "modules" : {
+            "Camera" : {},
             "VideoPlayer" : {}
         },
         "distribute" : {
@@ -34,6 +35,8 @@
                     "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
                     "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
                     "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
                     "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
                 ]
             },

+ 84 - 1
pages/index/index.vue

@@ -48,6 +48,27 @@
 			</view>
 		</view>
 
+		<!-- 中间横幅图片 @Author: Antigravity -->
+		<view class="middle-section">
+			<image src="/static/images/index-middle.png" class="middle-image" mode="widthFix"></image>
+		</view>
+
+		<!-- 底部居中标志 @Author: Antigravity -->
+		<view class="symbol-section">
+			<image src="/static/images/index-symbol.png" class="symbol-img" mode="widthFix"></image>
+		</view>
+
+		<!-- 底部手势与文字 @Author: Antigravity -->
+		<view class="hand-section">
+			<view class="hand-wrapper">
+				<image src="/static/images/index-hand.png" class="hand-img" mode="widthFix"></image>
+				<view class="hand-text-content">
+					<text class="hand-title">好萌友提醒您~</text>
+					<text class="hand-subtitle">功能优化中,敬请期待~</text>
+				</view>
+			</view>
+		</view>
+
 		<custom-tabbar></custom-tabbar>
 	</view>
 </template>
@@ -104,7 +125,7 @@ import customTabbar from '@/components/custom-tabbar/index.vue'
 }
 .nav-container {
 	display: flex;
-	height: 380rpx;
+	height: 280rpx; /* 将之前较高的 380rpx 调低,使整体按钮高度变矮 */
 	gap: 20rpx;
 }
 
@@ -161,4 +182,66 @@ import customTabbar from '@/components/custom-tabbar/index.vue'
 	display: block;
 }
 
+/* ====== 中间横幅 @Author: Antigravity ====== */
+.middle-section {
+	padding: 10rpx 0; /* 缩小自身上下间距 */
+	display: flex;
+	justify-content: center;
+}
+.middle-image {
+	width: 65%; /* 进一步缩小宽度到 65% */
+	border-radius: 20rpx;
+	display: block;
+}
+
+/* ====== 底部标志 @Author: Antigravity ====== */
+.symbol-section {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	padding: 10rpx 32rpx 20rpx; /* 控制绿色横幅的 padding 以对齐上方 */
+}
+.symbol-img {
+	width: 100%; /* 中间的标志(绿色大横幅)完美等宽于三个按钮 */
+	border-radius: 20rpx;
+	display: block;
+}
+
+/* ====== 底部手势与文字 @Author: Antigravity ====== */
+.hand-section {
+	padding: 30rpx 32rpx 60rpx;
+	display: flex;
+	justify-content: center;
+}
+.hand-wrapper {
+	position: relative;
+	width: 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+.hand-img {
+	width: 100%;
+	display: block;
+}
+.hand-text-content {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	gap: 12rpx;
+}
+.hand-title {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: bold;
+}
+.hand-subtitle {
+	font-size: 24rpx;
+	color: #999;
+}
+
 </style>

+ 9 - 7
pages/login/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<view class="login-page">
-		<nav-bar title="登录" bgColor="transparent" color="#fff"></nav-bar>
+		<nav-bar title="登录" bgColor="transparent" color="#fff" :showBack="false"></nav-bar>
 		<!-- 顶部渐变装饰区 -->
 		<view class="hero-bg">
 			<view class="deco-circle c1"></view>
@@ -9,7 +9,7 @@
 
 			<!-- 返回按钮 -->
 			<view class="back-btn" @click="onClickLeft">
-				<uni-icons type="left" size="20" color="#fff"></uni-icons>
+				<uni-icons type="left" size="22" color="#fff"></uni-icons>
 			</view>
 
 			<!-- Logo 区域 -->
@@ -17,7 +17,7 @@
 				<view class="logo-wrap">
 					<uni-icons type="headphones" size="42" color="#fff"></uni-icons>
 				</view>
-				<text class="brand-name">宠物服务平台</text>
+				<text class="brand-name">好萌友</text>
 				<text class="brand-desc">专业 · 安心 · 便捷</text>
 			</view>
 		</view>
@@ -94,7 +94,7 @@ const dialogVisible = ref(false)
 const dialogTitle = ref('')
 const dialogContent = ref('')
 
-const onClickLeft = () => uni.navigateBack()
+const onClickLeft = () => uni.reLaunch({ url: '/pages/index/index' })
 
 const onCheckChange = () => {
 	checked.value = !checked.value
@@ -226,13 +226,15 @@ const showAgreement = async (agreementId) => {
 	position: absolute;
 	top: calc(var(--status-bar-height, 44px) + 20rpx);
 	left: 32rpx;
-	width: 72rpx;
-	height: 72rpx;
+	width: 76rpx;
+	height: 76rpx;
 	border-radius: 50%;
-	background: rgba(255, 255, 255, 0.25);
+	background: rgba(255, 255, 255, 0.35);
 	display: flex;
 	align-items: center;
 	justify-content: center;
+	box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
+	z-index: 1000;
 }
 
 .hero-content {

+ 270 - 135
pages/my/complaint/submit/index.vue

@@ -28,8 +28,8 @@
 					<text class="title-text">{{ praiseFlag ? '评价详情' : '投诉原因' }}</text>
 					<text class="title-tip">必填</text>
 				</view>
-				<textarea class="content-textarea" v-model="reason" 
-					:placeholder="praiseFlag ? '请记录您的满意点,帮助履约师提升服务质量...' : '请详细描述您遇到的问题,我们会尽快为您处理...'" 
+				<textarea class="content-textarea" v-model="reason"
+					:placeholder="praiseFlag ? '请记录您的满意点,帮助履约师提升服务质量...' : '请详细描述您遇到的问题,我们会尽快为您处理...'"
 					maxlength="500"></textarea>
 				<view class="word-count">{{ reason.length }}/500</view>
 			</view>
@@ -62,7 +62,8 @@
 
 		<!-- 底部按钮 -->
 		<view class="bottom-bar">
-			<button class="submit-btn" :class="{ 'is-praise': praiseFlag }" @click="handleConfirmSubmit" :loading="submitting">
+			<button class="submit-btn" :class="{ 'is-praise': praiseFlag }" @click="handleConfirmSubmit"
+				:loading="submitting">
 				{{ praiseFlag ? '确认赞' : '确认不赞' }}
 			</button>
 		</view>
@@ -86,7 +87,8 @@ const orderCode = ref('')
 const fulfillerId = ref('')
 const praiseFlag = ref(false)
 const reason = ref('')
-const imageList = ref([])
+const imageList = ref([])  // 预览用 URL 列表
+const ossIdList = ref([])  // 提交用 OSS ID 列表
 const submitting = ref(false)
 
 onLoad((options) => {
@@ -97,96 +99,98 @@ onLoad((options) => {
 
 // 选择图片
 const chooseImage = () => {
-    uni.chooseImage({
-        count: 6 - imageList.value.length,
-        sizeType: ['compressed'],
-        success: (res) => {
-            const tempFiles = res.tempFiles
-            uploadFiles(tempFiles)
-        }
-    })
+	uni.chooseImage({
+		count: 6 - imageList.value.length,
+		sizeType: ['compressed'],
+		success: (res) => {
+			const tempFiles = res.tempFiles
+			uploadFiles(tempFiles)
+		}
+	})
 }
 
 // 上传图片到服务器
 const uploadFiles = (tempFiles) => {
-    uni.showLoading({ title: '上传中...' })
-    const token = uni.getStorageSync('token') || ''
-    
-    const uploadPromises = tempFiles.map(file => {
-        return new Promise((resolve, reject) => {
-            uni.uploadFile({
-                url: BASE_URL + '/resource/oss/upload',
-                filePath: file.path,
-                name: 'file',
-                header: {
-                    ...DEFAULT_HEADERS,
-                    'Authorization': `Bearer ${token}`
-                },
-                success: (res) => {
-                    const data = JSON.parse(res.data)
-                    if (data.code === 200) {
-                        resolve(data.data.url)
-                    } else {
-                        reject(data.msg || '上传失败')
-                    }
-                },
-                fail: (err) => reject('请求失败')
-            })
-        })
-    })
-
-    Promise.all(uploadPromises).then(urls => {
-        imageList.value = [...imageList.value, ...urls]
-        uni.hideLoading()
-    }).catch(err => {
-        uni.hideLoading()
-        uni.showToast({ title: String(err), icon: 'none' })
-    })
+	uni.showLoading({ title: '上传中...' })
+	const token = uni.getStorageSync('token') || ''
+
+	const uploadPromises = tempFiles.map(file => {
+		return new Promise((resolve, reject) => {
+			uni.uploadFile({
+				url: BASE_URL + '/resource/oss/upload',
+				filePath: file.path,
+				name: 'file',
+				header: {
+					...DEFAULT_HEADERS,
+					'Authorization': `Bearer ${token}`
+				},
+				success: (res) => {
+					const data = JSON.parse(res.data)
+					if (data.code === 200) {
+						resolve({ url: data.data.url, ossId: data.data.ossId })
+					} else {
+						reject(data.msg || '上传失败')
+					}
+				},
+				fail: (err) => reject('请求失败')
+			})
+		})
+	})
+
+	Promise.all(uploadPromises).then(results => {
+		imageList.value = [...imageList.value, ...results.map(r => r.url)]
+		ossIdList.value = [...ossIdList.value, ...results.map(r => r.ossId)]
+		uni.hideLoading()
+	}).catch(err => {
+		uni.hideLoading()
+		uni.showToast({ title: String(err), icon: 'none' })
+	})
 }
 
 const previewImage = (index) => {
-    uni.previewImage({
-        current: index,
-        urls: imageList.value
-    })
+	uni.previewImage({
+		current: index,
+		urls: imageList.value
+	})
 }
 
 const removeImage = (index) => {
-    imageList.value.splice(index, 1)
+	imageList.value.splice(index, 1)
+	ossIdList.value.splice(index, 1)
 }
 
 const handleConfirmSubmit = async () => {
-    if (!reason.value.trim()) {
-        uni.showToast({ title: praiseFlag.value ? '请输入评价内容' : '请输入投诉原因', icon: 'none' })
-        return
-    }
-    if (!orderId.value || !fulfillerId.value) {
-        uni.showToast({ title: '订单数据不完整,无法提交', icon: 'none' })
-        return
-    }
-
-    submitting.value = true
-    try {
-        const payload = {
-            orderId: orderId.value,
-            fulfiller: fulfillerId.value,
-            reason: reason.value,
-            photos: imageList.value.join(','),
-            praiseFlag: praiseFlag.value
-        }
-        
-        await addComplaint(payload)
-        
-        uni.showToast({ title: '提交成功', icon: 'success' })
-        setTimeout(() => {
-            uni.navigateBack()
-        }, 1500)
-    } catch (error) {
-        console.error('提交失败:', error)
-        // 500提示已经在全局处理
-    } finally {
-        submitting.value = false
-    }
+	if (!reason.value.trim()) {
+		uni.showToast({ title: praiseFlag.value ? '请输入评价内容' : '请输入投诉原因', icon: 'none' })
+		return
+	}
+	if (!orderId.value || !fulfillerId.value) {
+		uni.showToast({ title: '订单数据不完整,无法提交', icon: 'none' })
+		return
+	}
+
+	submitting.value = true
+	try {
+		const payload = {
+			orderId: orderId.value,
+			fulfiller: fulfillerId.value,
+			reason: reason.value,
+			photos: ossIdList.value.join(','),
+			praiseFlag: praiseFlag.value
+		}
+
+		await addComplaint(payload)
+
+		uni.showToast({ title: '提交成功', icon: 'success' })
+		setTimeout(() => {
+			uni.navigateBack()
+		}, 1500)
+	} catch (error) {
+		console.error('提交失败:', error)
+		// 500提示已经在全局处理
+	} finally {
+		submitting.value = false
+	}
 }
 </script>
 
@@ -203,7 +207,7 @@ const handleConfirmSubmit = async () => {
 .card-shadow {
 	background: #fff;
 	border-radius: 24rpx;
-	box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
 	margin-bottom: 30rpx;
 }
 
@@ -213,51 +217,129 @@ const handleConfirmSubmit = async () => {
 	align-items: center;
 	height: 140rpx;
 	padding: 0 16rpx;
-    .type-item {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        justify-content: center;
-        gap: 8rpx;
-        transition: all 0.3s;
-        height: 110rpx;
-        border-radius: 20rpx;
-        &.active {
-            background-color: #f6f6f6;
-            .type-text { font-weight: bold; color: #333; font-size: 30rpx; }
-            .type-emoji { transform: scale(1.3); }
-            .type-sub { opacity: 1; }
-        }
-        .icon-wrap {
-            width: 64rpx; height: 64rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center;
-        }
-        .icon-wrap.bad { background: rgba(244,67,54,0.08); }
-        .icon-wrap.good { background: rgba(76,175,80,0.08); }
-        .type-emoji { font-size: 36rpx; line-height: 1; transition: transform 0.3s ease; }
-        .type-text { font-size: 28rpx; color: #999; transition: all 0.3s; }
-        .type-sub { font-size: 20rpx; color: #bbb; opacity: 0; transition: opacity 0.3s; }
-    }
-    .type-divider { width: 2rpx; height: 60rpx; background: linear-gradient(180deg, transparent, #EEEEEE, transparent); border-radius: 2rpx; }
+
+	.type-item {
+		flex: 1;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		gap: 8rpx;
+		transition: all 0.3s;
+		height: 110rpx;
+		border-radius: 20rpx;
+
+		&.active {
+			background-color: #f6f6f6;
+
+			.type-text {
+				font-weight: bold;
+				color: #333;
+				font-size: 30rpx;
+			}
+
+			.type-emoji {
+				transform: scale(1.3);
+			}
+
+			.type-sub {
+				opacity: 1;
+			}
+		}
+
+		.icon-wrap {
+			width: 64rpx;
+			height: 64rpx;
+			border-radius: 50%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+		}
+
+		.icon-wrap.bad {
+			background: rgba(244, 67, 54, 0.08);
+		}
+
+		.icon-wrap.good {
+			background: rgba(76, 175, 80, 0.08);
+		}
+
+		.type-emoji {
+			font-size: 36rpx;
+			line-height: 1;
+			transition: transform 0.3s ease;
+		}
+
+		.type-text {
+			font-size: 28rpx;
+			color: #999;
+			transition: all 0.3s;
+		}
+
+		.type-sub {
+			font-size: 20rpx;
+			color: #bbb;
+			opacity: 0;
+			transition: opacity 0.3s;
+		}
+	}
+
+	.type-divider {
+		width: 2rpx;
+		height: 60rpx;
+		background: linear-gradient(180deg, transparent, #EEEEEE, transparent);
+		border-radius: 2rpx;
+	}
 }
 
 /* 表单板块 */
 .form-section {
 	padding: 32rpx;
+
 	.section-title {
-		display: flex; align-items: center; gap: 12rpx; margin-bottom: 24rpx;
-		.title-text { font-size: 30rpx; font-weight: bold; color: #333; }
-		.title-tip { 
-            font-size: 20rpx; color: #F44336; background: rgba(244,67,54,0.1); 
-            padding: 2rpx 10rpx; border-radius: 4rpx; 
-            &.gray { color: #999; background: #f5f5f5; }
-        }
+		display: flex;
+		align-items: center;
+		gap: 12rpx;
+		margin-bottom: 24rpx;
+
+		.title-text {
+			font-size: 30rpx;
+			font-weight: bold;
+			color: #333;
+		}
+
+		.title-tip {
+			font-size: 20rpx;
+			color: #F44336;
+			background: rgba(244, 67, 54, 0.1);
+			padding: 2rpx 10rpx;
+			border-radius: 4rpx;
+
+			&.gray {
+				color: #999;
+				background: #f5f5f5;
+			}
+		}
 	}
+
 	.content-textarea {
-		width: 100%; height: 260rpx; font-size: 28rpx; color: #333; line-height: 1.6;
-		background-color: #fafafa; border-radius: 16rpx; padding: 20rpx; box-sizing: border-box;
+		width: 100%;
+		height: 260rpx;
+		font-size: 28rpx;
+		color: #333;
+		line-height: 1.6;
+		background-color: #fafafa;
+		border-radius: 16rpx;
+		padding: 20rpx;
+		box-sizing: border-box;
+	}
+
+	.word-count {
+		text-align: right;
+		font-size: 22rpx;
+		color: #ccc;
+		margin-top: 12rpx;
 	}
-	.word-count { text-align: right; font-size: 22rpx; color: #ccc; margin-top: 12rpx; }
 }
 
 /* 图片上传网格 */
@@ -265,39 +347,92 @@ const handleConfirmSubmit = async () => {
 	display: grid;
 	grid-template-columns: repeat(3, 1fr);
 	gap: 20rpx;
+
 	.upload-item {
-		position: relative; aspect-ratio: 1; border-radius: 12rpx; overflow: hidden;
-		image { width: 100%; height: 100%; }
+		position: relative;
+		aspect-ratio: 1;
+		border-radius: 12rpx;
+		overflow: hidden;
+
+		image {
+			width: 100%;
+			height: 100%;
+		}
+
 		.delete-icon {
-			position: absolute; right: 0; top: 0; width: 36rpx; height: 36rpx;
-			background: rgba(0,0,0,0.5); border-bottom-left-radius: 12rpx;
-			display: flex; align-items: center; justify-content: center;
+			position: absolute;
+			right: 0;
+			top: 0;
+			width: 36rpx;
+			height: 36rpx;
+			background: rgba(0, 0, 0, 0.5);
+			border-bottom-left-radius: 12rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
 		}
 	}
+
 	.upload-add {
-		aspect-ratio: 1; border-radius: 12rpx; border: 2rpx dashed #EEEEEE;
-		display: flex; flex-direction: column; align-items: center; justify-content: center;
+		aspect-ratio: 1;
+		border-radius: 12rpx;
+		border: 2rpx dashed #EEEEEE;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
 		background: #fafafa;
-		.add-text { font-size: 22rpx; color: #ccc; margin-top: 8rpx; }
+
+		.add-text {
+			font-size: 22rpx;
+			color: #ccc;
+			margin-top: 8rpx;
+		}
 	}
 }
 
 .order-info-bar {
-	padding: 10rpx 0; font-size: 24rpx; color: #bbb; text-align: center;
+	padding: 10rpx 0;
+	font-size: 24rpx;
+	color: #bbb;
+	text-align: center;
 }
 
 /* 底部按钮 */
 .bottom-bar {
-	position: fixed; left: 0; right: 0; bottom: 0;
-	background: #fff; padding: 24rpx 40rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
-	box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.03);
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: #fff;
+	padding: 24rpx 40rpx;
+	padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+	box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.03);
+
 	.submit-btn {
-		height: 96rpx; line-height: 96rpx; border-radius: 48rpx; 
-		background: #333; color: #fff; font-size: 32rpx; font-weight: bold; border: none;
+		height: 96rpx;
+		line-height: 96rpx;
+		border-radius: 48rpx;
+		background: #333;
+		color: #fff;
+		font-size: 32rpx;
+		font-weight: bold;
+		border: none;
 		transition: all 0.3s;
-		&::after { border: none; }
-		&.is-praise { background: #FF9500; color: #fff; }
-		&:active { transform: scale(0.98); opacity: 0.9; }
+
+		&::after {
+			border: none;
+		}
+
+		&.is-praise {
+			background: #FF9500;
+			color: #fff;
+		}
+
+		&:active {
+			transform: scale(0.98);
+			opacity: 0.9;
+		}
 	}
 }
 </style>

+ 51 - 16
pages/my/index.vue

@@ -40,7 +40,8 @@
 				<text class="card-title">服务与工具</text>
 			</view>
 			<view class="tool-grid">
-				<view class="tool-item" v-for="item in menuItems" :key="item.path || item.title" @click="goToMenu(item)">
+				<view class="tool-item" v-for="item in menuItems" :key="item.path || item.title"
+					@click="goToMenu(item)">
 					<image class="custom-icon tool-icon" :src="item.icon" mode="aspectFit"></image>
 					<text class="tool-text">{{ item.title }}</text>
 				</view>
@@ -61,7 +62,8 @@
 
 				<view class="qr-section">
 					<text class="qr-title">客服二维码</text>
-					<image class="qr-img" :src="customerSetting.qrCodeUrl || '/static/images/logo.png'" mode="aspectFit" @click="previewQRCode">
+					<image class="qr-img" :src="customerSetting.qrCodeUrl || '/static/images/logo.png'" mode="aspectFit"
+						@click="previewQRCode">
 					</image>
 					<text class="qr-desc">点击查看大图</text>
 				</view>
@@ -69,7 +71,8 @@
 				<view class="service-list">
 					<view class="service-row">
 						<view class="service-icon-box online-bg">
-							<image class="service-icon-img" src="/static/icon/my/customerservice/online.png" mode="aspectFit"></image>
+							<image class="service-icon-img" src="/static/icon/my/customerservice/online.png"
+								mode="aspectFit"></image>
 						</view>
 						<view class="service-info">
 							<text class="service-name">在线客服</text>
@@ -82,7 +85,8 @@
 
 					<view class="service-row">
 						<view class="service-icon-box phone-bg">
-							<image class="service-icon-img" src="/static/icon/my/customerservice/phone.png" mode="aspectFit"></image>
+							<image class="service-icon-img" src="/static/icon/my/customerservice/phone.png"
+								mode="aspectFit"></image>
 						</view>
 						<view class="service-info">
 							<text class="service-name">客服电话</text>
@@ -131,8 +135,8 @@ export default {
 				qrCodeUrl: '',
 				enterpriseWechatLink: ''
 			},
-			// 订单状态导航
-			orderItems: orderStatusData.map(item => ({
+			// 订单状态导航(过滤掉已拒绝和已关闭)
+			orderItems: orderStatusData.filter(item => ![6, 7].includes(item.value)).map(item => ({
 				key: item.value,
 				label: item.label,
 				icon: iconMap[item.value] || '/static/images/my-pendingdispatch.png'
@@ -293,7 +297,7 @@ export default {
 						}
 					}
 				})
-			} catch(e) {
+			} catch (e) {
 				uni.showModal({ title: '读取日志失败', content: e.message || String(e), showCancel: false })
 			}
 		}
@@ -335,8 +339,15 @@ export default {
 }
 
 @keyframes slide-in {
-	from { transform: scale(0.85); opacity: 0; }
-	to { transform: scale(1); opacity: 1; }
+	from {
+		transform: scale(0.85);
+		opacity: 0;
+	}
+
+	to {
+		transform: scale(1);
+		opacity: 1;
+	}
 }
 
 .service-header {
@@ -359,7 +370,9 @@ export default {
 	width: 44rpx;
 	height: 44rpx;
 	position: relative;
-	&::before, &::after {
+
+	&::before,
+	&::after {
 		content: '';
 		position: absolute;
 		top: 20rpx;
@@ -370,8 +383,14 @@ export default {
 		transform: rotate(45deg);
 		border-radius: 4rpx;
 	}
-	&::after { transform: rotate(-45deg); }
-	&:active { opacity: 0.6; }
+
+	&::after {
+		transform: rotate(-45deg);
+	}
+
+	&:active {
+		opacity: 0.6;
+	}
 }
 
 .qr-section {
@@ -414,7 +433,10 @@ export default {
 	align-items: center;
 	padding: 32rpx 0;
 	border-bottom: 2rpx solid #EEEEEE;
-	&:last-child { border-bottom: none; }
+
+	&:last-child {
+		border-bottom: none;
+	}
 }
 
 .service-icon-box {
@@ -425,10 +447,12 @@ export default {
 	align-items: center;
 	justify-content: center;
 	margin-right: 24rpx;
+
 	&.online-bg {
 		background: linear-gradient(135deg, #71d192 0%, #4caf50 100%);
 		box-shadow: 0 8rpx 16rpx rgba(76, 175, 80, 0.25);
 	}
+
 	&.phone-bg {
 		background: linear-gradient(135deg, #ffcc33 0%, #f7ca3e 100%);
 		box-shadow: 0 8rpx 16rpx rgba(247, 202, 62, 0.25);
@@ -467,9 +491,20 @@ export default {
 	transition: transform 0.2s;
 	background: transparent;
 	border: 2rpx solid transparent;
-	&:active { transform: scale(0.95); }
-	&.green-btn { border-color: #5ec686; color: #5ec686; }
-	&.orange-btn { border-color: #f7ca3e; color: #f7ca3e; }
+
+	&:active {
+		transform: scale(0.95);
+	}
+
+	&.green-btn {
+		border-color: #5ec686;
+		color: #5ec686;
+	}
+
+	&.orange-btn {
+		border-color: #f7ca3e;
+		color: #f7ca3e;
+	}
 }
 
 

+ 109 - 1
pages/my/user/add/index.vue

@@ -47,6 +47,13 @@
 				</view>
 				<view class="right-arrow"></view>
 			</view>
+			<view class="form-item" @click="openRegionModal">
+				<text class="form-label">所在地区</text>
+				<view class="picker-value" :class="{'placeholder': !regionLabel}">
+					{{ regionLabel || '请选择省/市/区' }}
+				</view>
+				<view class="right-arrow"></view>
+			</view>
 			<view class="form-item">
 				<text class="form-label require">详细住址</text>
 				<input class="form-input" v-model="form.address" placeholder="请输入街道/门牌号" />
@@ -126,6 +133,34 @@
 			</view>
 		</view>
 
+		<!-- 省市区三级联动居中弹窗 -->
+		<view class="center-modal-mask" v-if="showRegionModal" @click="showRegionModal = false" @touchmove.stop.prevent>
+			<view class="center-modal-content region-modal" @click.stop>
+				<view class="modal-header">
+					<text class="modal-title">选择地区</text>
+					<view class="close-btn" @click="showRegionModal = false"></view>
+				</view>
+
+				<view class="cascade-indicator">
+					<text v-for="(node, idx) in regionPath" :key="idx" class="path-node" @click="backToRegionLevel(idx)">{{ node.name }}</text>
+					<text class="path-node active" v-if="regionPath.length < 3">请选择</text>
+				</view>
+
+				<scroll-view scroll-y class="modal-list-scroll">
+					<view
+						class="list-item"
+						v-for="item in currentRegionList"
+						:key="item.code"
+						@click="onRegionStepSelect(item)"
+					>
+						<text class="item-text">{{ item.name }}</text>
+						<view class="checkmark" v-if="isRegionSelected(item)"></view>
+					</view>
+					<view class="empty-tip" v-if="currentRegionList.length === 0">暂无数据</view>
+				</scroll-view>
+			</view>
+		</view>
+
 		<!-- 选择器组件实例 -->
 		<center-select v-model="showGenderSelect" title="选择性别" :options="genderOptions" :value="form.gender" @select="(item) => form.gender = item.value" />
 		<center-select v-model="showHouseTypeSelect" title="房屋类型" :options="houseTypeOptions" :value="form.houseType" @select="(item) => form.houseType = item.value" />
@@ -144,6 +179,7 @@ import centerSelect from '@/components/center-select/index.vue'
 import { addCustomer } from '@/api/archieves/customer'
 import { getInfo } from '@/api/system/user'
 import { listAreaStation } from '@/api/system/areaStation'
+import { listRegionTree } from '@/api/system/region'
 import { uploadFile } from '@/api/system/oss'
 import customerEnums from '@/json/customer.json'
 
@@ -153,12 +189,14 @@ const genderOptions = [{ label: '男', value: 0 }, { label: '女', value: 1 }]
 const saving = ref(false)
 const allStationNodes = ref([])
 const avatarDisplayUrl = ref('')
+const regionTree = ref([])
 
 // 弹窗状态
 const showGenderSelect = ref(false)
 const showHouseTypeSelect = ref(false)
 const showEntryMethodSelect = ref(false)
 const showStationModal = ref(false)
+const showRegionModal = ref(false)
 
 // 站点选择级联逻辑
 const currentStep = ref(0)
@@ -171,7 +209,8 @@ const selectedStationName = ref('')
 
 const form = reactive({
     name: '', phone: '', gender: undefined, areaId: undefined, stationId: undefined,
-    address: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', remark: '', avatar: undefined
+    address: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', remark: '', avatar: undefined,
+    regionCode: ''
 })
 
 onLoad(async () => {
@@ -179,6 +218,10 @@ onLoad(async () => {
         const stationRes = await listAreaStation()
         allStationNodes.value = Array.isArray(stationRes) ? stationRes : (stationRes?.data || [])
     } catch (err) { console.error('获取站点失败', err) }
+    try {
+        const res = await listRegionTree()
+        regionTree.value = Array.isArray(res) ? res : []
+    } catch (err) { console.error('获取地区数据失败', err) }
 })
 
 const openStationModal = () => {
@@ -231,6 +274,42 @@ const getStationLabel = computed(() => {
     return `${selectedCityName.value} - ${selectedAreaName.value} - ${selectedStationName.value}`
 })
 
+// ========== 省市区三级联动 ==========
+const regionPath = ref([])
+const regionLabel = ref('')
+
+const currentRegionList = computed(() => {
+    let list = regionTree.value
+    for (let node of regionPath.value) {
+        const found = list.find(l => l.code === node.code)
+        if (found && found.children) list = found.children
+        else list = []
+    }
+    return list
+})
+
+const openRegionModal = () => {
+    regionPath.value = []
+    showRegionModal.value = true
+}
+
+const backToRegionLevel = (idx) => { regionPath.value = regionPath.value.slice(0, idx) }
+
+const onRegionStepSelect = (item) => {
+    regionPath.value.push({ code: item.code, name: item.name })
+    if (!item.children || item.children.length === 0 || regionPath.value.length >= 3) {
+        const fullLabel = regionPath.value.map(p => p.name).join(' / ')
+        form.regionCode = regionPath.value.map(p => p.code).join('/')
+        regionLabel.value = fullLabel
+        showRegionModal.value = false
+    }
+}
+
+const isRegionSelected = (item) => {
+    const level = regionPath.value.length
+    return regionPath.value[level]?.code === item.code
+}
+
 const getGenderLabel = computed(() => genderOptions.find(o => o.value === form.gender)?.label || '请选择')
 const getHouseTypeLabel = computed(() => houseTypeOptions.find(o => o.value === form.houseType)?.label || '请选择')
 const getEntryMethodLabel = computed(() => entryMethodOptions.find(o => o.value === form.entryMethod)?.label || '请选择')
@@ -346,4 +425,33 @@ const onSave = async () => {
 /* 对勾 @Author: Antigravity */
 .checkmark { width: 12rpx; height: 24rpx; border-right: 4rpx solid #ff9500; border-bottom: 4rpx solid #ff9500; transform: rotate(45deg); }
 .empty-tip { padding: 80rpx 0; text-align: center; color: #ccc; font-size: 26rpx; }
+
+/* 省市区级联样式 */
+.region-modal {
+    max-height: 75vh;
+}
+
+.cascade-indicator {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 32rpx;
+    background: #fdfdfd;
+    border-bottom: 2rpx solid #f9f9f9;
+    gap: 8rpx;
+    flex-wrap: wrap;
+}
+
+.cascade-indicator .path-node {
+    font-size: 24rpx;
+    color: #999;
+    padding: 4rpx 12rpx;
+    border-radius: 8rpx;
+    background: #f5f5f5;
+}
+
+.cascade-indicator .path-node.active {
+    color: #ff9500;
+    font-weight: bold;
+    background: #fff5e6;
+}
 </style>

+ 126 - 1
pages/my/user/edit/index.vue

@@ -52,6 +52,13 @@
 					</view>
 					<view class="right-arrow"></view>
 				</view>
+				<view class="form-item" @click="openRegionModal">
+					<text class="form-label">所在地区</text>
+					<view class="picker-value" :class="{'placeholder': !regionLabel}">
+						{{ regionLabel || '请选择省/市/区' }}
+					</view>
+					<view class="right-arrow"></view>
+				</view>
 				<view class="form-item">
 					<text class="form-label require">详细住址</text>
 					<input class="form-input" v-model="form.address" placeholder="请输入街道/门牌号" />
@@ -119,6 +126,34 @@
 			</view>
 		</view>
 
+		<!-- 省市区三级联动居中弹窗 -->
+		<view class="center-modal-mask" v-if="showRegionModal" @click="showRegionModal = false" @touchmove.stop.prevent>
+			<view class="center-modal-content region-modal" @click.stop>
+				<view class="modal-header">
+					<text class="modal-title">选择地区</text>
+					<view class="close-btn" @click="showRegionModal = false"></view>
+				</view>
+
+				<view class="cascade-indicator">
+					<text v-for="(node, idx) in regionPath" :key="idx" class="path-node" @click="backToRegionLevel(idx)">{{ node.name }}</text>
+					<text class="path-node active" v-if="regionPath.length < 3">请选择</text>
+				</view>
+
+				<scroll-view scroll-y class="modal-list-scroll">
+					<view
+						class="list-item"
+						v-for="item in currentRegionList"
+						:key="item.code"
+						@click="onRegionStepSelect(item)"
+					>
+						<text class="item-text">{{ item.name }}</text>
+						<view class="checkmark" v-if="isRegionSelected(item)"></view>
+					</view>
+					<view class="empty-tip" v-if="currentRegionList.length === 0">暂无数据</view>
+				</scroll-view>
+			</view>
+		</view>
+
 		<center-select v-model="showGenderSelect" title="性别修改" :options="genderOptions" :value="form.gender" @select="(item) => form.gender = item.value" />
 		<center-select v-model="showHouseTypeSelect" title="房屋类型" :options="houseTypeOptions" :value="form.houseType" @select="(item) => form.houseType = item.value" />
 		<center-select v-model="showEntryMethodSelect" title="入门方式" :options="entryMethodOptions" :value="form.entryMethod" @select="onEntryMethodChangeDetail" />
@@ -135,6 +170,7 @@ import navBar from '@/components/nav-bar/index.vue'
 import centerSelect from '@/components/center-select/index.vue'
 import { getCustomer, updateCustomer } from '@/api/archieves/customer'
 import { listAreaStation } from '@/api/system/areaStation'
+import { listRegionTree } from '@/api/system/region'
 import { uploadFile } from '@/api/system/oss'
 import customerEnums from '@/json/customer.json'
 
@@ -145,11 +181,13 @@ const loading = ref(true)
 const saving = ref(false)
 const allStationNodes = ref([])
 const avatarDisplayUrl = ref('')
+const regionTree = ref([])
 
 const showGenderSelect = ref(false)
 const showHouseTypeSelect = ref(false)
 const showEntryMethodSelect = ref(false)
 const showStationModal = ref(false)
+const showRegionModal = ref(false)
 
 const currentStep = ref(0)
 const selectedCityId = ref(null)
@@ -161,7 +199,8 @@ const selectedStationName = ref('')
 
 const form = reactive({
     id: undefined, name: '', phone: '', gender: undefined, areaId: undefined, stationId: undefined,
-    address: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', remark: '', avatar: undefined
+    address: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', remark: '', avatar: undefined,
+    regionCode: ''
 })
 
 onLoad(async (options) => {
@@ -170,6 +209,9 @@ onLoad(async (options) => {
         const stationRes = await listAreaStation()
         allStationNodes.value = Array.isArray(stationRes) ? stationRes : (stationRes?.data || [])
         
+        const regionRes = await listRegionTree()
+        regionTree.value = Array.isArray(regionRes) ? regionRes : []
+        
         if (options.id) {
             const res = await getCustomer(options.id)
             if (res) {
@@ -177,6 +219,7 @@ onLoad(async (options) => {
                 Object.assign(form, data)
                 avatarDisplayUrl.value = data.avatarUrl || ''
                 resolveStationNames()
+                resolveRegionByCode()
             }
         }
     } catch(err) { console.error(err) } finally { loading.value = false }
@@ -203,6 +246,59 @@ const resolveStationNames = () => {
 	}
 }
 
+// ========== 省市区三级联动 ==========
+const regionPath = ref([])
+const regionLabel = ref('')
+
+const currentRegionList = computed(() => {
+    let list = regionTree.value
+    for (let node of regionPath.value) {
+        const found = list.find(l => l.code === node.code)
+        if (found && found.children) list = found.children
+        else list = []
+    }
+    return list
+})
+
+const openRegionModal = () => {
+    regionPath.value = []
+    showRegionModal.value = true
+}
+
+const backToRegionLevel = (idx) => { regionPath.value = regionPath.value.slice(0, idx) }
+
+const onRegionStepSelect = (item) => {
+    regionPath.value.push({ code: item.code, name: item.name })
+    if (!item.children || item.children.length === 0 || regionPath.value.length >= 3) {
+        const fullLabel = regionPath.value.map(p => p.name).join(' / ')
+        form.regionCode = regionPath.value.map(p => p.code).join('/')
+        regionLabel.value = fullLabel
+        showRegionModal.value = false
+    }
+}
+
+const isRegionSelected = (item) => {
+    const level = regionPath.value.length
+    return regionPath.value[level]?.code === item.code
+}
+
+// 根据 regionCode 回显已选地区
+const resolveRegionByCode = () => {
+	if (!form.regionCode) return
+	const codes = form.regionCode.split('/')
+	const names = []
+	let list = regionTree.value
+	for (let code of codes) {
+		const found = list.find(l => String(l.code) === String(code))
+		if (found) {
+			names.push(found.name)
+			regionPath.value.push({ code: found.code, name: found.name })
+			list = found.children || []
+		} else break
+	}
+	if (names.length > 0) regionLabel.value = names.join(' / ')
+}
+
 const openStationModal = () => {
 	currentStep.value = 0
 	showStationModal.value = true
@@ -339,4 +435,33 @@ const onSave = async () => {
 .list-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 0; border-bottom: 2rpx solid #f9f9f9; }
 .checkmark { width: 12rpx; height: 24rpx; border-right: 4rpx solid #ff9500; border-bottom: 4rpx solid #ff9500; transform: rotate(45deg); }
 .empty-tip { padding: 80rpx 0; text-align: center; color: #ccc; }
+
+/* 省市区级联样式 */
+.region-modal {
+    max-height: 75vh;
+}
+
+.cascade-indicator {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 32rpx;
+    background: #fdfdfd;
+    border-bottom: 2rpx solid #f9f9f9;
+    gap: 8rpx;
+    flex-wrap: wrap;
+}
+
+.cascade-indicator .path-node {
+    font-size: 24rpx;
+    color: #999;
+    padding: 4rpx 12rpx;
+    border-radius: 8rpx;
+    background: #f5f5f5;
+}
+
+.cascade-indicator .path-node.active {
+    color: #ff9500;
+    font-weight: bold;
+    background: #fff5e6;
+}
 </style>

+ 59 - 25
pages/order/apply/index.vue

@@ -194,27 +194,14 @@
 		<!-- 居中联动选择弹窗群 @Author: Antigravity -->
 		
 		<!-- 宠主搜索弹窗 -->
-		<view class="center-modal-mask" v-if="showUserSelect" @click="showUserSelect = false">
-			<view class="center-modal-content user-search-modal" @click.stop>
-				<view class="modal-header">
-					<view class="search-box">
-						<view class="search-icon"></view>
-						<input class="search-input" v-model="userSearchKey" placeholder="搜索宠主姓名/手机号" @confirm="fetchUsers" confirm-type="search" />
-						<view class="search-btn" @click="fetchUsers">查询</view>
-					</view>
+		<page-select v-model="showUserSelect" title="选择宠主用户" searchable :searchKey="userSearchKey" searchPlaceholder="搜索宠主姓名/手机号" :options="userList" labelKey="name" valueKey="id" :value="formData.customerId" :loading="userPage.loading" :finished="userPage.finished" emptyText="未找到相关宠主" @select="onUserSelect" @loadMore="fetchUsers(false)" @search="onUserSearch">
+			<template #item="{ item }">
+				<view class="user-info">
+					<text class="name">{{ item.name }}</text>
+					<text class="phone">{{ item.phone || item.phoneNumber }}</text>
 				</view>
-				<scroll-view scroll-y class="modal-list-scroll">
-					<view class="list-item" v-for="user in userList" :key="user.id" @click="onUserSelect(user)">
-						<view class="user-info">
-							<text class="name">{{ user.name }}</text>
-							<text class="phone">{{ user.phone || user.phoneNumber }}</text>
-						</view>
-						<view class="checkmark" v-if="formData.customerId === user.id"></view>
-					</view>
-					<view class="empty-tip" v-if="userList.length === 0">未找到相关宠主</view>
-				</scroll-view>
-			</view>
-		</view>
+			</template>
+		</page-select>
 		
 		<!-- 区域选择器 (Cascader) @Author: Antigravity -->
 		<view class="center-modal-mask" v-if="showRegionModal" @click="showRegionModal = false">
@@ -233,8 +220,8 @@
 			</view>
 		</view>
 
-		<!-- 门店选择 -->
-		<center-select v-model="showShopSelect" title="选择服务门店" :options="shopList" labelKey="name" valueKey="id" :value="formData.merchantId" @select="onShopSelect" />
+	<!-- 门店选择 -->
+	<page-select v-model="showShopSelect" title="选择服务门店" searchable :searchKey="shopSearchKey" searchPlaceholder="搜索门店名称" :options="shopList" labelKey="name" valueKey="id" :value="formData.merchantId" :loading="shopPage.loading" :finished="shopPage.finished" @select="onShopSelect" @loadMore="fetchShops(true)" @search="onShopSearch" />
 		
 		<!-- 宠物选择 -->
 		<center-select v-model="showPetPopup" title="选择指定宠物" :options="petOptions" labelKey="_label" valueKey="id" :value="formData.petId" @select="onPetSelect" />
@@ -267,6 +254,7 @@
 import { ref, reactive, computed, watch } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
+import pageSelect from '@/components/page-select/index.vue'
 import centerSelect from '@/components/center-select/index.vue'
 import { listStoreOnOrder } from '@/api/system/store'
 import { listCustomerOnOrder } from '@/api/archieves/customer'
@@ -289,9 +277,15 @@ const showRegionModal = ref(false)
 const showTimeModal = ref(false)
 
 const userSearchKey = ref('')
+const shopSearchKey = ref('')
 const selectedUser = ref(null)
 const selectedShop = ref(null)
 
+// 门店分页状态
+const shopPage = reactive({ pageNum: 1, pageSize: 20, loading: false, finished: false })
+// 宠主分页状态
+const userPage = reactive({ pageNum: 1, pageSize: 20, loading: false, finished: false })
+
 const pickAreaLabel = ref('')
 const pickEndAreaLabel = ref('')
 const sendStartAreaLabel = ref('')
@@ -317,7 +311,7 @@ onLoad((options) => {
 	const stored = uni.getStorageSync('currentService')
 	if (stored) serviceInfo.value = stored
 	initTimeRanges()
-	fetchShops(); fetchUsers(); fetchRegionTree()
+	fetchShops(); fetchUsers(true); fetchRegionTree()
 })
 
 const initTimeRanges = () => {
@@ -455,12 +449,51 @@ const findRegionLabel = (code, list) => {
 	return find(list, target) || ''
 }
 
-const fetchShops = () => listStoreOnOrder({ pageNum: 1, pageSize: 50, serviceId: serviceInfo.value?.id }).then(res => { shopList.value = res.rows || [] })
-const fetchUsers = () => listCustomerOnOrder({ pageNum: 1, pageSize: 20, content: userSearchKey.value }).then(res => { userList.value = res.rows || [] })
+const fetchShops = (loadMore = false) => {
+	if (shopPage.loading) return
+	if (loadMore && shopPage.finished) return
+	if (!loadMore) {
+		shopPage.pageNum = 1
+		shopPage.finished = false
+	}
+	shopPage.loading = true
+	listStoreOnOrder({ pageNum: shopPage.pageNum, pageSize: shopPage.pageSize, serviceId: serviceInfo.value?.id, name: shopSearchKey.value }).then(res => {
+		const rows = res.rows || []
+		if (loadMore) {
+			shopList.value = [...shopList.value, ...rows]
+		} else {
+			shopList.value = rows
+		}
+		shopPage.finished = rows.length < shopPage.pageSize
+		shopPage.pageNum++
+	}).finally(() => { shopPage.loading = false })
+}
+
+const fetchUsers = (reset = false) => {
+	if (userPage.loading) return
+	if (!reset && userPage.finished) return
+	if (reset) {
+		userPage.pageNum = 1
+		userPage.finished = false
+	}
+	userPage.loading = true
+	listCustomerOnOrder({ pageNum: userPage.pageNum, pageSize: userPage.pageSize, content: userSearchKey.value }).then(res => {
+		const rows = res.rows || []
+		if (reset) {
+			userList.value = rows
+		} else {
+			userList.value = [...userList.value, ...rows]
+		}
+		userPage.finished = rows.length < userPage.pageSize
+		userPage.pageNum++
+	}).finally(() => { userPage.loading = false })
+}
 const fetchPets = (uid) => listPetByUser(uid).then(res => { petList.value = Array.isArray(res) ? res : (res.rows || []) })
 const fetchRegionTree = () => listRegionTree().then(res => { regionTree.value = res || [] })
 
 const onShopSelect = (shop) => { selectedShop.value = shop; showShopSelect.value = false }
+const onShopSearch = (keyword) => { shopSearchKey.value = keyword; fetchShops(false) }
+const onUserSearch = (keyword) => { userSearchKey.value = keyword; fetchUsers(true) }
 const onUserSelect = (user) => {
 	selectedUser.value = user; formData.customerId = user.id; 
 	formData.petId = ''; formData.petName = ''; petList.value = []; fetchPets(user.id)
@@ -599,4 +632,5 @@ const onSubmit = async () => {
 
 .right-arrow { width: 12rpx; height: 12rpx; border-right: 3rpx solid #ccc; border-top: 3rpx solid #ccc; transform: rotate(45deg); flex-shrink: 0; }
 .empty-tip { padding: 80rpx 0; text-align: center; color: #ccc; font-size: 24rpx; }
+.user-info { display: flex; flex-direction: column; flex: 1; .name { font-size: 28rpx; font-weight: bold; color: #333; } .phone { font-size: 22rpx; color: #999; margin-top: 4rpx; } }
 </style>

+ 397 - 223
pages/order/detail/index.vue

@@ -17,209 +17,270 @@
 			<!-- 订单号与状态 -->
 			<view class="order-header">
 				<view class="order-id-row">
-				<text class="order-id">{{ order.code || order.id }}</text>
-				<text :class="['status-badge', `badge-${order.statusKey}`]">{{ order.statusText }}</text>
-				<text class="service-badge">{{ currentServiceName }}</text>
+					<text class="order-id">{{ order.code || order.id }}</text>
+					<text :class="['status-badge', `badge-${order.statusKey}`]">{{ order.statusText }}</text>
+					<text class="service-badge">{{ currentServiceName }}</text>
+				</view>
 			</view>
-		</view>
 
-		<!-- 状态进度条 -->
-		<view class="progress-card">
-			<view class="progress-steps">
-				<view v-for="(step, i) in progressSteps" :key="i"
-					:class="['step-item', { done: step.done, active: step.active }]">
-					<view class="step-circle">
-						<uni-icons v-if="step.done" type="checkmarkempty" size="12" color="#fff"></uni-icons>
-						<text v-else class="step-num">{{ i + 1 }}</text>
+			<!-- 状态进度条 -->
+			<view class="progress-card">
+				<view class="progress-steps">
+					<view v-for="(step, i) in progressSteps" :key="i"
+						:class="['step-item', { done: step.done, active: step.active }]">
+						<view class="step-circle">
+							<uni-icons v-if="step.done" type="checkmarkempty" size="12" color="#fff"></uni-icons>
+							<text v-else class="step-num">{{ i + 1 }}</text>
+						</view>
+						<view class="step-line" v-if="i < progressSteps.length - 1" :class="{ done: step.done }"></view>
+						<text class="step-label">{{ step.label }}</text>
+						<text class="step-time">{{ step.time }}</text>
 					</view>
-					<view class="step-line" v-if="i < progressSteps.length - 1" :class="{ done: step.done }"></view>
-					<text class="step-label">{{ step.label }}</text>
-					<text class="step-time">{{ step.time }}</text>
 				</view>
 			</view>
-		</view>
 
-		<!-- 宠物档案 + 用户信息 -->
-		<view class="info-row-cards">
-			<view class="info-card pet-card">
-				<text class="card-label">宠物档案</text>
-				<view class="pet-header">
-					<view class="pet-avatar">
-						<image v-if="order.petAvatarUrl" :src="order.petAvatarUrl" mode="aspectFill" class="avatar-img"></image>
-						<text v-else>{{ (order.petName || '宠')[0] }}</text>
-					</view>
-					<view class="pet-basic">
-						<text class="pet-name">
-							{{ order.petName || '-' }} 
-							<text class="gender-male" v-if="order.petGender === 'male'">♂</text>
-							<text class="gender-female" v-else-if="order.petGender === 'female'">♀</text>
-						</text>
-						<view class="pet-tags">
-							<text class="mini-tag" v-if="order.petAge">{{ order.petAge }}</text>
-							<text class="mini-tag" v-if="order.petWeight">{{ order.petWeight }}</text>
-							<text class="breed-badge" v-if="order.petBreed">{{ order.petBreed }}</text>
+			<!-- 宠物档案 + 用户信息 -->
+			<view class="info-row-cards">
+				<view class="info-card pet-card">
+					<text class="card-label">宠物档案</text>
+					<view class="pet-header">
+						<view class="pet-avatar">
+							<image v-if="order.petAvatarUrl" :src="order.petAvatarUrl" mode="aspectFill"
+								class="avatar-img"></image>
+							<text v-else>{{ (order.petName || '宠')[0] }}</text>
+						</view>
+						<view class="pet-basic">
+							<text class="pet-name">
+								{{ order.petName || '-' }}
+								<text class="gender-male" v-if="order.petGender === 'male'">♂</text>
+								<text class="gender-female" v-else-if="order.petGender === 'female'">♀</text>
+							</text>
+							<view class="pet-tags">
+								<text class="mini-tag" v-if="order.petAge">{{ order.petAge }}</text>
+								<text class="mini-tag" v-if="order.petWeight">{{ order.petWeight }}</text>
+								<text class="breed-badge" v-if="order.petBreed">{{ order.petBreed }}</text>
+							</view>
 						</view>
 					</view>
-				</view>
-				<view class="pet-attrs">
-					<view class="attr-item">
-						<text class="attr-label">品种</text>
-						<text class="attr-val">{{ order.petBreed || '-' }}</text>
-					</view>
-					<view class="attr-item">
-						<text class="attr-label">疫苗状态</text>
-						<text class="attr-val highlight">{{ order.petVaccine || '-' }}</text>
-					</view>
-					<view class="attr-item full">
-						<text class="attr-label">性格特点</text>
-						<text class="attr-val">{{ order.petCharacter || '-' }}</text>
+					<view class="pet-attrs">
+						<view class="attr-item">
+							<text class="attr-label">品种</text>
+							<text class="attr-val">{{ order.petBreed || '-' }}</text>
+						</view>
+						<view class="attr-item">
+							<text class="attr-label">疫苗状态</text>
+							<text class="attr-val highlight">{{ order.petVaccine || '-' }}</text>
+						</view>
+						<view class="attr-item full">
+							<text class="attr-label">性格特点</text>
+							<text class="attr-val">{{ order.petCharacter || '-' }}</text>
+						</view>
 					</view>
 				</view>
-			</view>
 
-			<view class="info-card user-card">
-				<text class="card-label">用户信息</text>
-				<view class="user-header">
-					<view class="user-avatar">
-						<image v-if="order.userAvatarUrl" :src="order.userAvatarUrl" mode="aspectFill" class="avatar-img"></image>
-						<uni-icons v-else type="person" size="26" color="#aaa"></uni-icons>
+				<view class="info-card user-card">
+					<text class="card-label">用户信息</text>
+					<view class="user-header">
+						<view class="user-avatar">
+							<image v-if="order.userAvatarUrl" :src="order.userAvatarUrl" mode="aspectFill"
+								class="avatar-img"></image>
+							<uni-icons v-else type="person" size="26" color="#aaa"></uni-icons>
+						</view>
+						<view class="user-basic">
+							<text class="user-name-text">{{ order.userName }}</text>
+							<text class="user-phone">{{ order.userPhone }}</text>
+						</view>
 					</view>
-					<view class="user-basic">
-						<text class="user-name-text">{{ order.userName }}</text>
-						<text class="user-phone">{{ order.userPhone }}</text>
+					<view class="service-address-box">
+						<text class="addr-label">服务地址</text>
+						<text class="addr-text">{{ order.address }}</text>
 					</view>
 				</view>
-				<view class="service-address-box">
-					<text class="addr-label">服务地址</text>
-					<text class="addr-text">{{ order.address }}</text>
-				</view>
-			</view>
-		</view>
-
-		<!-- 标签页 -->
-		<view class="detail-tabs-wrap">
-			<view class="tab-nav">
-				<view v-for="tab in tabList" :key="tab.name"
-					:class="['tab-nav-item', { active: activeTab === tab.name }]" @click="activeTab = tab.name">
-					<text>{{ tab.title }}</text>
-				</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">
-						<text class="bi-label">{{ item.label }}</text>
-						<text :class="['bi-val', item.highlight ? 'highlight' : '']">{{ item.value }}</text>
+			<!-- 标签页 -->
+			<view class="detail-tabs-wrap">
+				<view class="tab-nav">
+					<view v-for="tab in tabList" :key="tab.name"
+						:class="['tab-nav-item', { active: activeTab === tab.name }]" @click="activeTab = tab.name">
+						<text>{{ tab.title }}</text>
 					</view>
 				</view>
 
-				<!-- 接送任务详情 -->
-				<block v-if="order.type === 'transport'">
-					<text class="sub-title">接送任务详情</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 class="tab-content" v-if="activeTab === 'base'">
+					<view class="base-info-grid">
+						<view class="bi-item" v-for="item in baseInfoList" :key="item.label">
+							<text class="bi-label">{{ item.label }}</text>
+							<text :class="['bi-val', item.highlight ? 'highlight' : '']">{{ item.value }}</text>
 						</view>
-						<view class="task-body">
-							<view class="task-row">
-								<text class="task-label">起点</text>
-								<text class="task-value">{{ order.fromAddress || '-' }}</text>
+					</view>
+
+					<!-- 接送任务详情 -->
+					<block v-if="order.type === 'transport'">
+						<text class="sub-title">接送任务详情</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="task-row">
-								<text class="task-label">终点</text>
-								<text class="task-value">{{ order.toAddress || '-' }}</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 class="task-row contact-row">
-								<text class="task-value">{{ order.userName }} — {{ order.userPhone }}</text>
+						</view>
+					</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>
+					</block>
+				</view>
+
+				<!-- 指派履约者 -->
+				<view class="tab-content" v-if="activeTab === 'assignee'">
+					<view class="empty-state" v-if="order.statusKey === 'wait_dispatch'">
+						<uni-icons type="clock" size="40" color="#ccc"></uni-icons>
+						<text class="empty-text">等待派单中...</text>
 					</view>
-				</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 class="assignee-card" v-else>
+						<view class="assignee-header">
+							<view class="assignee-avatar">
+								<image v-if="order.assigneeAvatarUrl" :src="order.assigneeAvatarUrl" mode="aspectFill"
+									class="avatar-img"></image>
+								<uni-icons v-else type="person" size="30" color="#aaa"></uni-icons>
+							</view>
+							<view class="assignee-info">
+								<text class="assignee-name">{{ order.assigneeName }}</text>
+								<text class="assignee-phone">联系电话:{{ order.assigneePhone }}</text>
+								<text class="assignee-zone">归属区域:{{ order.assigneeZone }}</text>
+							</view>
 						</view>
 					</view>
-				</block>
-			</view>
-
-			<!-- 指派履约者 -->
-			<view class="tab-content" v-if="activeTab === 'assignee'">
-				<view class="empty-state" v-if="order.statusKey === 'wait_dispatch'">
-					<uni-icons type="clock" size="40" color="#ccc"></uni-icons>
-					<text class="empty-text">等待派单中...</text>
 				</view>
-				<view class="assignee-card" v-else>
-					<view class="assignee-header">
-						<view class="assignee-avatar">
-							<image v-if="order.assigneeAvatarUrl" :src="order.assigneeAvatarUrl" mode="aspectFill" class="avatar-img"></image>
-							<uni-icons v-else type="person" size="30" color="#aaa"></uni-icons>
-						</view>
-						<view class="assignee-info">
-							<text class="assignee-name">{{ order.assigneeName }}</text>
-							<text class="assignee-phone">联系电话:{{ order.assigneePhone }}</text>
-							<text class="assignee-zone">归属区域:{{ order.assigneeZone }}</text>
+
+				<!-- 服务进度 -->
+				<view class="tab-content" v-if="activeTab === 'progress'">
+					<view class="empty-state"
+						v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey) || serviceTimeline.length === 0">
+						<uni-icons type="info" size="40" color="#ccc"></uni-icons>
+						<text class="empty-text">服务尚未开始或暂无进度</text>
+					</view>
+					<view class="timeline" v-else>
+						<view class="tl-item" v-for="(tl, i) in serviceTimeline" :key="i">
+							<view class="tl-dot"></view>
+							<view class="tl-body">
+								<text class="tl-time">{{ tl.time }}</text>
+								<text class="tl-title">{{ tl.title }}</text>
+								<text class="tl-desc">{{ tl.desc }}</text>
+								<view class="tl-media" v-if="tl.media && tl.media.length">
+									<view v-for="(item, idx) in tl.media" :key="idx" class="media-item">
+										<image v-if="item.type === 'image'" mode="aspectFill" :src="item.url"
+											class="p-img" @click="previewImage(item.url, tl.media)"></image>
+										<view v-else-if="item.type === 'video'" class="p-video-box"
+											@click="openVideoPreview(item.url)">
+											<image src="/static/video-placeholder.png" mode="aspectFill" class="p-img"
+												style="background:#000;"></image>
+											<view class="play-icon-overlay">
+												<uni-icons type="videocam-filled" size="30" color="#fff"></uni-icons>
+											</view>
+										</view>
+									</view>
+								</view>
+							</view>
 						</view>
 					</view>
 				</view>
-			</view>
 
-			<!-- 服务进度 -->
-			<view class="tab-content" v-if="activeTab === 'progress'">
-				<view class="empty-state" v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey) || serviceTimeline.length === 0">
-					<uni-icons type="info" size="40" color="#ccc"></uni-icons>
-					<text class="empty-text">服务尚未开始或暂无进度</text>
+				<!-- 订单日志 -->
+				<view class="tab-content" v-if="activeTab === 'log'">
+					<view class="empty-state" v-if="orderLogs.length === 0">
+						<uni-icons type="info" size="40" color="#ccc"></uni-icons>
+						<text class="empty-text">暂无订单日志</text>
+					</view>
+					<view class="timeline" v-else>
+						<view class="tl-item" v-for="(log, i) in orderLogs" :key="i">
+							<view class="tl-dot log-dot"></view>
+							<view class="tl-body">
+								<text class="tl-time">{{ log.time }}</text>
+								<text class="tl-title">{{ log.title }}</text>
+								<text class="tl-desc">{{ log.desc }}</text>
+							</view>
+						</view>
+					</view>
 				</view>
-				<view class="timeline" v-else>
-					<view class="tl-item" v-for="(tl, i) in serviceTimeline" :key="i">
-						<view class="tl-dot"></view>
-						<view class="tl-body">
-							<text class="tl-time">{{ tl.time }}</text>
-							<text class="tl-title">{{ tl.title }}</text>
-							<text class="tl-desc">{{ tl.desc }}</text>
-							<view class="tl-media" v-if="tl.media && tl.media.length">
-								<view v-for="(item, idx) in tl.media" :key="idx" class="media-item">
-									<image v-if="item.type === 'image'" mode="aspectFill" :src="item.url" class="p-img" @click="previewImage(item.url, tl.media)"></image>
-									<view v-else-if="item.type === 'video'" class="p-video-box" @click="openVideoPreview(item.url)">
-										<image src="/static/video-placeholder.png" mode="aspectFill" class="p-img" style="background:#000;"></image>
-										<view class="play-icon-overlay">
-											<uni-icons type="videocam-filled" size="30" color="#fff"></uni-icons>
-										</view>
+
+				<!-- 投诉记录 -->
+				<view class="tab-content" v-if="activeTab === 'complaint'">
+					<view class="empty-state" v-if="complaintList.length === 0">
+						<uni-icons type="info" size="40" color="#ccc"></uni-icons>
+						<text class="empty-text">暂无投诉记录</text>
+					</view>
+					<view class="timeline" v-else>
+						<view class="tl-item" v-for="(complaint, i) in complaintList" :key="i">
+							<view class="tl-dot" style="background: #f56c6c;"></view>
+							<view class="tl-body">
+								<text class="tl-time">{{ complaint.createTime }}</text>
+								<text class="tl-title">投诉原因:{{ complaint.reason }}</text>
+								<view v-if="complaint.photoUrls" class="tl-media">
+									<view v-for="(url, idx) in (complaint.photoUrls || '').split(',')" :key="idx"
+										class="media-item">
+										<image mode="aspectFill" :src="url" class="p-img"
+											@click="previewImage(url, (complaint.photoUrls || '').split(','))"></image>
 									</view>
 								</view>
 							</view>
 						</view>
 					</view>
 				</view>
-			</view>
 
-			<!-- 订单日志 -->
-			<view class="tab-content" v-if="activeTab === 'log'">
-				<view class="empty-state" v-if="orderLogs.length === 0">
-					<uni-icons type="info" size="40" color="#ccc"></uni-icons>
-					<text class="empty-text">暂无订单日志</text>
-				</view>
-				<view class="timeline" v-else>
-					<view class="tl-item" v-for="(log, i) in orderLogs" :key="i">
-						<view class="tl-dot log-dot"></view>
-						<view class="tl-body">
-							<text class="tl-time">{{ log.time }}</text>
-							<text class="tl-title">{{ log.title }}</text>
-							<text class="tl-desc">{{ log.desc }}</text>
+				<!-- 服务变更记录 -->
+				<view class="tab-content" v-if="activeTab === 'serviceChange'">
+					<view class="empty-state" v-if="serviceChangeList.length === 0">
+						<uni-icons type="info" size="40" color="#ccc"></uni-icons>
+						<text class="empty-text">暂无服务变更记录</text>
+					</view>
+					<view class="timeline" v-else>
+						<view class="tl-item" v-for="(change, i) in serviceChangeList" :key="i">
+							<view class="tl-dot" style="background: #409eff;"></view>
+							<view class="tl-body">
+								<text class="tl-time">{{ change.createTime }}</text>
+								<text class="tl-title">服务变更 - {{ change.service }}</text>
+								<text class="tl-desc">申诉理由:{{ change.reason }}</text>
+								<text class="tl-desc" v-if="change.auditStatus === 1"
+									style="color:#67c23a;">审核状态:已通过</text>
+								<text class="tl-desc" v-else-if="change.auditStatus === 2"
+									style="color:#f56c6c;">审核状态:已驳回</text>
+								<text class="tl-desc" v-else style="color:#e6a23c;">审核状态:待审核</text>
+								<view v-if="change.photoUrls" class="tl-media">
+									<view v-for="(url, idx) in (change.photoUrls || '').split(',')" :key="idx"
+										class="media-item">
+										<image mode="aspectFill" :src="url" class="p-img"
+											@click="previewImage(url, (change.photoUrls || '').split(','))"></image>
+									</view>
+								</view>
+							</view>
 						</view>
 					</view>
 				</view>
 			</view>
-		</view>
 
 		</view>
 
@@ -231,9 +292,13 @@
 			</view>
 		</view>
 
-		<!-- 底部取消按钮 -->
-		<view class="cancel-bar safe-bottom" v-if="!loading && ['wait_dispatch', 'wait_accept'].includes(order.statusKey)">
-			<button class="cancel-order-btn" @click="onCancelOrder">取消订单</button>
+		<!-- 底部操作按钮 -->
+		<view class="cancel-bar safe-bottom"
+			v-if="!loading && (['wait_dispatch', 'wait_accept'].includes(order.statusKey) || ['serving', 'done'].includes(order.statusKey) && order.fulfiller)">
+			<button v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey)" class="cancel-order-btn"
+				@click="onCancelOrder">取消订单</button>
+			<button v-if="['serving', 'done'].includes(order.statusKey) && order.fulfiller" class="complaint-btn"
+				@click="onComplaint">投诉订单</button>
 		</view>
 
 		<!-- 自定义取消订单弹窗 -->
@@ -242,8 +307,10 @@
 			<view class="modal-content">
 				<view class="modal-title">提示</view>
 				<view class="modal-body">
-					<view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ order.id }}] 吗?</view>
-					<textarea class="cancel-input" v-model="cancelReason" placeholder="必填,请输入取消原因" placeholder-class="ph-color" :show-confirm-bar="false"></textarea>
+					<view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ order.id }}] 吗?
+					</view>
+					<textarea class="cancel-input" v-model="cancelReason" placeholder="必填,请输入取消原因"
+						placeholder-class="ph-color" :show-confirm-bar="false"></textarea>
 				</view>
 				<view class="modal-footer">
 					<view class="modal-btn btn-cancel" @click="closeCancelModal">取消</view>
@@ -265,6 +332,7 @@ import { getCustomer } from '@/api/archieves/customer'
 import { getFulfiller } from '@/api/fulfiller/fulfiller'
 import { listSubOrderLog } from '@/api/order/subOrderLog'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
+import { listSubOrderAppealByOrderId } from '@/api/order/subOrderAppeal'
 
 const activeTab = ref('base')
 const activeService = ref('transport')
@@ -275,7 +343,9 @@ const tabList = [
 	{ title: '基础信息', name: 'base' },
 	{ title: '履约者', name: 'assignee' },
 	{ title: '服务进度', name: 'progress' },
-	{ title: '订单日志', name: 'log' }
+	{ title: '订单日志', name: 'log' },
+	{ title: '服务变更', name: 'serviceChange' },
+	{ title: '投诉记录', name: 'complaint' }
 ]
 
 const currentServiceName = computed(() => {
@@ -286,8 +356,8 @@ const currentServiceName = computed(() => {
 const order = reactive({
 	id: '',
 	code: '',
-	statusKey: 'serving',
-	statusText: '服务',
+	statusKey: 'pending_service',
+	statusText: '服务',
 	status: 2,
 	petName: '',
 	petBreed: '',
@@ -330,6 +400,7 @@ const order = reactive({
 const orderLogsData = ref([])
 const fulfillerLogsData = ref([])
 const complaintList = ref([])
+const serviceChangeList = ref([])
 
 const loadOrderDetail = async (id) => {
 	if (!id) return
@@ -366,11 +437,12 @@ const loadOrderDetail = async (id) => {
 			if (res.usrPet) tasks.push(loadPetInfo(res.usrPet))
 			if (res.usrCustomer) tasks.push(loadCustomerInfo(res.usrCustomer))
 			if (res.fulfiller) tasks.push(loadFulfillerInfo(res.fulfiller))
-			
+
 			await Promise.all([
 				...tasks,
 				loadOrderLogs(id),
-				loadComplaints(id)
+				loadComplaints(id),
+				loadServiceChanges(id)
 			])
 		}
 	} catch (error) {
@@ -457,13 +529,23 @@ const loadComplaints = async (id) => {
 	}
 }
 
+const loadServiceChanges = async (id) => {
+	try {
+		const res = await listSubOrderAppealByOrderId(id)
+		serviceChangeList.value = res || []
+	} catch (error) {
+		console.error('加载服务变更记录失败:', error)
+		serviceChangeList.value = []
+	}
+}
+
 const getStatusKey = (status) => {
-	const map = { 0: 'wait_dispatch', 1: 'wait_accept', 2: 'serving', 3: 'confirming', 4: 'done', 5: 'cancel' }
+	const map = { 0: 'wait_dispatch', 1: 'wait_accept', 2: 'pending_service', 3: 'serving', 4: 'done', 5: 'cancel', 6: 'rejected', 7: 'closed' }
 	return map[status] || 'serving'
 }
 
 const getStatusName = (status) => {
-	const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
+	const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消', 6: '已拒绝', 7: '已关闭' }
 	return map[status] || '-'
 }
 
@@ -500,14 +582,7 @@ onLoad((options) => {
 })
 
 const progressSteps = computed(() => {
-	const status = order.status
-	const steps = [
-		{ label: '商户下单', time: '' },
-		{ label: '运营派单', time: '' },
-		{ label: '履约接单', time: '' },
-		{ label: '服务中', time: '' },
-		{ label: '已完成', time: '' }
-	]
+	const status = Number(order.status)
 	const getSystemLogTime = (step) => {
 		const log = (orderLogsData.value || []).find(l => parseInt(l.step) === step)
 		return log ? (log.createTime || log.time) : ''
@@ -516,58 +591,76 @@ const progressSteps = computed(() => {
 		const log = (fulfillerLogsData.value || []).find(l => parseInt(l.step) === step)
 		return log ? (log.createTime || log.time) : ''
 	}
-	const fulfillerCustomLogTime = () => {
-		const log = [...(fulfillerLogsData.value || [])].reverse().find(l => {
-			const s = parseInt(l.step)
-			return s >= 1 && s <= 98
-		})
-		return log ? (log.createTime || log.time) : ''
-	}
-
-	let active = 0
 
+	// 已取消 — 特殊两步流程
 	if (status === 5) {
 		const cancelTime = getSystemLogTime(5) || order.cancelTime || ''
 		return [
-			{ label: '商户下单', time: getSystemLogTime(0) || order.createTime || '', done: true, active: false },
-			{ label: '已取消', time: cancelTime, done: true, active: true }
-		].map(s => ({ ...s, time: s.time ? s.time.substring(5, 16) : '' }))
+			{ label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
+			{ label: '已取消', time: cancelTime.substring(5, 16), done: true, active: true }
+		]
 	}
 
-	steps[0].time = getSystemLogTime(0) || order.createTime || ''
-	if (steps[0].time) active = 1
-	
-	steps[1].time = getSystemLogTime(1) || ''
-	if (steps[1].time || status >= 1) active = 2
-
-	steps[2].time = getSystemLogTime(2) || getFulfillerLogTime(0) || ''
-	if (status === 1) {
-		steps[2].label = '待履约者接单'
-	} else if (status >= 2) {
-		steps[2].label = '履约者已接单'
-		if (steps[2].time) active = 3
+	// 已拒绝 — 特殊两步流程
+	if (status === 6) {
+		return [
+			{ label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
+			{ label: '已拒绝', time: '', done: true, active: true }
+		]
 	}
 
-	const cTime = fulfillerCustomLogTime()
-	steps[3].time = getSystemLogTime(3) || cTime || '' 
-	if (status === 2) {
-		steps[3].label = '待服务'
-		if (cTime) active = 4
-	} else if (status >= 3) {
-		steps[3].label = '服务进行中'
-		if (steps[3].time) active = 4
+	// 已关闭 — 特殊两步流程
+	if (status === 7) {
+		return [
+			{ label: '商户下单', time: (getSystemLogTime(0) || order.createTime || '').substring(5, 16), done: true, active: false },
+			{ label: '已关闭', time: '', done: true, active: true }
+		]
 	}
 
-	if (status === 4 || getSystemLogTime(4) || getFulfillerLogTime(99)) {
-		steps[4].time = getSystemLogTime(4) || getFulfillerLogTime(99) || ''
-		active = 5
+	// 六步流程(全四字):商户下单 → 运营派单 → 履约接单 → 等待服务 → 服务进行 → 订单完成
+	const steps = [
+		{ label: '商户下单', time: getSystemLogTime(0) || order.createTime || '' },
+		{ label: '运营派单', time: getSystemLogTime(1) || '' },
+		{ label: '履约接单', time: getSystemLogTime(2) || getFulfillerLogTime(0) || '' },
+		{ label: '等待服务', time: getSystemLogTime(3) || '' },
+		{ label: '服务进行', time: getFulfillerLogTime(99) || '' },
+		{ label: '订单完成', time: getSystemLogTime(4) || '' }
+	]
+
+	let active = 1 // 默认停在「运营派单」
+
+	switch (status) {
+		case 0: // 待派单
+			active = 1
+			break
+		case 1: // 待接单
+			active = 2
+			steps[2].label = '等待接单'
+			break
+		case 2: // 待服务
+			active = 3
+			steps[2].label = '已确认接'
+			steps[3].label = '等待服务'
+			break
+		case 3: // 服务中(到达打卡)
+			active = 4
+			steps[2].label = '已确认接'
+			steps[3].label = '已到达点'
+			steps[4].label = '服务进行'
+			break
+		case 4: // 已完成
+			active = 6
+			steps[2].label = '已确认接'
+			steps[3].label = '已到达点'
+			steps[4].label = '服务进行'
+			break
 	}
 
 	return steps.map((s, i) => ({
 		label: s.label,
 		time: s.time ? s.time.substring(5, 16) : '',
 		done: i < active,
-		active: i === active 
+		active: i === active
 	}))
 })
 
@@ -609,7 +702,19 @@ const closeVideoPreview = () => {
 }
 
 const previewImage = (url, mediaList) => {
-	const imgUrls = mediaList.filter(m => m.type === 'image').map(m => m.url);
+	let imgUrls = []
+	if (Array.isArray(mediaList)) {
+		if (mediaList.length > 0 && typeof mediaList[0] === 'string') {
+			// 直接的URL数组(服务变更记录)
+			imgUrls = mediaList
+		} else {
+			// 包含type属性的对象数组
+			imgUrls = mediaList.filter(m => m.type === 'image').map(m => m.url)
+		}
+	} else if (typeof mediaList === 'string') {
+		// 单个URL
+		imgUrls = [mediaList]
+	}
 	uni.previewImage({
 		current: url,
 		urls: imgUrls
@@ -667,7 +772,7 @@ const confirmCancelOrder = async () => {
 		uni.hideLoading()
 		uni.showToast({ title: '订单已取消', icon: 'success' })
 		showCancelModal.value = false
-		
+
 		// 重新初始化页面数据
 		loadOrderDetail(order.id)
 	} catch (error) {
@@ -676,6 +781,12 @@ const confirmCancelOrder = async () => {
 		uni.showToast({ title: '取消失败', icon: 'none' })
 	}
 }
+
+const onComplaint = () => {
+	uni.navigateTo({
+		url: `/pages/my/complaint/submit/index?orderId=${order.id}&fulfillerId=${order.fulfiller}&orderCode=${order.code}`
+	})
+}
 </script>
 
 <style lang="scss" scoped>
@@ -719,6 +830,11 @@ const confirmCancelOrder = async () => {
 	color: #ff9800;
 }
 
+.badge-pending_service {
+	background: #e3f2fd;
+	color: #49a3ff;
+}
+
 .badge-serving {
 	background: #e3f2fd;
 	color: #2196f3;
@@ -849,10 +965,23 @@ const confirmCancelOrder = async () => {
 	margin-bottom: 20rpx;
 }
 
-.pet-avatar, .user-avatar, .assignee-avatar {
-	width: 80rpx; height: 80rpx; border-radius: 50%; background: #f0f2f5;
-	display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0;
-	.avatar-img { width: 100%; height: 100%; }
+.pet-avatar,
+.user-avatar,
+.assignee-avatar {
+	width: 80rpx;
+	height: 80rpx;
+	border-radius: 50%;
+	background: #f0f2f5;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	overflow: hidden;
+	flex-shrink: 0;
+
+	.avatar-img {
+		width: 100%;
+		height: 100%;
+	}
 }
 
 .pet-basic {
@@ -1296,13 +1425,33 @@ const confirmCancelOrder = async () => {
 	background: transparent;
 	border-radius: 48rpx;
 	line-height: 92rpx;
-	&::after { border: none; }
+
+	&::after {
+		border: none;
+	}
+}
+
+.complaint-btn {
+	width: 100%;
+	height: 96rpx;
+	font-size: 32rpx;
+	font-weight: 600;
+	border: 2rpx solid #ff9800;
+	color: #ff9800;
+	background: transparent;
+	border-radius: 48rpx;
+	line-height: 92rpx;
+
+	&::after {
+		border: none;
+	}
 }
 
 /* 骨架屏动画与样式 */
 .skeleton-page {
 	padding: 24rpx;
 }
+
 .skeleton-box {
 	background: #e0e0e0;
 	border-radius: 16rpx;
@@ -1311,33 +1460,40 @@ const confirmCancelOrder = async () => {
 	background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
 	background-size: 400% 100%;
 }
+
 @keyframes skeleton-shimmer {
 	0% {
 		background-position: 100% 0;
 	}
+
 	100% {
 		background-position: -100% 0;
 	}
 }
+
 .skeleton-header {
 	height: 80rpx;
 	border-radius: 20rpx;
 }
+
 .skeleton-progress {
 	height: 120rpx;
 	border-radius: 28rpx;
 }
+
 .skeleton-row-cards {
 	display: flex;
 	gap: 20rpx;
 	margin-bottom: 24rpx;
 }
+
 .skeleton-card {
 	flex: 1;
 	height: 280rpx;
 	border-radius: 28rpx;
 	margin-bottom: 0;
 }
+
 .skeleton-content {
 	height: 500rpx;
 	border-radius: 28rpx;
@@ -1347,11 +1503,13 @@ const confirmCancelOrder = async () => {
 .fade-in {
 	animation: fadeIn 0.4s ease-out forwards;
 }
+
 @keyframes fadeIn {
 	from {
 		opacity: 0;
 		transform: translateY(10rpx);
 	}
+
 	to {
 		opacity: 1;
 		transform: translateY(0);
@@ -1365,6 +1523,7 @@ const confirmCancelOrder = async () => {
 	gap: 16rpx;
 	margin-top: 16rpx;
 }
+
 .media-item {
 	width: 160rpx;
 	height: 160rpx;
@@ -1373,10 +1532,13 @@ const confirmCancelOrder = async () => {
 	background: #f0f0f0;
 	position: relative;
 }
-.p-img, .p-video-box {
+
+.p-img,
+.p-video-box {
 	width: 100%;
 	height: 100%;
 }
+
 .play-icon-overlay {
 	position: absolute;
 	top: 50%;
@@ -1405,10 +1567,12 @@ const confirmCancelOrder = async () => {
 	align-items: center;
 	justify-content: center;
 }
+
 .preview-video {
 	width: 100%;
 	height: 60vh;
 }
+
 .close-video-btn {
 	position: absolute;
 	top: 100rpx;
@@ -1434,6 +1598,7 @@ const confirmCancelOrder = async () => {
 	align-items: center;
 	justify-content: center;
 }
+
 .modal-mask {
 	position: absolute;
 	top: 0;
@@ -1442,6 +1607,7 @@ const confirmCancelOrder = async () => {
 	height: 100%;
 	background-color: rgba(0, 0, 0, 0.5);
 }
+
 .modal-content {
 	position: relative;
 	width: 80%;
@@ -1450,6 +1616,7 @@ const confirmCancelOrder = async () => {
 	overflow: hidden;
 	z-index: 1000;
 }
+
 .modal-title {
 	padding: 30rpx 0 20rpx;
 	text-align: center;
@@ -1457,9 +1624,11 @@ const confirmCancelOrder = async () => {
 	font-weight: bold;
 	color: #333;
 }
+
 .modal-body {
 	padding: 10rpx 40rpx 30rpx;
 }
+
 .cancel-input {
 	width: 100%;
 	height: 160rpx;
@@ -1470,13 +1639,16 @@ const confirmCancelOrder = async () => {
 	box-sizing: border-box;
 	color: #333;
 }
+
 .ph-color {
 	color: #999;
 }
+
 .modal-footer {
 	display: flex;
 	border-top: 2rpx solid #EEEEEE;
 }
+
 .modal-btn {
 	flex: 1;
 	height: 90rpx;
@@ -1485,10 +1657,12 @@ const confirmCancelOrder = async () => {
 	font-size: 30rpx;
 	font-weight: 500;
 }
+
 .btn-cancel {
 	color: #666;
 	border-right: 2rpx solid #EEEEEE;
 }
+
 .btn-confirm {
 	color: #2196f3;
 }

+ 1 - 1
pages/order/list/index.vue

@@ -21,7 +21,7 @@
 					</view>
 				</picker>
 				<view class="search-wrap">
-					<uni-icons type="search" size="14" color="#999"></uni-icons>
+					<uni-icons type="search" size="14" color="#999" @click="onSearch"></uni-icons>
 					<input class="search-input" v-model="searchValue" placeholder="订单号/商户/宠主/手机号"
 						placeholder-class="placeholder-style" @confirm="onSearch" />
 				</view>

+ 9 - 9
pages/service/all/index.vue

@@ -28,7 +28,7 @@
 						<view class="service-cell" v-for="(service, sIndex) in cat.items" :key="sIndex"
 							@click="onServiceClick(service)">
 							<view class="icon-wrapper">
-								<image :src="service.icon" class="service-icon" mode="aspectFit"></image>
+								<image :src="service.icon" class="service-icon" mode="aspectFill"></image>
 							</view>
 							<text class="service-name">{{ service.name }}</text>
 						</view>
@@ -76,12 +76,12 @@ const filteredData = computed(() => {
 	const keyword = searchValue.value.trim().toLowerCase()
 	if (!keyword) return data
 	return data.map(group => ({
-			...group,
-			categories: group.categories.map(cat => ({
-				...cat,
-				items: cat.items.filter(s => s.name.toLowerCase().includes(keyword))
-			})).filter(cat => cat.items.length > 0)
-		})).filter(group => group.categories.some(cat => cat.items.length > 0))
+		...group,
+		categories: group.categories.map(cat => ({
+			...cat,
+			items: cat.items.filter(s => s.name.toLowerCase().includes(keyword))
+		})).filter(cat => cat.items.length > 0)
+	})).filter(group => group.categories.some(cat => cat.items.length > 0))
 })
 
 const currentCategories = computed(() => {
@@ -261,8 +261,8 @@ const onServiceClick = (service) => {
 }
 
 .service-icon {
-	width: 56rpx;
-	height: 56rpx;
+	width: 100%;
+	height: 100%;
 }
 
 .service-name {

+ 69 - 2
pages/service/detail/index.vue

@@ -19,6 +19,7 @@
 					<text class="price-symbol">¥</text>
 					<text class="price-num">{{ serviceData.price }}</text>
 					<text class="price-unit">{{ serviceData.unit }}</text>
+					<text class="price-suffix">起</text>
 				</view>
 			</view>
 			<text class="service-name-text">{{ serviceData.title }}</text>
@@ -37,7 +38,9 @@
 
 			<view class="tab-content" v-if="activeTab === 'intro'">
 				<text class="content-title">服务介绍</text>
-				<rich-text :nodes="serviceData.intro" class="content-text"></rich-text>
+				<view class="rich-container">
+					<rich-text :nodes="processedIntro" class="content-text"></rich-text>
+				</view>
 				<view class="intro-images" v-if="serviceData.introImages && serviceData.introImages.length">
 					<image v-for="(img, idx) in serviceData.introImages" :key="idx" :src="img" class="intro-img"
 						mode="widthFix"></image>
@@ -46,7 +49,9 @@
 
 			<view class="tab-content" v-if="activeTab === 'notice'">
 				<text class="content-title">下单须知</text>
-				<rich-text :nodes="serviceData.notice" class="content-text"></rich-text>
+				<view class="rich-container">
+					<rich-text :nodes="processedNotice" class="content-text"></rich-text>
+				</view>
 			</view>
 		</view>
 
@@ -109,6 +114,31 @@ const serviceData = computed(() => {
 	}
 })
 
+/**
+ * 富文本预处理:限制图片宽度、移除溢出样式,兼容H5富文本
+ */
+const processRichText = (html) => {
+	if (!html) return ''
+	let processed = html
+	// 1. 给 img 标签强制添加 max-width:100% + height:auto
+	processed = processed.replace(/<img([^>]*)>/gi, (match, attrs) => {
+		let cleanAttrs = attrs
+			.replace(/\s*width\s*=\s*["'][^"']*["']/gi, '')
+			.replace(/\s*height\s*=\s*["'][^"']*["']/gi, '')
+			.replace(/\s*style\s*=\s*["'][^"']*["']/gi, '')
+		return `<img${cleanAttrs} style="max-width: 100%; height: auto; display: block;">`
+	})
+	// 2. 处理 table/pre/code 等可能溢出的块级元素
+	processed = processed.replace(
+		/<(table|pre|code)([^>]*)>/gi,
+		(match, tag, attrs) => `<${tag}${attrs} style="max-width: 100%; overflow-x: auto; word-break: break-word;">`
+	)
+	return processed
+}
+
+const processedIntro = computed(() => processRichText(serviceData.value.intro))
+const processedNotice = computed(() => processRichText(serviceData.value.notice))
+
 const goToOrderApply = () => {
 	// @Author: Antigravity
 	if (!serviceInfo.value) return;
@@ -231,6 +261,12 @@ const goToOrderApply = () => {
 	margin-left: 4rpx;
 }
 
+.price-suffix {
+	font-size: 24rpx;
+	color: #999;
+	margin-left: 2rpx;
+}
+
 .bought-count {
 	font-size: 24rpx;
 	color: #999;
@@ -305,6 +341,12 @@ const goToOrderApply = () => {
 	border-radius: 4rpx;
 }
 
+.rich-container {
+	overflow: hidden;
+	word-break: break-word;
+	line-height: 1.8;
+}
+
 .content-text {
 	font-size: 28rpx;
 	color: #555;
@@ -346,3 +388,28 @@ const goToOrderApply = () => {
 	line-height: 92rpx;
 }
 </style>
+
+<!-- 非 scoped 样式:rich-text 内部元素需要穿透生效 -->
+<style lang="scss">
+.rich-container img {
+	max-width: 100% !important;
+	height: auto !important;
+	display: block;
+}
+.rich-container table,
+.rich-container pre,
+.rich-container code {
+	max-width: 100% !important;
+	overflow-x: auto;
+	word-break: break-word;
+}
+.rich-container p {
+	margin-bottom: 12px;
+}
+.rich-container h1, .rich-container h2, .rich-container h3,
+.rich-container h4, .rich-container h5, .rich-container h6 {
+	margin-top: 16px;
+	margin-bottom: 8px;
+	line-height: 1.4;
+}
+</style>

BIN
static/images/index-hand.png


BIN
static/images/index-header.png


BIN
static/images/index-middle.png


BIN
static/images/index-symbol.png


BIN
unpackage/cache/apk/__UNI__F19BBAD_cm.apk


+ 1 - 1
unpackage/cache/apk/apkurl

@@ -1 +1 @@
-https://app.liuyingyong.cn/build/download/293df480-3d58-11f1-82a4-d7eab18c4a86
+https://app.liuyingyong.cn/build/download/0b622d70-3ee7-11f1-8303-55599e2b379a

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/apk/cmManifestCache.json


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/app-service.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/manifest.json


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/index/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/login/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/submit/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/add/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/edit/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/add/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/edit/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/apply/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/detail/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/list/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/service/all/index.css


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/service/detail/index.css


BIN
unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-hand.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-header.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-middle.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/static/images/index-symbol.png


BIN
unpackage/release/apk/__UNI__F19BBAD__20260427180258.apk


+ 1 - 0
utils/config.js

@@ -3,6 +3,7 @@
 // 接口基础地址
 // export const BASE_URL = 'http://192.168.1.118:8080'
 export const BASE_URL = 'http://111.228.46.254/api'
+// export const BASE_URL = 'http://192.168.1.205:8080'
 
 // 请求超时时间(毫秒)
 export const TIMEOUT = 10000

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно