Bläddra i källkod

二期基本完成

Huanyi 1 timme sedan
förälder
incheckning
7def8b9141
100 ändrade filer med 4410 tillägg och 638 borttagningar
  1. 53 0
      api/archieves/customer.js
  2. 41 0
      api/archieves/pet.js
  3. 28 0
      api/fulfiller/complaint.js
  4. 8 0
      api/fulfiller/fulfiller.js
  5. 14 0
      api/order/order.js
  6. 13 5
      api/order/subOrder.js
  7. 10 1
      api/service/list.js
  8. 12 0
      api/system/appSetting.js
  9. 14 0
      api/system/customerServiceSetting.js
  10. 8 0
      api/system/dict/data.js
  11. 12 0
      api/system/region.js
  12. 14 0
      api/system/store.js
  13. 33 0
      api/system/user.js
  14. 10 0
      json/customer.json
  15. 17 4
      manifest.json
  16. 7 0
      pages.json
  17. 113 78
      pages/index/index.vue
  18. 12 0
      pages/login/index.vue
  19. 46 14
      pages/my/agreement/detail/index.vue
  20. 16 15
      pages/my/agreement/list/index.vue
  21. 145 56
      pages/my/complaint/list/index.vue
  22. 270 30
      pages/my/complaint/submit/index.vue
  23. 120 13
      pages/my/fee/statistics/index.vue
  24. 244 2
      pages/my/index.vue
  25. 206 25
      pages/my/pet/add/index.vue
  26. 356 27
      pages/my/pet/detail/index.vue
  27. 258 91
      pages/my/pet/edit/index.vue
  28. 78 19
      pages/my/pet/list/index.vue
  29. 13 3
      pages/my/settings/account-delete/index.vue
  30. 14 8
      pages/my/settings/change-password/index.vue
  31. 1 1
      pages/my/settings/index.vue
  32. 184 0
      pages/my/settings/profile/index.vue
  33. 262 20
      pages/my/user/add/index.vue
  34. 93 17
      pages/my/user/detail/index.vue
  35. 246 17
      pages/my/user/edit/index.vue
  36. 108 23
      pages/my/user/list/index.vue
  37. 479 60
      pages/order/apply/index.vue
  38. 495 88
      pages/order/detail/index.vue
  39. 121 20
      pages/order/list/index.vue
  40. 15 1
      pages/service/detail/index.vue
  41. BIN
      unpackage/cache/apk/__UNI__F19BBAD_cm.apk
  42. 1 0
      unpackage/cache/apk/apkurl
  43. 0 0
      unpackage/cache/apk/cmManifestCache.json
  44. 4 0
      unpackage/cache/certdata
  45. 4 0
      unpackage/cache/cloudcertificate/certini
  46. BIN
      unpackage/cache/cloudcertificate/package.keystore
  47. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-hdpi.png
  48. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xhdpi.png
  49. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xxhdpi.png
  50. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xxxhdpi.png
  51. 15 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappautomator.js
  52. 31 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappchooselocation.js
  53. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/__uniapperror.png
  54. 31 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappopenlocation.js
  55. 31 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniapppicker.js
  56. 6 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappquill.js
  57. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappquillimageresize.js
  58. 31 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappscan.js
  59. BIN
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappsuccess.png
  60. 24 0
      unpackage/cache/wgt/__UNI__F19BBAD/__uniappview.html
  61. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/app-config-service.js
  62. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/app-config.js
  63. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/app-service.js
  64. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/app.css
  65. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/manifest.json
  66. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/index/index.css
  67. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/login/index.css
  68. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/agreement/detail/index.css
  69. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/agreement/list/index.css
  70. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/list/index.css
  71. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/submit/index.css
  72. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/fee/statistics/index.css
  73. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/index.css
  74. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/add/index.css
  75. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/detail/index.css
  76. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/edit/index.css
  77. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/list/index.css
  78. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/review/list/index.css
  79. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/account-delete/index.css
  80. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/change-password/index.css
  81. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/index.css
  82. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/profile/index.css
  83. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/add/index.css
  84. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/detail/index.css
  85. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/edit/index.css
  86. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/list/index.css
  87. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/apply/index.css
  88. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/detail/index.css
  89. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/order/list/index.css
  90. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/service/all/index.css
  91. 0 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/service/detail/index.css
  92. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/service/review/index.css
  93. 1 0
      unpackage/cache/wgt/__UNI__F19BBAD/pages/store/apply/index.css
  94. 6 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/agreement.svg
  95. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-accept.svg
  96. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-cancel.svg
  97. 3 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-done.svg
  98. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-service.svg
  99. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-wait.svg
  100. 4 0
      unpackage/cache/wgt/__UNI__F19BBAD/static/icon/pet-archive.svg

+ 53 - 0
api/archieves/customer.js

@@ -6,3 +6,56 @@ export function getCustomer(id) {
     method: 'get'
   })
 }
+
+export function listAllCustomer(query) {
+  return request({
+    url: '/archieves/customer/listAll',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增用户
+export function addCustomer(data) {
+  return request({
+    url: '/archieves/customer',
+    method: 'post',
+    data: data
+  })
+}
+
+export function listCustomer(query) {
+  return request({
+    url: '/archieves/customer/list',
+    method: 'get',
+    data: query
+  })
+}
+
+export function changeCustomerStatus(id, status) {
+  return request({
+    url: '/archieves/customer/changeStatus',
+    method: 'put',
+    data: { id, status }
+  })
+}
+
+// 编辑用户
+export function updateCustomer(data) {
+  return request({
+    url: '/archieves/customer',
+    method: 'put',
+    data: data
+  })
+}
+/**
+ * 下单页宠主列表
+ * @Author: Antigravity
+ */
+export function listCustomerOnOrder(query) {
+  return request({
+    url: '/archieves/customer/listOnOrder',
+    method: 'get',
+    params: query
+  })
+}

+ 41 - 0
api/archieves/pet.js

@@ -6,3 +6,44 @@ export function getPet(id) {
     method: 'get'
   })
 }
+
+export function listPet(query) {
+  return request({
+    url: '/archieves/pet/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function delPet(id) {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'delete'
+  })
+}
+
+export function addPet(data) {
+  return request({
+    url: '/archieves/pet',
+    method: 'post',
+    data: data
+  })
+}
+
+export function updatePet(data) {
+  return request({
+    url: '/archieves/pet',
+    method: 'put',
+    data: data
+  })
+}
+/**
+ * 按用户查询宠物列表
+ * @Author: Antigravity
+ */
+export function listPetByUser(userId) {
+  return request({
+    url: '/archieves/pet/listByUser/' + userId,
+    method: 'get'
+  })
+}

+ 28 - 0
api/fulfiller/complaint.js

@@ -1,5 +1,6 @@
 import { request } from '@/utils/request'
 
+// 根据订单获取投诉记录
 export function listComplaintByOrder(orderId) {
   return request({
     url: '/fulfiller/complaint/listByOrder',
@@ -7,3 +8,30 @@ export function listComplaintByOrder(orderId) {
     params: { orderId }
   })
 }
+
+// 分页获取履约者的投诉记录
+export function listComplaintByFulfiller(query) {
+  return request({
+    url: '/fulfiller/complaint/pageByFulfiller',
+    method: 'get',
+    params: query
+  })
+}
+
+// 获取我创建的投诉记录
+export function listMyComplaint(query) {
+  return request({
+    url: '/fulfiller/complaint/pageMy',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增投诉 @Author: Antigravity
+export function addComplaint(data) {
+  return request({
+    url: '/fulfiller/complaint/add',
+    method: 'post',
+    data: data
+  })
+}

+ 8 - 0
api/fulfiller/fulfiller.js

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

+ 14 - 0
api/order/order.js

@@ -0,0 +1,14 @@
+import { request } from '@/utils/request'
+
+/**
+ * 创建订单
+ * @param data 参数
+ * @Author: Antigravity
+ */
+export function createOrder(data) {
+  return request({
+    url: '/order/order/create',
+    method: 'post',
+    data: data
+  })
+}

+ 13 - 5
api/order/subOrder.js

@@ -19,7 +19,7 @@ export function getSubOrderInfo(id) {
 export function dispatchSubOrder(data) {
   return request({
     url: '/order/subOrder/dispatch',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -27,7 +27,7 @@ export function dispatchSubOrder(data) {
 export function cancelSubOrder(data) {
   return request({
     url: '/order/subOrder/cancel',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -35,7 +35,7 @@ export function cancelSubOrder(data) {
 export function remarkSubOrder(data) {
   return request({
     url: '/order/subOrder/remark',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -43,7 +43,7 @@ export function remarkSubOrder(data) {
 export function confirmSubOrder(data) {
   return request({
     url: '/order/subOrder/confirm',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
@@ -51,7 +51,15 @@ export function confirmSubOrder(data) {
 export function nursingSummarySubOrder(data) {
   return request({
     url: '/order/subOrder/nursingSummary',
-    method: 'post',
+    method: 'put',
     data: data
   })
 }
+
+export function listOnFeeStatistic(query) {
+  return request({
+    url: '/order/subOrder/listOnFeeStatistic',
+    method: 'get',
+    params: query
+  })
+}

+ 10 - 1
api/service/list.js

@@ -1,8 +1,17 @@
 import { request } from '@/utils/request'
 
-export function listAll() {
+/**
+ * 获取所有服务类型列表
+ * @Author: Antigravity
+ */
+export function listAllService() {
   return request({
     url: '/service/list/listAll',
     method: 'get'
   })
 }
+
+// 保持兼容性
+export function listAll() {
+  return listAllService();
+}

+ 12 - 0
api/system/appSetting.js

@@ -0,0 +1,12 @@
+import { request } from '@/utils/request'
+
+/**
+ * 获取APP配置
+ * @Author: Antigravity
+ */
+export function getAppSetting(id) {
+  return request({
+    url: `/system/appSetting/${id}`,
+    method: 'get'
+  })
+}

+ 14 - 0
api/system/customerServiceSetting.js

@@ -0,0 +1,14 @@
+import { request } from '@/utils/request'
+
+
+/**
+ * 获取客服配置
+ * @Author: Antigravity
+ * @param {number} id 应用端ID
+ */
+export function getCustomerServiceSetting(id) {
+  return request({
+    url: '/system/customerServiceSetting/' + id,
+    method: 'get'
+  })
+}

+ 8 - 0
api/system/dict/data.js

@@ -0,0 +1,8 @@
+import { request } from '@/utils/request'
+
+export function getDicts(dictType) {
+  return request({
+    url: '/system/dict/data/type/' + dictType,
+    method: 'get'
+  })
+}

+ 12 - 0
api/system/region.js

@@ -0,0 +1,12 @@
+import { request } from '@/utils/request'
+
+/**
+ * 查询地区树结构
+ * @Author: Antigravity
+ */
+export function listRegionTree() {
+  return request({
+    url: '/system/region/listTree',
+    method: 'get'
+  })
+}

+ 14 - 0
api/system/store.js

@@ -0,0 +1,14 @@
+import { request } from '@/utils/request'
+
+/**
+ * 下单时查询门店列表
+ * @param query { name, site, serviceId, pageNum, pageSize }
+ * @Author: Antigravity
+ */
+export function listStoreOnOrder(query) {
+  return request({
+    url: '/system/store/listOnOrder',
+    method: 'get',
+    params: query
+  })
+}

+ 33 - 0
api/system/user.js

@@ -6,3 +6,36 @@ export function getInfo() {
     method: 'get'
   })
 }
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+  const data = {
+    oldPassword,
+    newPassword
+  }
+  return request({
+    url: '/system/user/profile/updatePwd',
+    method: 'put',
+    data: data
+  })
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+  return request({
+    url: '/system/user/profile',
+    method: 'put',
+    data: data
+  })
+}
+
+/**
+ * 账号注销
+ * @Author: Antigravity
+ */
+export function cancelUser() {
+  return request({
+    url: '/system/user/cancel',
+    method: 'delete'
+  })
+}

+ 10 - 0
json/customer.json

@@ -0,0 +1,10 @@
+{
+  "houseTypeOptions": [
+    { "label": "楼梯", "value": "stairs" },
+    { "label": "电梯", "value": "elevator" }
+  ],
+  "entryMethodOptions": [
+    { "label": "密码开门", "value": "password" },
+    { "label": "钥匙开门", "value": "key" }
+  ]
+}

+ 17 - 4
manifest.json

@@ -2,8 +2,8 @@
     "name" : "好萌友",
     "appid" : "__UNI__F19BBAD",
     "description" : "宠物服务商家端",
-    "versionName" : "0.0.1",
-    "versionCode" : 1,
+    "versionName" : "1.0.0",
+    "versionCode" : 10000,
     "transformPx" : false,
     "app-plus" : {
         "usingComponents" : true,
@@ -36,8 +36,21 @@
                     "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
                 ]
             },
-            "ios" : {},
-            "sdkConfigs" : {},
+            "ios" : {
+                "dSYMs" : false
+            },
+            "sdkConfigs" : {
+                "payment" : {
+                    "alipay" : {
+                        "__platform__" : [ "ios", "android" ]
+                    },
+                    "google" : {},
+                    "stripe" : {
+                        "__platform__" : [ "android" ],
+                        "returnURL_ios" : ""
+                    }
+                }
+            },
             "icons" : {
                 "android" : {
                     "hdpi" : "unpackage/res/icons/72x72.png",

+ 7 - 0
pages.json

@@ -160,6 +160,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/my/settings/profile/index",
+			"style": {
+				"navigationBarTitleText": "个人信息",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/my/settings/account-delete/index",
 			"style": {

+ 113 - 78
pages/index/index.vue

@@ -2,10 +2,10 @@
 	<view class="home-page">
 		<!-- 搜索栏 -->
 		<view class="search-wrapper">
-			<view class="location-box" @click="onLocationClick">
+<!-- <view class="location-box" @click="onLocationClick">
 				<text class="location-text">昆明市</text>
 				<uni-icons type="bottom" size="10" color="#fff"></uni-icons>
-			</view>
+			</view> -->
 			<view class="search-bar" @click="onSearchClick">
 				<uni-icons type="search" size="16" color="#999"></uni-icons>
 				<text class="search-placeholder">搜索宠物名/主人</text>
@@ -32,32 +32,26 @@
 			</view>
 		</view>
 
-		<!-- 服务分类卡片 -->
-		<view class="service-grid">
-			<!-- 左侧大块:宠物托运 -->
-			<view class="grid-item item-transport-large" @click="goToDetail('托运')">
-				<view class="text-info">
-					<text class="grid-title">宠物托运</text>
-					<text class="grid-desc">专车接送 · 全程监护</text>
+		<!-- 重构后的对称服务栅格布局 -->
+		<view class="new-service-grid" v-if="services.length >= 3">
+			<!-- 顶部主打服务 (Hero Card) -->
+			<view class="hero-card" @click="goToDetail(services[0])">
+				<view class="card-content">
+					<text class="card-title">{{ services[0].name }}</text>
+					<text class="card-desc">{{ services[0].remark }}</text>
 				</view>
-				<image src="/static/images/transport-3d.png" class="large-3d-icon" mode="aspectFit"></image>
+				<image :src="services[0].iconUrl" class="hero-icon" mode="aspectFit"></image>
 			</view>
 
-			<!-- 右侧两个小块 -->
-			<view class="grid-right">
-				<view class="grid-item item-wash-small" @click="goToDetail('洗护')">
-					<view class="text-info">
-						<text class="grid-title">宠物洗护</text>
-						<text class="sub-text">深度清洁</text>
-					</view>
-					<image src="/static/images/wash-3d.png" class="small-3d-icon" mode="aspectFit"></image>
-				</view>
-				<view class="grid-item item-feed-small" @click="goToDetail('喂养')">
-					<view class="text-info">
-						<text class="grid-title">宠物喂养</text>
-						<text class="sub-text">贴心陪伴</text>
+			<!-- 底部双列服务 (Secondary Cards) -->
+			<view class="card-row">
+				<view class="sub-card" v-for="(item, index) in services.slice(1, 3)" :key="index"
+					@click="goToDetail(item)">
+					<view class="card-content">
+						<text class="sub-title">{{ item.name }}</text>
+						<text class="sub-desc">{{ item.remark }}</text>
 					</view>
-					<image src="/static/images/feed-3d.png" class="small-3d-icon" mode="aspectFit"></image>
+					<image :src="item.iconUrl" class="sub-icon" mode="aspectFit"></image>
 				</view>
 			</view>
 		</view>
@@ -72,8 +66,8 @@
 		</view>
 
 		<view class="recommend-list">
-			<view class="recommend-card" v-for="(item, index) in services.slice(0, 5)" :key="index"
-				@click="goToDetail(item.name)">
+			<view class="recommend-card" v-for="(item, index) in services.slice(3, 8)" :key="index"
+				@click="goToDetail(item)">
 				<image :src="item.iconUrl" class="item-img" mode="aspectFill"></image>
 				<view class="item-info">
 					<view class="item-header">
@@ -95,24 +89,41 @@
 import { ref } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
 import customTabbar from '@/components/custom-tabbar/index.vue'
-import mockData from '@/mock/index.json'
-import { listAll } from '@/api/service/list'
+import { listAllService } from '@/api/service/list'
+import { getAppSetting } from '@/api/system/appSetting'
 import serviceModeEnum from '@/json/serviceMode.json'
 
-const bannerImages = mockData.bannerImages
+const bannerImages = ref([])
 const services = ref([])
 
 const fetchServices = async () => {
 	try {
-		const res = await listAll()
-		services.value = res || []
+		// @Author: Antigravity
+		const res = await listAllService()
+		// 兼容直接返回数组或返回包装对象的情况
+		services.value = res.rows || res || []
 	} catch (error) {
 		console.error("Failed to fetch services", error)
 	}
 }
 
+// 获取首页轮播图配置 @Author: Antigravity
+const fetchBanners = async () => {
+	try {
+		// 获取“好萌友”端配置 (ID 2) @Author: Antigravity
+		const res = await getAppSetting(2)
+		const data = res.data || res
+		if (data && data.homeBannerUrls) {
+			bannerImages.value = data.homeBannerUrls.split(',').filter(Boolean)
+		}
+	} catch (error) {
+		console.error('Failed to fetch banners', error)
+	}
+}
+
 onShow(() => {
 	fetchServices()
+	fetchBanners()
 })
 
 const onLocationClick = () => {
@@ -127,13 +138,17 @@ const goToServices = () => {
 	uni.reLaunch({ url: '/pages/service/all/index' })
 }
 
-const goToDetail = (type) => {
-	if (type === '托运') {
+const goToDetail = (item) => {
+	// @Author: Antigravity
+	// 存储完整的服务数据,供详情页读取
+	uni.setStorageSync('currentService', item)
+	
+	if (item.name === '托运') {
 		uni.reLaunch({ url: '/pages/service/all/index' })
 		return
 	}
 	uni.navigateTo({
-		url: `/pages/service/detail/index?service=${type}`
+		url: `/pages/service/detail/index?service=${item.name}`
 	})
 }
 </script>
@@ -221,78 +236,98 @@ const goToDetail = (type) => {
 	color: #ed6a0c;
 }
 
-.service-grid {
+.new-service-grid {
 	padding: 0 32rpx;
 	display: flex;
+	flex-direction: column;
 	gap: 24rpx;
-	margin-bottom: 32rpx;
+	margin-bottom: 24rpx;
 }
 
-.grid-item {
-	border-radius: 24rpx;
-	padding: 32rpx;
+.hero-card {
+	height: 260rpx;
+	background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);
+	border-radius: 32rpx;
+	padding: 40rpx;
 	position: relative;
 	overflow: hidden;
+	display: flex;
+	align-items: center;
+	box-shadow: 0 12rpx 32rpx rgba(37, 99, 235, 0.18);
 }
 
-.item-transport-large {
-	flex: 1.2;
-	background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
-	height: 360rpx;
+.card-content {
+	z-index: 1;
+	display: flex;
+	flex-direction: column;
 }
 
-.grid-right {
-	flex: 1;
+.card-title {
+	font-size: 38rpx;
+	color: #fff;
+	font-weight: 800;
+	letter-spacing: 2rpx;
+}
+
+.card-desc {
+	font-size: 24rpx;
+	color: rgba(255, 255, 255, 0.9);
+	margin-top: 12rpx;
+}
+
+.hero-icon {
+	position: absolute;
+	right: 20rpx;
+	bottom: -10rpx;
+	width: 240rpx;
+	height: 240rpx;
+	opacity: 0.85;
+}
+
+.card-row {
 	display: flex;
-	flex-direction: column;
 	gap: 24rpx;
 }
 
-.item-wash-small {
-	background: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
-	height: 168rpx;
+.sub-card {
+	flex: 1;
+	height: 200rpx;
+	border-radius: 32rpx;
+	padding: 30rpx;
+	position: relative;
+	overflow: hidden;
+	box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
 }
 
-.item-feed-small {
-	background: linear-gradient(135deg, #fab1a0 0%, #e17055 100%);
-	height: 168rpx;
+.sub-card:nth-child(1) {
+	background: linear-gradient(135deg, #34d399 0%, #059669 100%);
 }
 
-.grid-title {
-	display: block;
-	font-size: 32rpx;
-	color: #fff;
-	font-weight: 800;
+.sub-card:nth-child(2) {
+	background: linear-gradient(135deg, #fb923c 0%, #ea580c 100%);
 }
 
-.grid-desc {
+.sub-title {
+	font-size: 30rpx;
+	color: #fff;
+	font-weight: bold;
 	display: block;
-	margin-top: 8rpx;
-	font-size: 22rpx;
-	color: rgba(255, 255, 255, 0.9);
 }
 
-.sub-text {
-	display: block;
+.sub-desc {
 	font-size: 20rpx;
-	color: rgba(255, 255, 255, 0.8);
-	margin-top: 4rpx;
-}
-
-.large-3d-icon {
-	position: absolute;
-	right: -10rpx;
-	bottom: -10rpx;
-	width: 260rpx;
-	height: 260rpx;
+	color: rgba(255, 255, 255, 0.85);
+	margin-top: 8rpx;
+	display: block;
 }
 
-.small-3d-icon {
+.sub-icon {
 	position: absolute;
-	right: -10rpx;
-	bottom: -10rpx;
-	width: 150rpx;
-	height: 150rpx;
+	right: 4rpx;
+	bottom: 4rpx;
+	width: 100rpx;
+	height: 100rpx;
+	opacity: 0.8;
 }
 
 .section-header {

+ 12 - 0
pages/login/index.vue

@@ -82,6 +82,7 @@ import { ref } from 'vue'
 import navBar from '@/components/nav-bar/index.vue'
 import policyDialog from '@/components/policy-dialog/index.vue'
 import { login } from '@/api/auth'
+import { getInfo } from '@/api/system/user'
 import { getAgreement } from '@/api/system/agreement'
 import { AgreementType } from '@/enums/agreementType'
 import { DEFAULT_HEADERS } from '@/utils/config'
@@ -126,6 +127,17 @@ const onSubmit = async () => {
 
 		if (res.access_token) {
 			uni.setStorageSync('token', res.access_token)
+			
+			// 获取用户信息并存储 tenantId
+			try {
+				const userRes = await getInfo()
+				if (userRes && userRes.user && userRes.user.tenantId) {
+					uni.setStorageSync('tenantId', userRes.user.tenantId)
+				}
+			} catch (e) {
+				console.error('获取用户信息失败', e)
+			}
+			
 			uni.showToast({ title: '登录成功', icon: 'success' })
 			setTimeout(() => {
 				uni.reLaunch({ url: '/pages/index/index' })

+ 46 - 14
pages/my/agreement/detail/index.vue

@@ -1,33 +1,65 @@
 <template>
 	<view class="agreement-detail-page">
-		<view class="content-card">
-			<text class="title">{{ title }}</text>
-			<text class="update-time">最后更新:2024-02-01</text>
+		<nav-bar :title="title"></nav-bar>
+		<view class="content-card" v-if="!loading">
+			<text class="title">{{ agreementData.title || title }}</text>
 			<rich-text :nodes="contentHtml" class="rich-content"></rich-text>
 		</view>
+		<view v-else class="loading-state">
+			<text>协议内容加载中...</text>
+		</view>
 	</view>
 </template>
 
 <script setup>
+// @Author: Antigravity
 import { ref } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
+import { getAgreement } from '@/api/system/agreement'
+import navBar from '@/components/nav-bar/index.vue'
 
 const title = ref('协议详情')
-const contentHtml = ref('<p>协议内容加载中...</p>')
+const agreementData = ref({})
+const contentHtml = ref('')
+const loading = ref(true)
 
-onLoad((options) => {
+onLoad(async (options) => {
 	if (options.title) title.value = decodeURIComponent(options.title)
-	contentHtml.value = `<p style="font-size:14px;color:#555;line-height:2;">本协议是您与宠物服务平台之间关于使用平台服务所订立的协议。请您仔细阅读以下全部内容。</p>
-	<p style="font-size:14px;color:#555;line-height:2;"><b>一、服务说明</b><br/>平台为宠物主人提供接送、喂遛、洗护等上门服务的信息撮合及交易支持。</p>
-	<p style="font-size:14px;color:#555;line-height:2;"><b>二、用户责任</b><br/>用户应确保提供的宠物信息真实有效,并对宠物的健康状况负责。</p>
-	<p style="font-size:14px;color:#555;line-height:2;"><b>三、隐私保护</b><br/>我们重视您的隐私,所有个人信息将按照隐私政策进行处理。</p>
-	<p style="font-size:14px;color:#555;line-height:2;"><b>四、免责条款</b><br/>因不可抗力导致的服务中断,平台不承担赔偿责任。</p>`
+	if (options.id) {
+		fetchAgreement(options.id)
+	}
 })
+
+const fetchAgreement = async (id) => {
+	loading.value = true
+	try {
+		const res = await getAgreement(id)
+		if (res) {
+			agreementData.value = res
+			// 解码 Base64 内容 (Admin端采用 btoa(unescape(encodeURIComponent(content))) 编码)
+			if (res.content) {
+				try {
+					contentHtml.value = decodeURIComponent(escape(atob(res.content)))
+				} catch (e) {
+					// 如果解码失败,回显原始内容
+					contentHtml.value = res.content
+				}
+			} else {
+				contentHtml.value = '暂无协议内容'
+			}
+		}
+	} catch (error) {
+		console.error('获取协议详情失败', error)
+	} finally {
+		loading.value = false
+	}
+}
 </script>
 
 <style lang="scss" scoped>
-.agreement-detail-page { min-height: 100vh; background: #f7f8fa; padding: 24rpx; }
-.content-card { background: #fff; border-radius: 24rpx; padding: 40rpx 32rpx; }
-.title { display: block; font-size: 36rpx; font-weight: 800; color: #333; margin-bottom: 12rpx; }
-.update-time { display: block; font-size: 24rpx; color: #999; margin-bottom: 32rpx; padding-bottom: 24rpx; border-bottom: 1rpx solid #f5f5f5; }
+.agreement-detail-page { min-height: 100vh; background: #f7f8fa; padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); }
+.content-card { background: #fff; margin: 24rpx; border-radius: 24rpx; padding: 40rpx 32rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); }
+.title { display: block; font-size: 36rpx; font-weight: 800; color: #333; margin-bottom: 32rpx; position: relative; padding-bottom: 24rpx; border-bottom: 1rpx solid #f5f5f5; }
+.rich-content { display: block; font-size: 28rpx; color: #555; line-height: 1.8; word-break: break-all; }
+.loading-state { text-align: center; padding: 100rpx 0; color: #999; font-size: 28rpx; }
 </style>

+ 16 - 15
pages/my/agreement/list/index.vue

@@ -5,7 +5,6 @@
 			<view class="agreement-item" v-for="item in agreements" :key="item.id" @click="goToDetail(item)">
 				<view class="item-info">
 					<text class="item-title">{{ item.title }}</text>
-					<text class="item-date">更新时间:{{ item.updateTime }}</text>
 				</view>
 				<uni-icons type="right" size="14" color="#ccc"></uni-icons>
 			</view>
@@ -14,15 +13,23 @@
 </template>
 
 <script setup>
+// @Author: Antigravity
 import { ref } from 'vue'
 import navBar from '@/components/nav-bar/index.vue'
+
+// 协议列表,ID 参考 admin web 配置及后端 SysAgreementController
 const agreements = ref([
-	{ id: 1, title: '隐私政策', updateTime: '2024-01-15' },
-	{ id: 2, title: '用户服务协议', updateTime: '2024-01-15' },
-	{ id: 3, title: '商家托运协议', updateTime: '2024-02-01' },
-	{ id: 4, title: '宠物洗护服务规范', updateTime: '2024-02-01' }
+	{ id: 2, title: '隐私政策' },
+	{ id: 1, title: '用户服务协议' },
+	{ id: 4, title: '商家托运协议' },
+	{ id: 5, title: '宠物洗护服务规范' } // 根据需求预留 ID:5
 ])
-const goToDetail = (item) => uni.navigateTo({ url: `/pages/my/agreement/detail/index?title=${item.title}` })
+
+const goToDetail = (item) => {
+	uni.navigateTo({ 
+		url: `/pages/my/agreement/detail/index?id=${item.id}&title=${encodeURIComponent(item.title)}` 
+	})
+}
 </script>
 
 <style lang="scss" scoped>
@@ -41,7 +48,8 @@ const goToDetail = (item) => uni.navigateTo({ url: `/pages/my/agreement/detail/i
 	background: #fff;
 	border-radius: 20rpx;
 	padding: 32rpx;
-	margin-bottom: 16rpx;
+	margin-bottom: 20rpx;
+	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
 }
 
 .item-info {
@@ -52,13 +60,6 @@ const goToDetail = (item) => uni.navigateTo({ url: `/pages/my/agreement/detail/i
 	display: block;
 	font-size: 30rpx;
 	color: #333;
-	font-weight: 600;
-	margin-bottom: 8rpx;
-}
-
-.item-date {
-	display: block;
-	font-size: 24rpx;
-	color: #999;
+	font-weight: 500;
 }
 </style>

+ 145 - 56
pages/my/complaint/list/index.vue

@@ -1,48 +1,115 @@
 <template>
 	<view class="complaint-list-page">
-		<nav-bar title="我的评价"></nav-bar>
+		<nav-bar title="投诉管理"></nav-bar>
 		<view class="list-container">
 			<view class="empty-state" v-if="historyList.length === 0">
 				<text>暂无投诉记录</text>
 			</view>
 			<view v-else class="history-card" v-for="item in historyList" :key="item.id">
 				<view class="card-header">
-					<text class="order-no">订单号:{{ item.orderNo }}</text>
-					<text :class="['status-text', item.status === '已处理' ? 'text-green' : 'text-orange']">{{ item.status
-						}}</text>
+					<view class="left-box">
+						<text class="status-tag" :class="item.praiseFlag ? 'praise' : 'complaint'">{{ item.praiseFlag ? '赞' : '不赞' }}</text>
+						<text class="order-no">单号:{{ item.orderCode || '-' }}</text>
+					</view>
+					<text class="status-text">已提交</text>
 				</view>
 				<view class="card-body">
-					<view class="rate-row">
-						<text class="label">评价评分:</text>
-						<text class="stars">{{ '★'.repeat(item.rating) }}{{ '☆'.repeat(5 - item.rating) }}</text>
+					<view class="reason-row">
+						<text class="label">{{ item.praiseFlag ? '理由:' : '不赞原因:' }}</text>
+						<text class="reason-content">{{ item.reason || '未填写内容' }}</text>
 					</view>
-					<text class="content-text">{{ item.content }}</text>
-					<view class="images-preview" v-if="item.images && item.images.length">
-						<image v-for="(img, idx) in item.images" :key="idx" :src="img" class="preview-img"
-							mode="aspectFill"></image>
+					
+					<!-- 凭证图片展示 @Author: Antigravity -->
+					<view class="photo-grid" v-if="item.photos">
+						<image 
+							v-for="(url, index) in item.photos.split(',')" 
+							:key="index" 
+							:src="url" 
+							mode="aspectFill"
+							class="photo-item"
+							@click="previewImage(item.photos.split(','), index)"
+						></image>
 					</view>
 				</view>
 				<view class="card-footer">
-					<text class="time">{{ item.time }}</text>
+					<text class="time">{{ item.createTime || '-' }}</text>
 				</view>
 			</view>
+
+			
+			<view v-if="historyList.length > 0 && !hasMore" class="no-more">没有更多了</view>
 		</view>
 	</view>
 </template>
 
 <script setup>
+// @Author: Antigravity
 import { ref } from 'vue'
+import { onShow, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
-import complaintMockData from '@/mock/complaint.json'
-
-const historyList = ref(complaintMockData)
+import { listMyComplaint } from '@/api/fulfiller/complaint'
+import { getInfo } from '@/api/system/user'
+
+const historyList = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+
+const loadData = async (isLoadMore = false) => {
+	try {
+		uni.showNavigationBarLoading()
+		const res = await listMyComplaint({
+			pageNum: pageNum.value,
+			pageSize: pageSize.value
+		})
+		
+		const rows = res.rows || []
+		if (isLoadMore) {
+			historyList.value = [...historyList.value, ...rows]
+		} else {
+			historyList.value = rows
+		}
+		hasMore.value = historyList.value.length < (res.total || 0)
+	} catch (error) {
+		console.error('加载投诉记录失败', error)
+	} finally {
+		uni.hideNavigationBarLoading()
+		uni.stopPullDownRefresh()
+	}
+}
+
+onShow(() => {
+	pageNum.value = 1
+	loadData()
+})
+
+onPullDownRefresh(() => {
+	pageNum.value = 1
+	loadData()
+})
+
+onReachBottom(() => {
+	if (hasMore.value) {
+		pageNum.value++
+		loadData(true)
+	}
+})
+
+const previewImage = (urls, index) => {
+
+	uni.previewImage({
+		urls: urls,
+		current: index
+	})
+}
 </script>
 
 <style lang="scss" scoped>
 .complaint-list-page {
 	min-height: 100vh;
 	background: #f7f8fa;
-	padding-bottom: 40rpx;
+	padding-bottom: constant(safe-area-inset-bottom);
+	padding-bottom: env(safe-area-inset-bottom);
 }
 
 .list-container {
@@ -58,82 +125,104 @@ const historyList = ref(complaintMockData)
 
 .history-card {
 	background: #fff;
-	border-radius: 24rpx;
-	padding: 32rpx;
+	border-radius: 20rpx;
+	padding: 28rpx;
 	margin-bottom: 24rpx;
+	box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
 }
 
 .card-header {
 	display: flex;
 	justify-content: space-between;
-	border-bottom: 1rpx solid #f5f5f5;
+	align-items: center;
 	padding-bottom: 20rpx;
-	margin-bottom: 24rpx;
-	font-size: 26rpx;
-}
-
-.order-no {
-	color: #666;
+	border-bottom: 1rpx solid #f5f5f5;
+	margin-bottom: 20rpx;
 }
 
-.status-text {
-	font-weight: bold;
+.left-box {
+	display: flex;
+	align-items: center;
+	gap: 16rpx;
 }
 
-.text-green {
-	color: #4caf50;
+.order-no {
+	font-size: 26rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.status-tag {
+	font-size: 20rpx;
+	padding: 2rpx 12rpx;
+	border-radius: 6rpx;
+	font-weight: 700;
+	&.praise {
+		color: #52c41a;
+		background: #f6ffed;
+		border: 1rpx solid #b7eb8f;
+	}
+	&.complaint {
+		color: #ff4d4f;
+		background: #fff1f0;
+		border: 1rpx solid #ffa39e;
+	}
 }
 
-.text-orange {
-	color: #ff9800;
+.status-text {
+	font-size: 24rpx;
+	color: #999;
 }
 
-.rate-row {
+.reason-row {
 	display: flex;
-	align-items: center;
-	margin-bottom: 16rpx;
+	flex-direction: column;
+	gap: 12rpx;
 }
 
 .label {
-	font-size: 26rpx;
-	color: #333;
-}
-
-.stars {
-	color: #f7ca3e;
-	font-size: 28rpx;
-	margin-left: 8rpx;
+	font-size: 24rpx;
+	color: #999;
 }
 
-.content-text {
+.reason-content {
 	font-size: 28rpx;
 	color: #333;
-	line-height: 1.5;
-	margin-bottom: 20rpx;
+	line-height: 1.6;
 }
 
-.images-preview {
-	display: flex;
-	gap: 16rpx;
-	flex-wrap: wrap;
+.photo-grid {
+	display: grid;
+	grid-template-columns: repeat(3, 1fr);
+	gap: 12rpx;
+	margin-top: 20rpx;
 }
 
-.preview-img {
-	width: 120rpx;
-	height: 120rpx;
-	border-radius: 8rpx;
-	border: 1rpx solid #eee;
+.photo-item {
+	width: 100%;
+	height: 200rpx;
+	border-radius: 12rpx;
+	background: #f5f5f5;
 }
 
 .card-footer {
+	margin-top: 24rpx;
+	padding-top: 20rpx;
+	border-top: 1rpx solid #f5f5f5;
 	display: flex;
 	justify-content: flex-end;
-	border-top: 1rpx solid #f5f5f5;
-	padding-top: 20rpx;
 }
 
 .time {
 	font-size: 24rpx;
 	color: #999;
 }
+
+.no-more {
+	text-align: center;
+	font-size: 24rpx;
+	color: #ccc;
+	padding: 20rpx 0;
+}
 </style>
+

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

@@ -1,55 +1,295 @@
 <template>
 	<view class="complaint-submit-page">
-		<view class="form-card">
-			<view class="form-item">
-				<text class="form-label">关联订单</text>
-				<text class="form-value">{{ orderId || '无' }}</text>
+		<nav-bar :title="praiseFlag ? '发表评价' : '提交投诉'" color="#000"></nav-bar>
+
+		<view class="page-content">
+			<!-- 评价类型切换 -->
+			<view class="type-card card-shadow">
+				<view class="type-item" :class="{ 'active': !praiseFlag }" @click="praiseFlag = false">
+					<view class="icon-wrap bad">
+						<uni-icons type="info-filled" size="24" color="#F44336" v-if="!praiseFlag"></uni-icons>
+						<uni-icons type="info" size="24" color="#999" v-else></uni-icons>
+					</view>
+					<text class="type-text">不赞</text>
+				</view>
+				<view class="type-divider"></view>
+				<view class="type-item" :class="{ 'active': praiseFlag }" @click="praiseFlag = true">
+					<view class="icon-wrap good">
+						<uni-icons type="hand-up-filled" size="24" color="#4CAF50" v-if="praiseFlag"></uni-icons>
+						<uni-icons type="hand-up" size="24" color="#999" v-else></uni-icons>
+					</view>
+					<text class="type-text">赞</text>
+				</view>
+			</view>
+
+			<!-- 内容输入 -->
+			<view class="form-section card-shadow">
+				<view class="section-title">
+					<text class="title-text">{{ praiseFlag ? '评价详情' : '投诉原因' }}</text>
+					<text class="title-tip">必填</text>
+				</view>
+				<textarea class="content-textarea" v-model="reason" 
+					:placeholder="praiseFlag ? '请记录您的满意点,帮助履约师提升服务质量...' : '请详细描述您遇到的问题,我们会尽快为您处理...'" 
+					maxlength="500"></textarea>
+				<view class="word-count">{{ reason.length }}/500</view>
 			</view>
-			<view class="form-item">
-				<text class="form-label">投诉评分</text>
-				<view class="rate-wrap">
-					<text v-for="i in 5" :key="i" :class="['star', { active: i <= rating }]" @click="rating = i">★</text>
+
+			<!-- 图片上传 -->
+			<view class="form-section card-shadow">
+				<view class="section-title">
+					<text class="title-text">凭证图片</text>
+					<text class="title-tip gray">最多6张</text>
+				</view>
+				<view class="upload-grid">
+					<view class="upload-item" v-for="(img, index) in imageList" :key="index">
+						<image :src="img" mode="aspectFill" @click="previewImage(index)"></image>
+						<view class="delete-icon" @click.stop="removeImage(index)">
+							<uni-icons type="closeempty" size="12" color="#fff"></uni-icons>
+						</view>
+					</view>
+					<view class="upload-add" v-if="imageList.length < 6" @click="chooseImage">
+						<uni-icons type="plusempty" size="32" color="#ccc"></uni-icons>
+						<text class="add-text">上传凭证</text>
+					</view>
 				</view>
 			</view>
-			<view class="form-item column">
-				<text class="form-label">投诉内容</text>
-				<textarea class="form-textarea" v-model="content" placeholder="请详细描述您的投诉内容..." maxlength="500"></textarea>
+
+			<!-- 订单信息提示 -->
+			<view class="order-info-bar">
+				<text>关联订单:{{ orderCode }}</text>
 			</view>
 		</view>
-		<button class="submit-btn" @click="onSubmit">提交投诉</button>
+
+		<!-- 底部按钮 -->
+		<view class="bottom-bar">
+			<button class="submit-btn" :class="{ 'is-praise': praiseFlag }" @click="handleConfirmSubmit" :loading="submitting">
+				{{ praiseFlag ? '确认赞' : '确认不赞' }}
+			</button>
+		</view>
 	</view>
 </template>
 
 <script setup>
 import { ref } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
+import navBar from '@/components/nav-bar/index.vue'
+import { addComplaint } from '@/api/fulfiller/complaint'
+import { BASE_URL, DEFAULT_HEADERS } from '@/utils/config'
+
+/**
+ * 投诉/评价提交页面 (参考网页端设计)
+ * @Author: Antigravity
+ */
 
 const orderId = ref('')
-const rating = ref(3)
-const content = ref('')
+const orderCode = ref('')
+const fulfillerId = ref('')
+const praiseFlag = ref(false)
+const reason = ref('')
+const imageList = ref([])
+const submitting = ref(false)
 
 onLoad((options) => {
 	if (options.orderId) orderId.value = options.orderId
+	if (options.orderCode) orderCode.value = options.orderCode
+	if (options.fulfillerId) fulfillerId.value = options.fulfillerId
 })
 
-const onSubmit = () => {
-	if (!content.value) { uni.showToast({ title: '请填写投诉内容', icon: 'none' }); return }
-	uni.showToast({ title: '投诉已提交', icon: 'success' })
-	setTimeout(() => uni.navigateBack(), 1000)
+// 选择图片
+const chooseImage = () => {
+    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' })
+    })
+}
+
+const previewImage = (index) => {
+    uni.previewImage({
+        current: index,
+        urls: imageList.value
+    })
+}
+
+const removeImage = (index) => {
+    imageList.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
+    }
 }
 </script>
 
 <style lang="scss" scoped>
-.complaint-submit-page { min-height: 100vh; background: #f7f8fa; padding: 24rpx; padding-bottom: 160rpx; }
-.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; }
-.form-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 1rpx solid #f5f5f5; }
-.form-item.column { flex-direction: column; align-items: flex-start; }
-.form-item:last-child { border-bottom: none; }
-.form-label { width: 180rpx; font-size: 28rpx; color: #333; flex-shrink: 0; margin-bottom: 16rpx; }
-.form-value { flex: 1; font-size: 28rpx; color: #666; text-align: right; }
-.rate-wrap { display: flex; gap: 12rpx; }
-.star { font-size: 40rpx; color: #ddd; }
-.star.active { color: #f7ca3e; }
-.form-textarea { width: 100%; font-size: 28rpx; color: #333; height: 240rpx; background: #f9f9f9; border-radius: 16rpx; padding: 20rpx; }
-.submit-btn { margin-top: 48rpx; width: 100%; height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
+.complaint-submit-page {
+	min-height: 100vh;
+	background-color: #f8f9fb;
+}
+
+.page-content {
+	padding: 30rpx;
+}
+
+.card-shadow {
+	background: #fff;
+	border-radius: 24rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
+	margin-bottom: 30rpx;
+}
+
+/* 类型切换卡片 */
+.type-card {
+	display: flex;
+	align-items: center;
+	height: 120rpx;
+	padding: 0 20rpx;
+    .type-item {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 16rpx;
+        transition: all 0.3s;
+        height: 80rpx;
+        border-radius: 16rpx;
+        &.active {
+            background-color: #fdfdfd;
+            .type-text { font-weight: bold; color: #333; }
+        }
+        .icon-wrap {
+            width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center;
+        }
+        .type-text { font-size: 28rpx; color: #999; }
+    }
+    .type-divider { width: 1rpx; height: 40rpx; background-color: #eee; }
+}
+
+/* 表单板块 */
+.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; }
+        }
+	}
+	.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;
+	}
+	.word-count { text-align: right; font-size: 22rpx; color: #ccc; margin-top: 12rpx; }
+}
+
+/* 图片上传网格 */
+.upload-grid {
+	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%; }
+		.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;
+		}
+	}
+	.upload-add {
+		aspect-ratio: 1; border-radius: 12rpx; border: 2rpx dashed #eee;
+		display: flex; flex-direction: column; align-items: center; justify-content: center;
+		background: #fafafa;
+		.add-text { font-size: 22rpx; color: #ccc; margin-top: 8rpx; }
+	}
+}
+
+.order-info-bar {
+	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);
+	.submit-btn {
+		height: 96rpx; line-height: 96rpx; border-radius: 48rpx; 
+		background: #333; color: #fff; font-size: 32rpx; font-weight: bold; border: none;
+		transition: all 0.3s;
+		&.is-praise { background: #FF9500; color: #fff; }
+		&:active { transform: scale(0.98); opacity: 0.9; }
+	}
+}
 </style>

+ 120 - 13
pages/my/fee/statistics/index.vue

@@ -4,11 +4,11 @@
 		<!-- 顶部背景 -->
 		<view class="header-bg">
 			<view class="total-title">总服务费 (元)</view>
-			<view class="total-amount">500.00</view>
+			<view class="total-amount">{{ totalFee }}</view>
 			<view class="date-picker-wrap">
-				<picker mode="date" fields="day" @change="onDateChange">
+				<picker mode="date" fields="month" @change="onDateChange">
 					<view class="date-range">
-						<text>2026/02/25 - 2026/03/27</text>
+						<text>{{ currentDateText }}</text>
 						<uni-icons type="bottom" size="14" color="#fff"></uni-icons>
 					</view>
 				</picker>
@@ -18,17 +18,17 @@
 		<!-- 统计卡片 -->
 		<view class="stats-card">
 			<view class="stats-item">
-				<text class="stats-value">3</text>
+				<text class="stats-value">{{ totalCount }}</text>
 				<text class="stats-label">单数</text>
 			</view>
 			<view class="stats-divider"></view>
 			<view class="stats-item">
-				<text class="stats-value">280.00</text>
+				<text class="stats-value">{{ maxFee }}</text>
 				<text class="stats-label">最高单笔</text>
 			</view>
 			<view class="stats-divider"></view>
 			<view class="stats-item">
-				<text class="stats-value">166.67</text>
+				<text class="stats-value">{{ avgFee }}</text>
 				<text class="stats-label">平均单笔</text>
 			</view>
 		</view>
@@ -57,18 +57,125 @@
 </template>
 
 <script setup>
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
 import NavBar from '@/components/nav-bar/index.vue'
+import { listOnFeeStatistic } from '@/api/order/subOrder'
+import { listAll } from '@/api/service/list'
 
-const orderDetails = ref([
-	{ title: '宠物寄养(1天)', time: '2026-03-19 09:00', amount: '100.00', orderNo: 'ORD20260319011' },
-	{ title: '狗狗全套美容', time: '2026-03-17 10:30', amount: '280.00', orderNo: 'ORD20260317022' },
-	{ title: '猫咪洗澡护肤', time: '2026-03-15 14:00', amount: '120.00', orderNo: 'ORD20260315001' }
-])
+const orderDetails = ref([])
+const totalFee = ref('0.00')
+const totalCount = ref(0)
+const maxFee = ref('0.00')
+const avgFee = ref('0.00')
+
+const serviceMap = ref({})
+
+const currentDateText = ref('本月')
+const queryParams = ref({
+	pageNum: 1,
+	pageSize: 9999,
+	'params[beginTime]': '',
+	'params[endTime]': ''
+})
+
+const getList = async () => {
+	try {
+		uni.showLoading({ title: '加载中' })
+		const res = await listOnFeeStatistic(queryParams.value)
+		
+		const list = res.rows || []
+		let sum = 0
+		let max = 0
+		
+		orderDetails.value = list.map(item => {
+			// 后端金额是分,转化为元
+			const amount = (item.orderCommission || 0) / 100
+			sum += amount
+			if (amount > max) {
+				max = amount
+			}
+            
+            // 组装标题 (优先团购套餐,否则通过serviceId获取服务名称)
+            let finalTitle = item.groupPurchasePackageName || ''
+            if (!finalTitle) {
+                const sId = (item.service || item.serviceId || '').toString().trim()
+                if (sId) {
+                    finalTitle = serviceMap.value[sId] || '未知服务'
+                }
+            }
+
+			return {
+				title: finalTitle || '基础服务',
+				time: item.createTime || '',
+				amount: amount.toFixed(2),
+				orderNo: item.code || ''
+			}
+		})
+		
+		totalCount.value = list.length
+		totalFee.value = sum.toFixed(2)
+		maxFee.value = max.toFixed(2)
+		avgFee.value = list.length > 0 ? (sum / list.length).toFixed(2) : '0.00'
+		
+	} catch (e) {
+		console.error(e)
+	} finally {
+		uni.hideLoading()
+	}
+}
 
 const onDateChange = (e) => {
-	console.log('date change', e.detail.value)
+	const val = e.detail.value // "YYYY-MM"
+	currentDateText.value = val
+	// 简单的处理将月份转为当月起始和结束时间
+	const date = new Date(val + '-01')
+	const year = date.getFullYear()
+	const month = date.getMonth()
+	const lastDay = new Date(year, month + 1, 0).getDate()
+	
+	queryParams.value['params[beginTime]'] = `${val}-01 00:00:00`
+	queryParams.value['params[endTime]'] = `${val}-${lastDay} 23:59:59`
+	
+	getList()
+}
+
+const getServices = async () => {
+    try {
+        const res = await listAll()
+        const dataList = res.data || res.rows || (Array.isArray(res) ? res : [])
+        if (Array.isArray(dataList)) {
+            const tempMap = {}
+            dataList.forEach(s => {
+                if (s.id) {
+                    tempMap[String(s.id).trim()] = s.name
+                }
+            })
+            serviceMap.value = tempMap
+        }
+    } catch (e) {
+        console.error('获取服务类型失败', e)
+    }
 }
+
+onLoad(async () => {
+	// 默认本月
+	const now = new Date()
+	const year = now.getFullYear()
+	let month = now.getMonth() + 1
+	if (month < 10) month = '0' + month
+	
+	const val = `${year}-${month}`
+	currentDateText.value = val
+	const lastDay = new Date(year, month, 0).getDate()
+	
+	queryParams.value['params[beginTime]'] = `${val}-01 00:00:00`
+	queryParams.value['params[endTime]'] = `${val}-${lastDay} 23:59:59`
+	
+    uni.showLoading({ title: '加载中' })
+    await getServices()
+	getList()
+})
 </script>
 
 <style lang="scss" scoped>

+ 244 - 2
pages/my/index.vue

@@ -48,18 +48,73 @@
 			<text>~ 感谢您的陪伴 ~</text>
 		</view>
 
+		<!-- 客服中心弹窗 @Author: Antigravity -->
+		<view class="service-popup-mask" v-if="showServicePopup" @click="closeServicePopup">
+			<view class="service-popup" @click.stop>
+				<view class="service-header">
+					<text class="service-title">联系客服</text>
+					<uni-icons type="closeempty" size="20" color="#999" @click="closeServicePopup"></uni-icons>
+				</view>
+
+				<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>
+					<text class="qr-desc">点击查看大图</text>
+				</view>
+
+				<view class="service-list">
+					<view class="service-row" @click="openOnlineService">
+						<view class="service-row-icon-box green">
+							<uni-icons type="chat-filled" size="24" color="#fff"></uni-icons>
+						</view>
+						<view class="service-info">
+							<text class="service-name">在线客服</text>
+							<text class="service-desc">{{ customerSetting.wechatAccount || '企业微信专属客服在线解答' }}</text>
+						</view>
+						<view class="call-btn-mini green-btn">
+							<text>去咨询</text>
+						</view>
+					</view>
+
+					<view class="service-row" @click="callServicePhone">
+						<view class="service-row-icon-box orange">
+							<uni-icons type="phone-filled" size="24" color="#fff"></uni-icons>
+						</view>
+						<view class="service-info">
+							<text class="service-name">客服电话</text>
+							<text class="service-desc">{{ customerSetting.phoneNumber || '暂无电话' }}</text>
+						</view>
+						<view class="call-btn-mini orange-btn">
+							<text>拨打</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
 		<custom-tabbar></custom-tabbar>
 	</view>
 </template>
 
 <script setup>
-import { ref } from 'vue'
+// @Author: Antigravity
+import { ref, reactive } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
 import { getInfo } from '@/api/system/user'
+import { getCustomerServiceSetting } from '@/api/system/customerServiceSetting'
 import customTabbar from '@/components/custom-tabbar/index.vue'
 import orderStatusData from '@/json/orderStatus.json'
 
 const userInfo = ref(null)
+const showServicePopup = ref(false)
+const customerSetting = reactive({
+	wechatAccount: '',
+	phoneNumber: '',
+	qrCode: '',
+	qrCodeUrl: '',
+	enterpriseWechatLink: ''
+})
 
 const fetchUserInfo = async () => {
 	const token = uni.getStorageSync('token')
@@ -77,8 +132,21 @@ const fetchUserInfo = async () => {
 	}
 }
 
+// 获取客服配置 (ID=2)
+const fetchCustomerSetting = async () => {
+	try {
+		const res = await getCustomerServiceSetting(2)
+		if (res) {
+			Object.assign(customerSetting, res)
+		}
+	} catch (error) {
+		console.error('获取客服配置失败', error)
+	}
+}
+
 onShow(() => {
 	fetchUserInfo()
+	fetchCustomerSetting()
 })
 
 const goToLogin = () => {
@@ -88,7 +156,6 @@ const goToLogin = () => {
 }
 
 const goToOrder = (statusValue) => {
-	// statusValue 可以是 'all' 或枚举值
 	if (statusValue === 'all') {
 		uni.reLaunch({ url: '/pages/order/list/index' })
 	} else {
@@ -97,11 +164,54 @@ const goToOrder = (statusValue) => {
 }
 
 const goToMenu = (item) => {
+	if (item.title === '客服中心') {
+		showServicePopup.value = true
+		return
+	}
 	if (item.path) {
 		uni.navigateTo({ url: item.path })
 	}
 }
 
+const closeServicePopup = () => {
+	showServicePopup.value = false
+}
+
+const previewQRCode = () => {
+	if (!customerSetting.qrCodeUrl) return
+	uni.previewImage({
+		urls: [customerSetting.qrCodeUrl]
+	})
+}
+
+const openOnlineService = () => {
+	if (customerSetting.enterpriseWechatLink) {
+		// #ifdef H5
+		window.location.href = customerSetting.enterpriseWechatLink
+		// #endif
+		// #ifndef H5
+		uni.setClipboardData({
+			data: customerSetting.wechatAccount || customerSetting.enterpriseWechatLink,
+			success: () => {
+				uni.showToast({ title: '客服账号已复制,请在微信中添加', icon: 'none' })
+			}
+		})
+		// #endif
+	} else {
+		uni.showToast({ title: '在线客服暂未配置', icon: 'none' })
+	}
+}
+
+const callServicePhone = () => {
+	if (!customerSetting.phoneNumber) {
+		uni.showToast({ title: '暂无客服电话', icon: 'none' })
+		return
+	}
+	uni.makePhoneCall({
+		phoneNumber: customerSetting.phoneNumber
+	})
+}
+
 // 订单图标 - 从 orderStatus.json 生成
 const iconMap = {
 	0: '/static/icon/order-wait.svg',
@@ -131,12 +241,144 @@ const menuItems = [
 </script>
 
 <style lang="scss" scoped>
+/* 保持原有样式并添加弹窗样式 @Author: Antigravity */
 .my-page {
 	min-height: 100vh;
 	background-color: #fcfaf5;
 	padding-bottom: 140rpx;
 }
 
+/* ... 之前的样式保留,下文补充新样式 ... */
+
+.service-popup-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.6);
+	z-index: 999;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	backdrop-filter: blur(4px);
+}
+
+.service-popup {
+	width: 620rpx;
+	background-color: #fff;
+	border-radius: 48rpx;
+	padding: 0;
+	overflow: hidden;
+	box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
+	animation: slide-in 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
+}
+
+@keyframes slide-in {
+	from { transform: scale(0.85); opacity: 0; }
+	to { transform: scale(1); opacity: 1; }
+}
+
+.service-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 40rpx;
+	border-bottom: 2rpx solid #faf8f5;
+}
+
+.service-title {
+	font-size: 32rpx;
+	font-weight: 800;
+	color: #4a3e2e;
+}
+
+.qr-section {
+	padding: 48rpx 0;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	background: linear-gradient(180deg, #fff 0%, #fffbf2 100%);
+}
+
+.qr-title {
+	font-size: 28rpx;
+	font-weight: 700;
+	color: #6d5b45;
+	margin-bottom: 30rpx;
+}
+
+.qr-img {
+	width: 320rpx;
+	height: 320rpx;
+	border-radius: 32rpx;
+	padding: 16rpx;
+	background: #fff;
+	box-shadow: 0 8rpx 32rpx rgba(247, 202, 62, 0.15);
+	margin-bottom: 20rpx;
+}
+
+.qr-desc {
+	font-size: 24rpx;
+	color: #a39686;
+	font-weight: 600;
+}
+
+.service-list {
+	padding: 20rpx 40rpx 40rpx;
+}
+
+.service-row {
+	display: flex;
+	align-items: center;
+	padding: 32rpx 0;
+	border-bottom: 2rpx solid #fcfaf5;
+	&:last-child { border-bottom: none; }
+}
+
+.service-row-icon-box {
+	width: 88rpx;
+	height: 88rpx;
+	border-radius: 28rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-right: 24rpx;
+	&.green { background: #5ec686; box-shadow: 0 8rpx 16rpx rgba(94, 198, 134, 0.2); }
+	&.orange { background: #f7ca3e; box-shadow: 0 8rpx 16rpx rgba(247, 202, 62, 0.2); }
+}
+
+.service-info {
+	flex: 1;
+}
+
+.service-name {
+	display: block;
+	font-size: 28rpx;
+	font-weight: 800;
+	color: #4a3e2e;
+	margin-bottom: 4rpx;
+}
+
+.service-desc {
+	display: block;
+	font-size: 22rpx;
+	color: #a39686;
+	font-weight: 600;
+}
+
+.call-btn-mini {
+	padding: 12rpx 28rpx;
+	border-radius: 20rpx;
+	font-size: 24rpx;
+	font-weight: 700;
+	transition: transform 0.2s;
+	&:active { transform: scale(0.95); }
+	&.green-btn { background: #eef9f2; color: #5ec686; }
+	&.orange-btn { background: #fef6df; color: #f7ca3e; }
+}
+
+
 .header-curve {
 	position: relative;
 	background: #f7ca3e;

+ 206 - 25
pages/my/pet/add/index.vue

@@ -1,59 +1,240 @@
 <template>
 	<view class="pet-add-page">
+		<nav-bar title="新增宠物档案"></nav-bar>
+		
+		<!-- 基础信息 -->
+		<view class="section-title">基础信息</view>
 		<view class="form-card">
 			<view class="form-item">
-				<text class="form-label">宠物名称</text>
+				<text class="form-label require">宠物名称</text>
 				<input class="form-input" v-model="form.name" placeholder="请输入宠物名称" />
 			</view>
 			<view class="form-item">
-				<text class="form-label">品种</text>
-				<input class="form-input" v-model="form.breed" placeholder="请输入品种" />
+				<text class="form-label require">所属主人</text>
+				<picker :range="customerOptions" range-key="name" @change="onCustomerChange">
+					<view class="picker-value" :class="{'placeholder': !form.userId}">{{ getCustomerLabel }}</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">品种</text>
+				<view class="form-combox-wrapper">
+					<uni-combox :candidates="breedCandidates" v-model="form.breed" placeholder="可选择也可自填品种" emptyTips="未找到对应品种,可直接输入"></uni-combox>
+				</view>
 			</view>
 			<view class="form-item">
 				<text class="form-label">性别</text>
-				<picker :range="genderOptions" @change="onGenderChange">
-					<view class="picker-value">{{ form.gender || '请选择' }}</view>
+				<picker :range="genderOptions" range-key="label" @change="onGenderChange">
+					<view class="picker-value">{{ getGenderLabel }}</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">体型</text>
+				<picker :range="sizeOptions" range-key="label" @change="onSizeChange">
+					<view class="picker-value" :class="{'placeholder': !form.size}">{{ getSizeLabel }}</view>
 				</picker>
 			</view>
 			<view class="form-item">
-				<text class="form-label">年龄</text>
-				<input class="form-input" v-model="form.age" placeholder="请输入年龄" />
+				<text class="form-label require">年龄(岁)</text>
+				<input class="form-input" v-model="form.age" type="number" placeholder="请输入年龄" />
 			</view>
 			<view class="form-item">
-				<text class="form-label">体重(kg)</text>
+				<text class="form-label require">体重(kg)</text>
 				<input class="form-input" v-model="form.weight" type="digit" placeholder="请输入体重" />
 			</view>
+		</view>
+
+		<!-- 性格特征 -->
+		<view class="section-title">性格特征</view>
+		<view class="form-card">
+			<view class="form-item">
+				<text class="form-label">性格关键词</text>
+				<input class="form-input" v-model="form.personality" placeholder="例如活泼、粘人" />
+			</view>
+			<view class="form-item vertical">
+				<text class="form-label">萌宠性格描述</text>
+				<textarea class="form-textarea" v-model="form.cutePersonality" placeholder="详细描述宠物的性格"></textarea>
+			</view>
+		</view>
+
+		<!-- 家庭信息 -->
+		<view class="section-title">家庭信息</view>
+		<view class="form-card">
+			<view class="form-item">
+				<text class="form-label require">房屋类型</text>
+				<picker :range="houseTypeOptions" range-key="label" @change="onHouseTypeChange">
+					<view class="picker-value" :class="{'placeholder': !form.houseType}">{{ getHouseTypeLabel }}</view>
+				</picker>
+			</view>
 			<view class="form-item">
-				<text class="form-label">备注</text>
-				<textarea class="form-textarea" v-model="form.note" placeholder="请输入备注信息"></textarea>
+				<text class="form-label require">入门方式</text>
+				<picker :range="entryMethodOptions" range-key="label" @change="onEntryMethodChange">
+					<view class="picker-value" :class="{'placeholder': !form.entryMethod}">{{ getEntryMethodLabel }}</view>
+				</picker>
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'password'">
+				<text class="form-label require">门锁密码</text>
+				<input class="form-input" v-model="form.entryPassword" placeholder="请输入门锁密码" />
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'key'">
+				<text class="form-label require">钥匙存放处</text>
+				<input class="form-input" v-model="form.keyLocation" placeholder="请输入取钥匙位置" />
 			</view>
 		</view>
-		<button class="save-btn" @click="onSave">新增宠物</button>
+
+		<!-- 健康状况 -->
+		<view class="section-title">健康状况</view>
+		<view class="form-card">
+			<view class="form-item">
+				<text class="form-label require">健康状态</text>
+				<picker :range="healthStatusOptions" @change="onHealthChange">
+					<view class="picker-value">{{ form.healthStatus || '请选择' }}</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">疫苗接种</text>
+				<picker :range="vaccineOptions" @change="onVaccineChange">
+					<view class="picker-value">{{ form.vaccineStatus || '请选择' }}</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">攻击倾向</text>
+				<picker :range="aggressionOptions" range-key="label" @change="onAggressionChange">
+					<view class="picker-value">{{ getAggressionLabel }}</view>
+				</picker>
+			</view>
+			<view class="form-item vertical">
+				<text class="form-label require">既往病史</text>
+				<textarea class="form-textarea" v-model="form.medicalHistory" placeholder="如有病史请务必记录"></textarea>
+			</view>
+			<view class="form-item vertical">
+				<text class="form-label require">过敏史</text>
+				<textarea class="form-textarea" v-model="form.allergies" placeholder="如有过敏源请务必记录"></textarea>
+			</view>
+			<view class="form-item vertical">
+				<text class="form-label">补充备注</text>
+				<textarea class="form-textarea" v-model="form.remark" placeholder="请输入其他备注信息"></textarea>
+			</view>
+		</view>
+
+		<!-- 底部固定操作栏 -->
+		<view class="footer-bar">
+			<button class="save-btn" @click="onSave">保存档案</button>
+		</view>
 	</view>
 </template>
 
 <script setup>
-import { reactive } from 'vue'
+import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { addPet } from '@/api/archieves/pet'
+import { listAllCustomer } from '@/api/archieves/customer'
+import { getDicts } from '@/api/system/dict/data'
+import navBar from '@/components/nav-bar/index.vue'
+import customerEnums from '@/json/customer.json'
+
+const genderOptions = [{ label: '未知', value: 0 }, { label: '公', value: 1 }, { label: '母', value: 2 }]
+const sizeOptions = [{ label: '小型(0-10kg)', value: 'small' }, { label: '中型(10-25kg)', value: 'medium' }, { label: '大型(25kg+)', value: 'large' }]
+const { houseTypeOptions, entryMethodOptions } = customerEnums
+const healthStatusOptions = ['健康', '亚健康', '疾病']
+const vaccineOptions = ['无', '已打1次', '已打2次', '已打3次']
+const aggressionOptions = [{ label: '否', value: 0 }, { label: '是', value: 1 }]
+
+const customerOptions = ref([])
+const breedCandidates = ref([])
+
+onLoad(async () => {
+	try {
+        const res = await listAllCustomer({ status: 0 })
+        customerOptions.value = Array.isArray(res) ? res : (res?.data || [])
+    } catch(e) {
+		console.error('获取主人列表失败', e)
+	}
+
+	try {
+		const dictRes = await getDicts('sys_pet_breed')
+		const list = Array.isArray(dictRes) ? dictRes : (dictRes?.data || [])
+		// 提取中文名给 combox,RuoYi 系统对应的字段可能是 dictLabel/dictValue
+		breedCandidates.value = list.map(item => item.dictLabel || item.dictValue || item.label || item.value)
+	} catch(e) {
+		console.error('获取宠物品种字典失败', e)
+	}
+})
 
-const genderOptions = ['公', '母']
-const form = reactive({ name: '', breed: '', gender: '', age: '', weight: '', note: '' })
+const form = reactive({
+	name: '', userId: '', breed: '', gender: 0, age: '', weight: '', size: '',
+	houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
+	personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
+	vaccineStatus: '无', medicalHistory: '', allergies: '', remark: ''
+})
 
-const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value] }
-const onSave = () => {
-	if (!form.name) { uni.showToast({ title: '请输入宠物名称', icon: 'none' }); return }
-	uni.showToast({ title: '新增成功', icon: 'success' })
-	setTimeout(() => uni.navigateBack(), 1000)
+const getCustomerLabel = computed(() => {
+    const c = customerOptions.value.find(i => i.id === form.userId)
+    return c ? `${c.name} - ${c.phone}` : '请选择主人'
+})
+
+const getGenderLabel = computed(() => genderOptions.find(i => i.value === form.gender)?.label || '请选择')
+const getSizeLabel = computed(() => sizeOptions.find(i => i.value === form.size)?.label || '请选择')
+const getHouseTypeLabel = computed(() => houseTypeOptions.find(i => i.value === form.houseType)?.label || '请选择')
+const getEntryMethodLabel = computed(() => entryMethodOptions.find(i => i.value === form.entryMethod)?.label || '请选择')
+const getAggressionLabel = computed(() => aggressionOptions.find(i => i.value === form.aggression)?.label || '请选择')
+
+const onCustomerChange = (e) => { form.userId = customerOptions.value[e.detail.value]?.id }
+const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value].value }
+const onSizeChange = (e) => { form.size = sizeOptions[e.detail.value].value }
+const onHouseTypeChange = (e) => { form.houseType = houseTypeOptions[e.detail.value].value }
+const onEntryMethodChange = (e) => { form.entryMethod = entryMethodOptions[e.detail.value].value }
+const onHealthChange = (e) => { form.healthStatus = healthStatusOptions[e.detail.value] }
+const onVaccineChange = (e) => { form.vaccineStatus = vaccineOptions[e.detail.value] }
+const onAggressionChange = (e) => { form.aggression = aggressionOptions[e.detail.value].value }
+
+const onSave = async () => {
+  if (!form.name) return uni.showToast({ title: '请输入宠物名称', icon: 'none' })
+  if (!form.userId) return uni.showToast({ title: '请选择所属主人', icon: 'none' })
+  if (!form.breed) return uni.showToast({ title: '请输入品种', icon: 'none' })
+  if (!form.size) return uni.showToast({ title: '请选择体型', icon: 'none' })
+  if (!form.weight) return uni.showToast({ title: '请输入体重', icon: 'none' })
+  if (!form.age) return uni.showToast({ title: '请输入年龄', icon: 'none' })
+  if (!form.houseType) return uni.showToast({ title: '请选择家庭房屋类型', icon: 'none' })
+  if (!form.entryMethod) return uni.showToast({ title: '请选择入门方式', icon: 'none' })
+  if (form.entryMethod === 'password' && !form.entryPassword) return uni.showToast({ title: '请输入门锁密码', icon: 'none' })
+  if (form.entryMethod === 'key' && !form.keyLocation) return uni.showToast({ title: '请输入钥匙存放位置', icon: 'none' })
+  if (!form.healthStatus) return uni.showToast({ title: '请选择健康状态', icon: 'none' })
+  if (!form.vaccineStatus) return uni.showToast({ title: '请选择疫苗情况', icon: 'none' })
+  if (!form.medicalHistory) return uni.showToast({ title: '请输入既往病史', icon: 'none' })
+  if (!form.allergies) return uni.showToast({ title: '请输入过敏史', icon: 'none' })
+
+  try {
+      uni.showLoading({ title: '保存中' })
+      await addPet(form)
+      uni.hideLoading()
+      uni.showToast({ title: '新增成功', icon: 'success' })
+      setTimeout(() => uni.navigateBack(), 1000)
+  } catch (error) {
+      uni.hideLoading()
+      uni.showToast({ title: '保存失败', icon: 'none' })
+  }
 }
 </script>
 
 <style lang="scss" scoped>
-.pet-add-page { min-height: 100vh; background: #f7f8fa; padding: 24rpx; padding-bottom: 160rpx; }
-.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; }
+.pet-add-page { min-height: 100vh; background: #f7f8fa; padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); }
+.section-title { font-size: 28rpx; font-weight: bold; color: #666; padding: 24rpx 32rpx 12rpx; }
+.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; margin: 0 24rpx; }
 .form-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .form-item:last-child { border-bottom: none; }
-.form-label { width: 180rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-item.vertical { flex-direction: column; align-items: flex-start; border-bottom: none; }
+.form-item.vertical .form-textarea { width: 100%; height: 160rpx; margin-top: 16rpx; background: #f9f9f9; padding: 16rpx; box-sizing: border-box; border-radius: 12rpx; }
+.form-label { width: 220rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-label.require::before { content: '*'; color: #f56c6c; margin-right: 4rpx; }
 .form-input { flex: 1; font-size: 28rpx; color: #333; text-align: right; }
-.picker-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; }
-.form-textarea { flex: 1; font-size: 28rpx; color: #333; height: 160rpx; }
-.save-btn { margin-top: 48rpx; width: 100%; height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
+.form-combox-wrapper { flex: 1; min-width: 0; }
+.picker-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; min-height: 40rpx; }
+.picker-value.placeholder { color: #ccc; }
+.footer-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); z-index: 100; }
+.save-btn { width: 100%; height: 88rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 44rpx; font-size: 32rpx; font-weight: bold; line-height: 88rpx; }
+
+:deep(.uni-combox) { border: none !important; }
+:deep(.uni-combox__input-box) { display: flex; justify-content: flex-end; padding-right: 0; }
+:deep(.uni-combox__input) { text-align: right; font-size: 28rpx; padding-right: 0;}
+:deep(.uni-combox__input-plholder) { color: #ccc; text-align: right; font-size: 28rpx; padding-right: 0;}
 </style>

+ 356 - 27
pages/my/pet/detail/index.vue

@@ -1,43 +1,372 @@
 <template>
 	<view class="pet-detail-page">
+		<nav-bar title="宠物档案详情" bgColor="transparent" fontColor="#fff"></nav-bar>
+		
 		<view class="pet-hero">
-			<image src="https://images.unsplash.com/photo-1552053831-71594a27632d?q=80&w=600&auto=format&fit=crop"
-				class="hero-img" mode="aspectFill"></image>
+			<image :src="petInfo.avatar || defaultAvatar" class="hero-img" mode="aspectFill"></image>
 			<view class="hero-overlay"></view>
-			<view class="hero-info">
-				<text class="pet-name">旺财</text>
-				<text class="pet-breed">金毛寻回犬 · 公 · 3岁</text>
+			<view class="hero-content">
+				<view class="hero-main">
+					<text class="pet-name">{{ petInfo.name || '加载中...' }}</text>
+					<view class="tag-list">
+						<text class="gender-tag" :class="genderClass">{{ genderLabel }}</text>
+						<text class="size-tag">{{ sizeLabel }}</text>
+					</view>
+				</view>
+				<text class="pet-summary">{{ petInfo.breed }} · {{ petInfo.age || 0 }}岁 · {{ petInfo.weight || 0 }}kg</text>
 			</view>
 		</view>
-		<view class="info-card">
-			<view class="info-row"><text class="label">体重</text><text class="value">30 kg</text></view>
-			<view class="info-row"><text class="label">健康状况</text><text class="value">健康</text></view>
-			<view class="info-row"><text class="label">绝育状态</text><text class="value">已绝育</text></view>
-			<view class="info-row"><text class="label">疫苗状态</text><text class="value green">已接种 (3/3)</text></view>
-			<view class="info-row"><text class="label">性格特点</text><text class="value">活泼好动,喜欢球类玩具</text></view>
-			<view class="info-row"><text class="label">主人</text><text class="value">张先生 13800138000</text></view>
+
+		<view class="detail-container">
+			<!-- 基础信息 -->
+			<view class="section-card">
+				<view class="section-header">
+					<view class="title-line"></view>
+					<text class="section-title">基础信息</text>
+				</view>
+				<view class="info-grid">
+					<view class="info-item">
+						<text class="label">宠物编号</text>
+						<text class="value">{{ petInfo.id || '-' }}</text>
+					</view>
+					<view class="info-item">
+						<text class="label">所属主人</text>
+						<text class="value">{{ petInfo.ownerName || petInfo.userName || petInfo.customerName || '-' }}</text>
+					</view>
+					<view class="info-item">
+						<text class="label">主人电话</text>
+						<text class="value">{{ petInfo.ownerPhone || petInfo.phonenumber || '-' }}</text>
+					</view>
+					<view class="info-item col-2">
+						<text class="label">性格关键词</text>
+						<text class="value">{{ petInfo.personality || '无' }}</text>
+					</view>
+					<view class="info-item col-2">
+						<text class="label">萌宠性格</text>
+						<text class="value block">{{ petInfo.cutePersonality || '暂无详细描述' }}</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 家庭信息 -->
+			<view class="section-card">
+				<view class="section-header">
+					<view class="title-line"></view>
+					<text class="section-title">家庭信息</text>
+				</view>
+				<view class="info-grid">
+					<view class="info-item">
+						<text class="label">房屋类型</text>
+						<text class="value">{{ houseTypeLabel }}</text>
+					</view>
+					<view class="info-item">
+						<text class="label">入门方式</text>
+						<text class="value">{{ entryMethodLabel }}</text>
+					</view>
+					<view class="info-item col-2" v-if="petInfo.entryMethod === 'password'">
+						<text class="label">门锁密码</text>
+						<text class="value highlight">{{ petInfo.entryPassword || '-' }}</text>
+					</view>
+					<view class="info-item col-2" v-if="petInfo.entryMethod === 'key'">
+						<text class="label">钥匙存放处</text>
+						<text class="value">{{ petInfo.keyLocation || '-' }}</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 健康状况 -->
+			<view class="section-card">
+				<view class="section-header">
+					<view class="title-line"></view>
+					<text class="section-title">健康状况</text>
+				</view>
+				<view class="info-grid">
+					<view class="info-item">
+						<text class="label">健康状态</text>
+						<text class="value" :class="healthClass">{{ petInfo.healthStatus || '未知' }}</text>
+					</view>
+					<view class="info-item">
+						<text class="label">疫苗情况</text>
+						<text class="value">{{ petInfo.vaccineStatus || '未记录' }}</text>
+					</view>
+					<view class="info-item">
+						<text class="label">攻击倾向</text>
+						<text class="value" :class="petInfo.aggression ? 'red' : ''">{{ petInfo.aggression ? '是' : '否' }}</text>
+					</view>
+					<view class="info-item col-2">
+						<text class="label">既往病史</text>
+						<text class="value block">{{ petInfo.medicalHistory || '无' }}</text>
+					</view>
+					<view class="info-item col-2">
+						<text class="label">过敏史</text>
+						<text class="value block">{{ petInfo.allergies || '无' }}</text>
+					</view>
+				</view>
+			</view>
+
+			<!-- 备注信息 -->
+			<view class="section-card" v-if="petInfo.remark">
+				<view class="section-header">
+					<view class="title-line"></view>
+					<text class="section-title">补充备注</text>
+				</view>
+				<view class="remark-content">
+					{{ petInfo.remark }}
+				</view>
+			</view>
+		</view>
+
+		<view class="footer-bar">
+			<button class="edit-btn" @click="goToEdit">编辑档案</button>
 		</view>
-		<button class="edit-btn" @click="goToEdit">编辑档案</button>
 	</view>
 </template>
 
 <script setup>
-const goToEdit = () => uni.navigateTo({ url: '/pages/my/pet/edit/index' })
+// @Author: Antigravity
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getPet } from '@/api/archieves/pet'
+import navBar from '@/components/nav-bar/index.vue'
+import customerEnums from '@/json/customer.json'
+
+const defaultAvatar = 'https://images.unsplash.com/photo-1552053831-71594a27632d?q=80&w=600&auto=format&fit=crop'
+const petId = ref(null)
+const petInfo = ref({})
+
+const genderOptions = { 0: '未知', 1: '公', 2: '母' }
+const sizeOptions = { 'small': '小型', 'medium': '中型', 'large': '大型' }
+const { houseTypeOptions, entryMethodOptions } = customerEnums
+
+onLoad((options) => {
+	if (options.id) {
+		petId.value = options.id
+		fetchDetail()
+	}
+})
+
+const fetchDetail = async () => {
+	try {
+		uni.showLoading({ title: '加载中...' })
+		const res = await getPet(petId.value)
+		petInfo.value = res || {}
+		uni.hideLoading()
+	} catch (e) {
+		uni.hideLoading()
+		console.error('获取详情失败', e)
+	}
+}
+
+const genderLabel = computed(() => {
+	const g = petInfo.value.gender
+	return g === 1 ? '公' : (g === 2 ? '母' : '未知')
+})
+
+const genderClass = computed(() => {
+	const g = petInfo.value.gender
+	return g === 1 ? 'male' : (g === 2 ? 'female' : '')
+})
+
+const sizeLabel = computed(() => sizeOptions[petInfo.value.size] || '未知体型')
+
+const houseTypeLabel = computed(() => {
+	const opt = houseTypeOptions.find(o => o.value === petInfo.value.houseType)
+	return opt ? opt.label : '未记录'
+})
+
+const entryMethodLabel = computed(() => {
+	const opt = entryMethodOptions.find(o => o.value === petInfo.value.entryMethod)
+	return opt ? opt.label : '未记录'
+})
+
+const healthClass = computed(() => {
+	const s = petInfo.value.healthStatus
+	if (s === '健康') return 'green'
+	if (s === '疾病') return 'red'
+	return 'orange'
+})
+
+const goToEdit = () => {
+	uni.navigateTo({
+		url: `/pages/my/pet/edit/index?id=${petId.value}`
+	})
+}
 </script>
 
 <style lang="scss" scoped>
-.pet-detail-page { min-height: 100vh; background: #f7f8fa; padding-bottom: 160rpx; }
-.pet-hero { position: relative; height: 480rpx; overflow: hidden; }
-.hero-img { width: 100%; height: 100%; }
-.hero-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, transparent 40%, rgba(0, 0, 0, 0.7) 100%); }
-.hero-info { position: absolute; bottom: 40rpx; left: 40rpx; z-index: 2; }
-.pet-name { display: block; font-size: 48rpx; font-weight: 900; color: #fff; }
-.pet-breed { display: block; font-size: 28rpx; color: rgba(255, 255, 255, 0.9); margin-top: 8rpx; }
-.info-card { background: #fff; border-radius: 32rpx 32rpx 0 0; margin-top: -40rpx; position: relative; z-index: 3; padding: 40rpx 32rpx; }
-.info-row { display: flex; justify-content: space-between; padding: 24rpx 0; border-bottom: 1rpx solid #f5f5f5; }
-.info-row:last-child { border-bottom: none; }
-.label { font-size: 28rpx; color: #999; }
-.value { font-size: 28rpx; color: #333; font-weight: 500; }
+.pet-detail-page {
+	min-height: 100vh;
+	background: #f4f6f9;
+	padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
+}
+
+.pet-hero {
+	position: relative;
+	height: 520rpx;
+	width: 100%;
+}
+
+.hero-img {
+	width: 100%;
+	height: 100%;
+}
+
+.hero-overlay {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0) 40%, rgba(0,0,0,0.8) 100%);
+}
+
+.hero-content {
+	position: absolute;
+	bottom: 60rpx;
+	left: 40rpx;
+	right: 40rpx;
+	z-index: 5;
+}
+
+.hero-main {
+	display: flex;
+	align-items: center;
+	margin-bottom: 20rpx;
+}
+
+.pet-name {
+	font-size: 56rpx;
+	font-weight: 800;
+	color: #fff;
+	margin-right: 24rpx;
+}
+
+.tag-list {
+	display: flex;
+	gap: 12rpx;
+}
+
+.gender-tag, .size-tag {
+	padding: 4rpx 20rpx;
+	border-radius: 30rpx;
+	font-size: 22rpx;
+	background: rgba(255, 255, 255, 0.2);
+	color: #fff;
+	backdrop-filter: blur(4rpx);
+}
+
+.gender-tag.male { background: rgba(0, 122, 255, 0.6); }
+.gender-tag.female { background: rgba(255, 45, 85, 0.6); }
+
+.pet-summary {
+	font-size: 28rpx;
+	color: rgba(255, 255, 255, 0.9);
+}
+
+.detail-container {
+	margin-top: -40rpx;
+	position: relative;
+	z-index: 10;
+	padding: 0 24rpx;
+}
+
+.section-card {
+	background: #fff;
+	border-radius: 24rpx;
+	padding: 32rpx;
+	margin-bottom: 24rpx;
+	box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
+}
+
+.section-header {
+	display: flex;
+	align-items: center;
+	margin-bottom: 32rpx;
+}
+
+.title-line {
+	width: 8rpx;
+	height: 32rpx;
+	background: #ff9500;
+	border-radius: 4rpx;
+	margin-right: 16rpx;
+}
+
+.section-title {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.info-grid {
+	display: grid;
+	grid-template-columns: 1fr 1fr;
+	gap: 32rpx 24rpx;
+}
+
+.info-item {
+	display: flex;
+	flex-direction: column;
+	gap: 8rpx;
+}
+
+.info-item.col-2 {
+	grid-column: span 2;
+}
+
+.label {
+	font-size: 24rpx;
+	color: #999;
+}
+
+.value {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.value.block {
+	line-height: 1.6;
+	color: #666;
+	font-weight: normal;
+}
+
+.value.highlight {
+	color: #ff9500;
+	font-weight: bold;
+	font-size: 32rpx;
+}
+
 .value.green { color: #4caf50; }
-.edit-btn { margin: 40rpx 32rpx; width: calc(100% - 64rpx); height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
+.value.red { color: #f44336; }
+.value.orange { color: #ff9800; }
+
+.remark-content {
+	font-size: 28rpx;
+	color: #666;
+	line-height: 1.6;
+}
+
+.footer-bar {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	background: #fff;
+	padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
+	box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.05);
+	z-index: 100;
+}
+
+.edit-btn {
+	width: 100%;
+	height: 88rpx;
+	background: linear-gradient(90deg, #ffd53f, #ff9500);
+	color: #333;
+	border: none;
+	border-radius: 44rpx;
+	font-size: 32rpx;
+	font-weight: bold;
+	line-height: 88rpx;
+}
 </style>

+ 258 - 91
pages/my/pet/edit/index.vue

@@ -1,117 +1,284 @@
 <template>
 	<view class="pet-edit-page">
-		<view class="form-card">
-			<view class="form-item">
-				<text class="form-label">宠物名称</text>
-				<input class="form-input" v-model="form.name" placeholder="请输入宠物名称" />
-			</view>
-			<view class="form-item">
-				<text class="form-label">品种</text>
-				<input class="form-input" v-model="form.breed" placeholder="请输入品种" />
-			</view>
-			<view class="form-item">
-				<text class="form-label">性别</text>
-				<picker :range="genderOptions" @change="onGenderChange">
-					<view class="picker-value">{{ form.gender || '请选择' }}</view>
-				</picker>
+		<nav-bar title="编辑宠物档案"></nav-bar>
+		
+		<view v-if="loading" class="loading-state">
+			<uni-load-more status="loading"></uni-load-more>
+		</view>
+
+		<block v-else>
+			<!-- 基础信息 -->
+			<view class="section-title">基础信息</view>
+			<view class="form-card">
+				<view class="form-item">
+					<text class="form-label require">宠物名称</text>
+					<input class="form-input" v-model="form.name" placeholder="请输入宠物名称" />
+				</view>
+				<view class="form-item">
+					<text class="form-label require">宠物类型</text>
+					<picker :range="typeOptions" range-key="label" @change="onTypeChange">
+						<view class="picker-value">{{ getTypeLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">所属主人</text>
+					<picker :range="customerOptions" range-key="name" @change="onCustomerChange">
+						<view class="picker-value" :class="{'placeholder': !form.userId}">{{ getCustomerLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">品种</text>
+					<view class="form-combox-wrapper">
+						<uni-combox :candidates="breedCandidates" v-model="form.breed" placeholder="请选择或输入品种" emptyTips="未找到对应品种,可直接输入"></uni-combox>
+					</view>
+				</view>
+				<view class="form-item">
+					<text class="form-label">性别</text>
+					<picker :range="genderOptions" range-key="label" @change="onGenderChange">
+						<view class="picker-value">{{ getGenderLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">体型</text>
+					<picker :range="sizeOptions" range-key="label" @change="onSizeChange">
+						<view class="picker-value" :class="{'placeholder': !form.size}">{{ getSizeLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">年龄(岁)</text>
+					<input class="form-input" v-model="form.age" type="number" placeholder="请输入年龄" />
+				</view>
+				<view class="form-item">
+					<text class="form-label require">体重(kg)</text>
+					<input class="form-input" v-model="form.weight" type="digit" placeholder="请输入体重" />
+				</view>
 			</view>
-			<view class="form-item">
-				<text class="form-label">年龄</text>
-				<input class="form-input" v-model="form.age" placeholder="请输入年龄" />
+
+			<!-- 性格特征 -->
+			<view class="section-title">性格特征</view>
+			<view class="form-card">
+				<view class="form-item">
+					<text class="form-label">性格关键词</text>
+					<input class="form-input" v-model="form.personality" placeholder="例如活泼、粘人" />
+				</view>
+				<view class="form-item vertical">
+					<text class="form-label">萌宠性格描述</text>
+					<textarea class="form-textarea" v-model="form.cutePersonality" placeholder="详细描述宠物的性格"></textarea>
+				</view>
 			</view>
-			<view class="form-item">
-				<text class="form-label">体重(kg)</text>
-				<input class="form-input" v-model="form.weight" type="digit" placeholder="请输入体重" />
+
+			<!-- 家庭信息 -->
+			<view class="section-title">家庭信息</view>
+			<view class="form-card">
+				<view class="form-item">
+					<text class="form-label require">房屋类型</text>
+					<picker :range="houseTypeOptions" range-key="label" @change="onHouseTypeChange">
+						<view class="picker-value" :class="{'placeholder': !form.houseType}">{{ getHouseTypeLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">入门方式</text>
+					<picker :range="entryMethodOptions" range-key="label" @change="onEntryMethodChange">
+						<view class="picker-value" :class="{'placeholder': !form.entryMethod}">{{ getEntryMethodLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item" v-if="form.entryMethod === 'password'">
+					<text class="form-label require">门锁密码</text>
+					<input class="form-input" v-model="form.entryPassword" placeholder="请输入门锁密码" />
+				</view>
+				<view class="form-item" v-if="form.entryMethod === 'key'">
+					<text class="form-label require">钥匙存放处</text>
+					<input class="form-input" v-model="form.keyLocation" placeholder="请输入取钥匙位置" />
+				</view>
 			</view>
-			<view class="form-item">
-				<text class="form-label">健康状况</text>
-				<input class="form-input" v-model="form.health" placeholder="请输入健康状况" />
+
+			<!-- 健康状况 -->
+			<view class="section-title">健康状况</view>
+			<view class="form-card">
+				<view class="form-item">
+					<text class="form-label require">健康状态</text>
+					<picker :range="healthStatusOptions" @change="onHealthChange">
+						<view class="picker-value">{{ form.healthStatus || '请选择' }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">疫苗接种</text>
+					<picker :range="vaccineOptions" @change="onVaccineChange">
+						<view class="picker-value">{{ form.vaccineStatus || '请选择' }}</view>
+					</picker>
+				</view>
+				<view class="form-item">
+					<text class="form-label require">攻击倾向</text>
+					<picker :range="aggressionOptions" range-key="label" @change="onAggressionChange">
+						<view class="picker-value">{{ getAggressionLabel }}</view>
+					</picker>
+				</view>
+				<view class="form-item vertical">
+					<text class="form-label require">既往病史</text>
+					<textarea class="form-textarea" v-model="form.medicalHistory" placeholder="如有病史请务必记录"></textarea>
+				</view>
+				<view class="form-item vertical">
+					<text class="form-label require">过敏史</text>
+					<textarea class="form-textarea" v-model="form.allergies" placeholder="如有过敏源请务必记录"></textarea>
+				</view>
+				<view class="form-item vertical">
+					<text class="form-label">补充备注</text>
+					<textarea class="form-textarea" v-model="form.remark" placeholder="请输入其他备注信息"></textarea>
+				</view>
 			</view>
-			<view class="form-item">
-				<text class="form-label">备注</text>
-				<textarea class="form-textarea" v-model="form.note" placeholder="请输入备注信息"></textarea>
+
+			<!-- 底部固定操作栏 -->
+			<view class="footer-bar">
+				<button class="save-btn" @click="onSave">保存修改</button>
 			</view>
-		</view>
-		<button class="save-btn" @click="onSave">保存修改</button>
+		</block>
 	</view>
 </template>
 
 <script setup>
-import { reactive } from 'vue'
+// @Author: Antigravity
+import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getPet, updatePet } from '@/api/archieves/pet'
+import { listAllCustomer } from '@/api/archieves/customer'
+import { getDicts } from '@/api/system/dict/data'
+import navBar from '@/components/nav-bar/index.vue'
+import customerEnums from '@/json/customer.json'
 
-const genderOptions = ['公', '母']
-const form = reactive({ name: '旺财', breed: '金毛寻回犬', gender: '公', age: '3岁', weight: '30', health: '健康', note: '' })
+const loading = ref(true)
+const typeOptions = [{ label: '猫', value: 1 }, { label: '狗', value: 2 }, { label: '其他', value: 3 }]
+const genderOptions = [{ label: '未知', value: 0 }, { label: '公', value: 1 }, { label: '母', value: 2 }]
+const sizeOptions = [{ label: '小型(0-10kg)', value: 'small' }, { label: '中型(10-25kg)', value: 'medium' }, { label: '大型(25kg+)', value: 'large' }]
+const { houseTypeOptions, entryMethodOptions } = customerEnums
+const healthStatusOptions = ['健康', '亚健康', '疾病']
+const vaccineOptions = ['无', '已打1次', '已打2次', '已打3次']
+const aggressionOptions = [{ label: '否', value: 0 }, { label: '是', value: 1 }]
 
-const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value] }
-const onSave = () => {
-	uni.showToast({ title: '保存成功', icon: 'success' })
-	setTimeout(() => uni.navigateBack(), 1000)
-}
-</script>
+const customerOptions = ref([])
+const breedCandidates = ref([])
+const petId = ref(null)
 
-<style lang="scss" scoped>
-.pet-edit-page {
-	min-height: 100vh;
-	background: #f7f8fa;
-	padding: 24rpx;
-	padding-bottom: 160rpx;
-}
+const form = reactive({
+	id: '', name: '', userId: '', type: 1, breed: '', gender: 0, age: '', weight: '', size: '',
+	arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
+	personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
+	vaccineStatus: '无', medicalHistory: '', allergies: '', remark: ''
+})
 
-.form-card {
-	background: #fff;
-	border-radius: 24rpx;
-	padding: 8rpx 32rpx;
-}
+onLoad(async (options) => {
+	if (options.id) {
+		petId.value = options.id
+		// 1. 获取所有必要的基础数据(客人列表、品种字典)
+		await initData()
+		// 2. 获取详情并回显
+		await fetchDetail()
+	} else {
+		uni.showToast({ title: '参数错误', icon: 'none' })
+		setTimeout(() => uni.navigateBack(), 1500)
+	}
+})
 
-.form-item {
-	display: flex;
-	align-items: center;
-	padding: 28rpx 0;
-	border-bottom: 1rpx solid #f5f5f5;
+const initData = async () => {
+	try {
+		const [custRes, dictRes] = await Promise.all([
+			listAllCustomer({ status: 0 }),
+			getDicts('sys_pet_breed')
+		])
+		customerOptions.value = Array.isArray(custRes) ? custRes : (custRes?.data || [])
+		const dictList = Array.isArray(dictRes) ? dictRes : (dictRes?.data || [])
+		breedCandidates.value = dictList.map(item => item.dictLabel || item.dictValue || item.label || item.value)
+	} catch(e) {
+		console.error('初始化数据失败', e)
+	}
 }
 
-.form-item:last-child {
-	border-bottom: none;
+const fetchDetail = async () => {
+	try {
+		loading.value = true
+		const res = await getPet(petId.value)
+		if (res) {
+			// 将接口返回的数据合并到响应式表单对象中
+			Object.assign(form, res)
+		}
+	} catch (e) {
+		console.error('获取宠物详情失败', e)
+	} finally {
+		loading.value = false
+	}
 }
 
-.form-label {
-	width: 180rpx;
-	font-size: 28rpx;
-	color: #333;
-	flex-shrink: 0;
-}
+// 标签转换逻辑
+const getTypeLabel = computed(() => typeOptions.find(o => o.value === form.type)?.label || '请选择')
+const onTypeChange = (e) => { form.type = typeOptions[e.detail.value].value }
 
-.form-input {
-	flex: 1;
-	font-size: 28rpx;
-	color: #333;
-	text-align: right;
-}
+const getCustomerLabel = computed(() => {
+	const opt = customerOptions.value.find(o => String(o.id) === String(form.userId))
+	return opt ? opt.name : '请选择主人'
+})
+const onCustomerChange = (e) => { form.userId = customerOptions.value[e.detail.value].id }
 
-.picker-value {
-	flex: 1;
-	font-size: 28rpx;
-	color: #333;
-	text-align: right;
-}
+const getGenderLabel = computed(() => genderOptions.find(o => o.value === form.gender)?.label || '未知')
+const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value].value }
 
-.form-textarea {
-	flex: 1;
-	font-size: 28rpx;
-	color: #333;
-	height: 160rpx;
-}
+const getSizeLabel = computed(() => sizeOptions.find(o => o.value === form.size)?.label || '请选择')
+const onSizeChange = (e) => { form.size = sizeOptions[e.detail.value].value }
+
+const getHouseTypeLabel = computed(() => houseTypeOptions.find(o => o.value === form.houseType)?.label || '请选择')
+const onHouseTypeChange = (e) => { form.houseType = houseTypeOptions[e.detail.value].value }
+
+const getEntryMethodLabel = computed(() => entryMethodOptions.find(o => o.value === form.entryMethod)?.label || '请选择')
+const onEntryMethodChange = (e) => { form.entryMethod = entryMethodOptions[e.detail.value].value }
+
+const onHealthChange = (e) => { form.healthStatus = healthStatusOptions[e.detail.value] }
+const onVaccineChange = (e) => { form.vaccineStatus = vaccineOptions[e.detail.value] }
 
-.save-btn {
-	margin-top: 48rpx;
-	width: 100%;
-	height: 96rpx;
-	background: linear-gradient(90deg, #ffd53f, #ff9500);
-	color: #333;
-	border: none;
-	border-radius: 48rpx;
-	font-size: 32rpx;
-	font-weight: bold;
-	line-height: 96rpx;
+const getAggressionLabel = computed(() => aggressionOptions.find(o => o.value === form.aggression)?.label || '否')
+const onAggressionChange = (e) => { form.aggression = aggressionOptions[e.detail.value].value }
+
+const onSave = async () => {
+	// 校验逻辑
+	if (!form.name) return uni.showToast({ title: '请输入宠物名称', icon: 'none' })
+	if (!form.userId) return uni.showToast({ title: '请选择所属主人', icon: 'none' })
+	if (!form.breed) return uni.showToast({ title: '请输入或选择品种', icon: 'none' })
+	if (!form.size) return uni.showToast({ title: '请选择体型', icon: 'none' })
+	if (!form.age) return uni.showToast({ title: '请输入年龄', icon: 'none' })
+	if (!form.weight) return uni.showToast({ title: '请输入体重', icon: 'none' })
+	if (!form.houseType) return uni.showToast({ title: '请选择房屋类型', icon: 'none' })
+	if (!form.entryMethod) return uni.showToast({ title: '请选择入门方式', icon: 'none' })
+	
+	try {
+		uni.showLoading({ title: '保存中' })
+		await updatePet(form)
+		uni.hideLoading()
+		uni.showToast({ title: '保存成功', icon: 'success' })
+		setTimeout(() => uni.navigateBack(), 1000)
+	} catch (error) {
+		uni.hideLoading()
+	}
 }
+</script>
+
+<style lang="scss" scoped>
+.pet-edit-page { min-height: 100vh; background: #f7f8fa; padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); }
+.loading-state { padding-top: 100rpx; }
+.section-title { font-size: 28rpx; font-weight: bold; color: #666; padding: 24rpx 32rpx 12rpx; }
+.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; margin: 0 24rpx; }
+.form-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 1rpx solid #f5f5f5; }
+.form-item:last-child { border-bottom: none; }
+.form-item.vertical { flex-direction: column; align-items: flex-start; border-bottom: none; }
+.form-item.vertical .form-textarea { width: 100%; height: 160rpx; margin-top: 16rpx; background: #f9f9f9; padding: 16rpx; box-sizing: border-box; border-radius: 12rpx; }
+.form-label { width: 220rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-label.require::before { content: '*'; color: #f56c6c; margin-right: 4rpx; }
+.form-input { flex: 1; font-size: 28rpx; color: #333; text-align: right; }
+.form-combox-wrapper { flex: 1; min-width: 0; }
+.picker-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; min-height: 40rpx; }
+.picker-value.placeholder { color: #ccc; }
+
+.footer-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); z-index: 100; }
+.save-btn { width: 100%; height: 88rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 44rpx; font-size: 32rpx; font-weight: bold; line-height: 88rpx; }
+
+:deep(.uni-combox) { border: none !important; }
+:deep(.uni-combox__input-box) { display: flex; justify-content: flex-end; padding-right: 0; }
+:deep(.uni-combox__input) { text-align: right; font-size: 28rpx; padding-right: 0;}
+:deep(.uni-combox__input-plholder) { color: #ccc; text-align: right; font-size: 28rpx; padding-right: 0;}
 </style>

+ 78 - 19
pages/my/pet/list/index.vue

@@ -5,7 +5,7 @@
 		<view class="action-bar">
 			<view class="search-box">
 				<uni-icons type="search" size="14" color="#999"></uni-icons>
-				<input type="text" placeholder="搜索宠物名/主人" class="search-input" />
+				<input type="text" v-model="searchKeyword" placeholder="搜索宠物名/主人" class="search-input" confirm-type="search" @confirm="onSearch" />
 			</view>
 			<button size="mini" class="add-btn" @click="goToAdd">+ 新增档案</button>
 		</view>
@@ -13,16 +13,16 @@
 		<!-- 宠物档案卡片列表 -->
 		<view class="list-container">
 			<view class="pet-card" v-for="pet in pets" :key="pet.id" @click="handleCardClick(pet)">
-				<image :src="pet.avatar" class="pet-photo" mode="aspectFill"></image>
+				<image :src="pet.avatarUrl || '/static/default-avatar.png'" class="pet-photo" mode="aspectFill"></image>
 				<view class="card-info">
 					<view class="info-top">
 						<text class="pet-name">{{ pet.name }}</text>
-						<text class="owner-name">{{ pet.ownerName }}</text>
+						<text class="owner-name">{{ pet.ownerName || '-' }}</text>
 					</view>
-					<text class="pet-meta">{{ pet.breed }} · {{ pet.age }}</text>
+					<text class="pet-meta">{{ pet.breed || '-' }} · {{ pet.age || 0 }}</text>
 					<view class="health-overview">
-						<text class="health-badge">{{ pet.health }}</text>
-						<text class="vaccine-info">疫苗: {{ pet.vaccine }}</text>
+						<text class="health-badge">{{ pet.healthStatus || '健康' }}</text>
+						<text class="vaccine-info">疫苗: {{ pet.vaccineStatus || '无' }}</text>
 					</view>
 					<view class="card-footer">
 						<view class="action-btn-group">
@@ -33,18 +33,74 @@
 					</view>
 				</view>
 			</view>
+
+			<view v-if="pets.length === 0" style="text-align: center; color: #999; padding: 50rpx 0; font-size: 28rpx;">
+				暂无宠物数据
+			</view>
 		</view>
 	</view>
 </template>
 
 <script setup>
 import { ref } from 'vue'
+import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
-import petMockData from '@/mock/pet.json'
+import { listPet, delPet } from '@/api/archieves/pet'
+
+const pets = ref([])
+const searchKeyword = ref('')
+const pageNum = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+
+const loadPets = async (isLoadMore = false) => {
+	try {
+		uni.showNavigationBarLoading()
+		const res = await listPet({
+			pageNum: pageNum.value,
+			pageSize: pageSize.value,
+			keyword: searchKeyword.value
+		})
+		const rows = res?.rows || []
+		if (isLoadMore) {
+			pets.value = [...pets.value, ...rows]
+		} else {
+			pets.value = rows
+		}
+		hasMore.value = pets.value.length < (res?.total || 0)
+	} catch (error) {
+		console.error('获取宠物列表失败:', error)
+	} finally {
+		uni.hideNavigationBarLoading()
+		uni.stopPullDownRefresh()
+	}
+}
+
+onShow(() => {
+	pageNum.value = 1
+	loadPets()
+})
+
+onPullDownRefresh(() => {
+	pageNum.value = 1
+	loadPets()
+})
+
+onReachBottom(() => {
+	if (hasMore.value) {
+		pageNum.value++
+		loadPets(true)
+	}
+})
+
+const onSearch = () => {
+	pageNum.value = 1
+	loadPets()
+}
 
 const goToAdd = () => uni.navigateTo({ url: '/pages/my/pet/add/index' })
-const goToDetail = (pet) => uni.navigateTo({ url: '/pages/my/pet/detail/index' })
-const goToEdit = (pet) => uni.navigateTo({ url: '/pages/my/pet/edit/index' })
+const goToDetail = (pet) => uni.navigateTo({ url: `/pages/my/pet/detail/index?id=${pet.id}` })
+const goToEdit = (pet) => uni.navigateTo({ url: `/pages/my/pet/edit/index?id=${pet.id}` })
 
 const handleCardClick = (pet) => {
 	goToEdit(pet)
@@ -52,21 +108,24 @@ const handleCardClick = (pet) => {
 
 const onDelete = (pet) => {
 	uni.showModal({
-		title: '确认删除',
-		content: `确定要删除宠物档案 [${pet.name}] 吗?`,
-		success: (res) => {
+		title: '提示',
+		content: '确认删除该宠物档案吗?',
+		success: async (res) => {
 			if (res.confirm) {
-				pets.value = pets.value.filter(item => item.id !== pet.id)
-				uni.showToast({ title: '删除成功', icon: 'success' })
+				try {
+					uni.showLoading({ title: '处理中...' })
+					await delPet(pet.id)
+					uni.hideLoading()
+					uni.showToast({ title: '删除成功', icon: 'success' })
+					// 刷新当前页数据
+					loadPets()
+				} catch (error) {
+					uni.hideLoading()
+				}
 			}
 		}
 	})
 }
-
-const pets = ref(petMockData.map(p => ({
-	id: p.id, name: p.name, breed: p.breed, age: p.age,
-	ownerName: p.ownerName, health: p.health, vaccine: p.vaccine, avatar: p.avatar
-})))
 </script>
 
 <style lang="scss" scoped>

+ 13 - 3
pages/my/settings/account-delete/index.vue

@@ -27,6 +27,8 @@
 
 <script setup>
 import { ref } from 'vue'
+import { cancelUser } from '@/api/system/user'
+
 const confirmed = ref(false)
 const onCheckChange = () => { confirmed.value = !confirmed.value }
 const onDelete = () => {
@@ -34,10 +36,18 @@ const onDelete = () => {
 		title: '最终确认',
 		content: '账号注销后无法恢复,确定继续吗?',
 		confirmColor: '#f44336',
-		success: (res) => {
+		success: async (res) => {
 			if (res.confirm) {
-				uni.showToast({ title: '账号已注销', icon: 'success' })
-				setTimeout(() => uni.navigateTo({ url: '/pages/login/login/index' }), 1000)
+				try {
+					// @Author: Antigravity
+					await cancelUser()
+					uni.showToast({ title: '账号已注销', icon: 'success' })
+					// 注销成功后清除 token 并跳转回登录页
+					uni.removeStorageSync('token')
+					setTimeout(() => uni.reLaunch({ url: '/pages/login/index' }), 1500)
+				} catch (error) {
+					// 错误处理由 request.js 统一提示,此处无需额外处理
+				}
 			}
 		}
 	})

+ 14 - 8
pages/my/settings/change-password/index.vue

@@ -1,5 +1,6 @@
 <template>
 	<view class="change-password-page">
+		<nav-bar title="修改密码" bgColor="#fff" color="#000"></nav-bar>
 		<view class="form-container">
 			<uni-forms ref="formRef" :model="formData" :rules="rules">
 				<view class="form-group">
@@ -34,6 +35,8 @@
 
 <script setup>
 import { ref, reactive } from 'vue'
+import navBar from '@/components/nav-bar/index.vue'
+import { updateUserPwd } from '@/api/system/user'
 
 const formRef = ref(null)
 const formData = reactive({
@@ -72,16 +75,19 @@ const submit = async () => {
 		await formRef.value.validate()
 		uni.showLoading({ title: '修改中...' })
 
-		// 模拟接口请求
+		await updateUserPwd(formData.oldPassword, formData.newPassword)
+		
+		uni.hideLoading()
+		uni.showToast({ title: '密码修改成功,请重新登录', icon: 'success' })
+		
+		// 清除令牌并跳转回登录页
 		setTimeout(() => {
-			uni.hideLoading()
-			uni.showToast({ title: '密码修改成功', icon: 'success' })
-			setTimeout(() => {
-				uni.reLaunch({ url: '/pages/login/index' })
-			}, 1500)
-		}, 1000)
+			uni.removeStorageSync('token')
+			uni.reLaunch({ url: '/pages/login/index' })
+		}, 1500)
 	} catch (err) {
-		console.log('表单校验失败', err)
+		uni.hideLoading()
+		console.log('表单校验失败或请求失败', err)
 	}
 }
 </script>

+ 1 - 1
pages/my/settings/index.vue

@@ -3,7 +3,7 @@
 		<nav-bar title="设置"></nav-bar>
 		<view class="menu-list">
 			<view class="cell-group">
-				<view class="cell-item" @click="onTodo">
+				<view class="cell-item" @click="goTo('/pages/my/settings/profile/index')">
 					<text class="cell-title">个人信息</text>
 					<uni-icons type="right" size="14" color="#ccc"></uni-icons>
 				</view>

+ 184 - 0
pages/my/settings/profile/index.vue

@@ -0,0 +1,184 @@
+<template>
+	<view class="profile-page">
+		<nav-bar title="个人信息" bgColor="#fff" color="#000"></nav-bar>
+		
+		<view class="form-container" v-if="!loading">
+			<uni-forms ref="formRef" :model="formData" :rules="rules">
+				<view class="form-group">
+					<uni-forms-item label="用户昵称" name="nickName" required label-width="160rpx">
+						<uni-easyinput v-model="formData.nickName" placeholder="请输入昵称" :inputBorder="false" />
+					</uni-forms-item>
+					<view class="line"></view>
+					
+					<uni-forms-item label="手机号码" name="phonenumber" required label-width="160rpx">
+						<uni-easyinput type="number" v-model="formData.phonenumber" placeholder="请输入手机号码" :inputBorder="false" />
+					</uni-forms-item>
+					<view class="line"></view>
+
+					<uni-forms-item label="邮箱" name="email" label-width="160rpx">
+						<uni-easyinput v-model="formData.email" placeholder="请输入邮箱" :inputBorder="false" />
+					</uni-forms-item>
+					<view class="line"></view>
+
+					<uni-forms-item label="性别" name="sex" label-width="160rpx">
+						<picker :range="sexOptions" range-key="label" @change="onSexChange">
+							<view class="picker-value" :class="{'placeholder': formData.sex === undefined || formData.sex === ''}">
+								{{ getSexLabel }}
+							</view>
+						</picker>
+					</uni-forms-item>
+				</view>
+			</uni-forms>
+
+			<view class="btn-group">
+				<button class="submit-btn" @click="submit">保存修改</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+// @Author: Antigravity
+import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import navBar from '@/components/nav-bar/index.vue'
+import { getInfo, updateUserProfile } from '@/api/system/user'
+
+const loading = ref(true)
+const formRef = ref(null)
+const formData = reactive({
+	nickName: '',
+	phonenumber: '',
+	email: '',
+	sex: '' // 0男 1女 2未知
+})
+
+const sexOptions = [
+	{ label: '男', value: '0' },
+	{ label: '女', value: '1' },
+	{ label: '未知', value: '2' }
+]
+
+const rules = {
+	nickName: {
+		rules: [{ required: true, errorMessage: '请输入昵称' }]
+	},
+	phonenumber: {
+		rules: [
+			{ required: true, errorMessage: '请输入手机号码' },
+			{ pattern: /^1[3-9]\d{9}$/, errorMessage: '请输入正确的手机号码' }
+		]
+	},
+	email: {
+		rules: [{ format: 'email', errorMessage: '请输入正确的邮箱格式' }]
+	}
+}
+
+onLoad(async () => {
+    try {
+        const res = await getInfo()
+        if (res && res.user) {
+            formData.nickName = res.user.nickName || ''
+            formData.phonenumber = res.user.phonenumber || ''
+            formData.email = res.user.email || ''
+            formData.sex = res.user.sex || '2'
+        }
+    } catch (e) {
+        console.error('获取个人信息失败', e)
+    } finally {
+        loading.value = false
+    }
+})
+
+const getSexLabel = computed(() => {
+    const opt = sexOptions.find(o => o.value === String(formData.sex))
+    return opt ? opt.label : '请选择'
+})
+
+const onSexChange = (e) => {
+    formData.sex = sexOptions[e.detail.value].value
+}
+
+const submit = async () => {
+	try {
+		await formRef.value.validate()
+		uni.showLoading({ title: '保存中...' })
+
+		await updateUserProfile(formData)
+		
+		uni.hideLoading()
+		uni.showToast({ title: '个人信息修改成功', icon: 'success' })
+		setTimeout(() => {
+			uni.navigateBack()
+		}, 1000)
+	} catch (err) {
+		uni.hideLoading()
+		console.log('保存失败', err)
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.profile-page {
+	min-height: 100vh;
+	background-color: #f7f8fa;
+}
+
+.form-container {
+    padding-top: 20rpx;
+}
+
+.form-group {
+	background: #fff;
+	margin: 0 24rpx;
+	border-radius: 20rpx;
+	padding: 0 32rpx;
+}
+
+.line {
+	height: 1rpx;
+	background: #f5f5f5;
+	margin: 0;
+}
+
+.btn-group {
+	margin: 60rpx 32rpx 32rpx;
+}
+
+.submit-btn {
+	height: 88rpx;
+	line-height: 88rpx;
+	background: linear-gradient(90deg, #ffd53f, #ff9500);
+	color: #333;
+	border: none;
+	border-radius: 44rpx;
+	font-size: 30rpx;
+	font-weight: bold;
+}
+
+.picker-value {
+    flex: 1;
+    font-size: 26rpx;
+    color: #666;
+    text-align: right;
+    min-height: 40rpx;
+    line-height: 72rpx;
+    padding-right: 20rpx;
+}
+.picker-value.placeholder {
+    color: #999;
+}
+
+:deep(.uni-forms-item) {
+	border: none !important;
+	padding: 10rpx 0;
+}
+:deep(.uni-forms-item__label) {
+    font-size: 28rpx;
+    color: #333;
+}
+:deep(.uni-easyinput__content-input) {
+    text-align: right;
+    font-size: 26rpx;
+}
+</style>

+ 262 - 20
pages/my/user/add/index.vue

@@ -1,39 +1,281 @@
 <template>
 	<view class="user-add-page">
+		<NavBar title="新增用户" bgColor="#fff" color="#000"></NavBar>
+
+		<!-- 基本资料 -->
+		<view class="section-title">基本资料</view>
 		<view class="form-card">
-			<view class="form-item"><text class="form-label">姓名</text><input class="form-input" v-model="form.name" placeholder="请输入姓名" /></view>
-			<view class="form-item"><text class="form-label">手机号</text><input class="form-input" v-model="form.phone" type="number" placeholder="请输入手机号" /></view>
+			<view class="form-item">
+				<text class="form-label require">姓名</text>
+				<input class="form-input" v-model="form.name" placeholder="请输入姓名" />
+			</view>
+			<view class="form-item">
+				<text class="form-label require">手机号</text>
+				<input class="form-input" v-model="form.phone" type="number" placeholder="请输入手机号" />
+			</view>
 			<view class="form-item">
 				<text class="form-label">性别</text>
-				<picker :range="genderOptions" @change="onGenderChange"><view class="picker-value">{{ form.gender || '请选择' }}</view></picker>
+				<picker :range="genderOptions" range-key="label" @change="onGenderChange">
+					<view class="picker-value" :class="{'placeholder': form.gender === undefined}">
+						{{ getGenderLabel }}
+					</view>
+				</picker>
 			</view>
-			<view class="form-item"><text class="form-label">住址</text><input class="form-input" v-model="form.address" placeholder="请输入住址" /></view>
-			<view class="form-item"><text class="form-label">备注</text><textarea class="form-textarea" v-model="form.note" placeholder="请输入备注"></textarea></view>
 		</view>
-		<button class="save-btn" @click="onSave">新增用户</button>
+
+		<!-- 居住信息 -->
+		<view class="section-title">居住信息</view>
+		<view class="form-card">
+			<view class="form-item">
+				<text class="form-label require">所属站点</text>
+				<picker mode="multiSelector" :range="stationOptions" range-key="name" @change="onStationChange" @columnchange="onStationColumnChange" :value="stationIndex">
+					<view class="picker-value" :class="{'placeholder': form.stationId === undefined}">
+						{{ getStationLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">详细住址</text>
+				<input class="form-input" v-model="form.address" placeholder="请输入街道/门牌号" />
+			</view>
+			<view class="form-item">
+				<text class="form-label">房屋类型</text>
+				<picker :range="houseTypeOptions" range-key="label" @change="onHouseTypeChange">
+					<view class="picker-value" :class="{'placeholder': !form.houseType}">
+						{{ getHouseTypeLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">入门方式</text>
+				<picker :range="entryMethodOptions" range-key="label" @change="onEntryMethodChange">
+					<view class="picker-value" :class="{'placeholder': !form.entryMethod}">
+						{{ getEntryMethodLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'password'">
+				<text class="form-label require">开门密码</text>
+				<input class="form-input" v-model="form.entryPassword" placeholder="请输入密码" />
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'key'">
+				<text class="form-label require">钥匙位置</text>
+				<input class="form-input" v-model="form.keyLocation" placeholder="如:地毯下" />
+			</view>
+		</view>
+
+		<!-- 其他 -->
+		<view class="section-title">其他</view>
+		<view class="form-card">
+			<view class="form-item vertical">
+				<text class="form-label">备注说明</text>
+				<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注" />
+			</view>
+		</view>
+
+		<!-- 底部固定操作栏 -->
+		<view class="footer-bar">
+			<button class="save-btn" :loading="saving" @click="onSave">新增用户</button>
+		</view>
 	</view>
 </template>
 
 <script setup>
-import { reactive } from 'vue'
-const genderOptions = ['男', '女']
-const form = reactive({ name: '', phone: '', gender: '', address: '', note: '' })
-const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value] }
-const onSave = () => {
-	if (!form.name || !form.phone) { uni.showToast({ title: '请填写必要信息', icon: 'none' }); return }
-	uni.showToast({ title: '新增成功', icon: 'success' })
-	setTimeout(() => uni.navigateBack(), 1000)
+// @Author: Antigravity
+import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import NavBar from '@/components/nav-bar/index.vue'
+import { addCustomer } from '@/api/archieves/customer'
+import { getInfo } from '@/api/system/user'
+import { listAreaStation } from '@/api/system/areaStation'
+import customerEnums from '@/json/customer.json'
+
+const { houseTypeOptions, entryMethodOptions } = customerEnums
+const genderOptions = [{ label: '男', value: 0 }, { label: '女', value: 1 }]
+
+const saving = ref(false)
+const stationOptions = ref([[], [], []])
+const stationIndex = ref([0, 0, 0])
+const allStationNodes = ref([])
+
+const form = reactive({
+    name: '',
+    phone: '',
+    gender: undefined,
+    areaId: undefined,
+    stationId: undefined,
+    address: '',
+    houseType: '',
+    entryMethod: '',
+    entryPassword: '',
+    keyLocation: '',
+    remark: ''
+})
+
+onLoad(async () => {
+    try {
+        const stationRes = await listAreaStation()
+        allStationNodes.value = Array.isArray(stationRes) ? stationRes : (stationRes?.data || [])
+        initStationPicker()
+    } catch (err) {
+        console.error('获取站点失败', err)
+    }
+})
+
+// 初始化三级联动
+const initStationPicker = () => {
+    const nodes = allStationNodes.value
+    // 第一级:城市 (parentId == 0)
+    const cities = nodes.filter(n => String(n.parentId) === '0')
+    if (cities.length === 0) return
+
+    // 第二级:默认第一个城市的区域
+    const areas = nodes.filter(n => String(n.parentId) === String(cities[0].id))
+    
+    // 第三级:默认第一个区域的站点
+    const stations = areas.length > 0 ? nodes.filter(n => String(n.parentId) === String(areas[0].id)) : []
+
+    stationOptions.value = [cities, areas, stations]
+    stationIndex.value = [0, 0, 0]
+}
+
+// 监听滚动变动
+const onStationColumnChange = (e) => {
+    const column = e.detail.column
+    const value = e.detail.value
+    stationIndex.value[column] = value
+    const nodes = allStationNodes.value
+
+    if (column === 0) {
+        // 切城市:更新区域和站点
+        const selectedCity = stationOptions.value[0][value]
+        if (selectedCity) {
+            const newAreas = nodes.filter(n => String(n.parentId) === String(selectedCity.id))
+            stationOptions.value[1] = newAreas
+            stationOptions.value[2] = newAreas.length > 0 ? nodes.filter(n => String(n.parentId) === String(newAreas[0].id)) : []
+        } else {
+            stationOptions.value[1] = []
+            stationOptions.value[2] = []
+        }
+        stationIndex.value.splice(1, 1, 0)
+        stationIndex.value.splice(2, 1, 0)
+    } else if (column === 1) {
+        // 切区域:更新站点
+        const selectedArea = stationOptions.value[1][value]
+        if (selectedArea) {
+            const newStations = nodes.filter(n => String(n.parentId) === String(selectedArea.id))
+            stationOptions.value[2] = newStations
+        } else {
+            stationOptions.value[2] = []
+        }
+        stationIndex.value.splice(2, 1, 0)
+    }
+}
+
+// 确认选择
+const onStationChange = (e) => {
+    stationIndex.value = e.detail.value
+    const stations = stationOptions.value[2]
+    const selectedStation = stations[stationIndex.value[2]]
+    if (selectedStation && String(selectedStation.type) === '2') {
+        form.stationId = selectedStation.id
+        form.areaId = selectedStation.parentId
+    } else {
+        uni.showToast({ title: '请选择到具体的站点层级', icon: 'none' })
+    }
+}
+
+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 || '请选择')
+
+const getStationLabel = computed(() => {
+    if (!form.stationId) return '请选择'
+    const city = stationOptions.value[0][stationIndex.value[0]]?.name || ''
+    const area = stationOptions.value[1][stationIndex.value[1]]?.name || ''
+    const station = stationOptions.value[2][stationIndex.value[2]]?.name || ''
+    return `${city} - ${area} - ${station}`
+})
+const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value].value }
+const onHouseTypeChange = (e) => { form.houseType = houseTypeOptions[e.detail.value].value }
+const onEntryMethodChange = (e) => {
+    form.entryMethod = entryMethodOptions[e.detail.value].value
+    form.entryPassword = ''
+    form.keyLocation = ''
+}
+
+const onSave = async () => {
+    if (!form.name) return uni.showToast({ title: '请输入姓名', icon: 'none' })
+    if (!form.phone) return uni.showToast({ title: '请输入手机号', icon: 'none' })
+    if (!form.stationId) return uni.showToast({ title: '请选择所属站点', icon: 'none' })
+    if (!form.address) return uni.showToast({ title: '请输入详细住址', icon: 'none' })
+    if (!form.entryMethod) return uni.showToast({ title: '请选择入门方式', icon: 'none' })
+    if (form.entryMethod === 'password' && !form.entryPassword) return uni.showToast({ title: '请输入开门密码', icon: 'none' })
+    if (form.entryMethod === 'key' && !form.keyLocation) return uni.showToast({ title: '请输入钥匙位置', icon: 'none' })
+
+    saving.value = true
+    try {
+        const submitData = { ...form }
+        let tenantId = uni.getStorageSync('tenantId')
+        
+        // 如果本地没有获取到,重新调用接口获取一遍
+        if (!tenantId) {
+            const userRes = await getInfo()
+            if (userRes && userRes.user && userRes.user.tenantId) {
+                tenantId = userRes.user.tenantId
+                uni.setStorageSync('tenantId', tenantId) // 顺便存回本地
+            }
+        }
+        
+        if (tenantId) submitData.tenantId = tenantId
+        
+        await addCustomer(submitData)
+        uni.showToast({ title: '新增成功', icon: 'success' })
+        setTimeout(() => uni.navigateBack(), 1000)
+    } catch(err) {
+        // 500 错误由 request 拦截器统一弹出 res.msg,无需二次封装
+    } finally {
+        saving.value = false
+    }
 }
 </script>
 
 <style lang="scss" scoped>
-.user-add-page { min-height: 100vh; background: #f7f8fa; padding: 24rpx; padding-bottom: 160rpx; }
-.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; }
+.user-add-page { min-height: 100vh; background: #f7f8fa; padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); }
+
+.section-title {
+    font-size: 28rpx; font-weight: bold; color: #666;
+    padding: 24rpx 32rpx 12rpx;
+}
+
+.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; margin: 0 24rpx 24rpx; }
 .form-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .form-item:last-child { border-bottom: none; }
-.form-label { width: 160rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-item.vertical { flex-direction: column; align-items: flex-start; }
+.form-item.vertical .form-textarea { width: 100%; height: 160rpx; margin-top: 16rpx; background: #f9f9f9; padding: 16rpx; box-sizing: border-box; border-radius: 12rpx; font-size: 28rpx; }
+
+.form-label { width: 200rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-label.require::before { content: '*'; color: #f56c6c; margin-right: 4rpx; }
 .form-input { flex: 1; font-size: 28rpx; color: #333; text-align: right; }
-.picker-value { font-size: 28rpx; color: #333; }
-.form-textarea { flex: 1; font-size: 28rpx; color: #333; height: 160rpx; }
-.save-btn { margin-top: 48rpx; width: 100%; height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
+.picker-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; min-height: 40rpx; }
+.picker-value.placeholder { color: #ccc; }
+
+.footer-bar {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	background: #fff;
+	padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
+	box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
+	z-index: 100;
+}
+
+.save-btn {
+    width: 100%;
+    height: 88rpx;
+    background: linear-gradient(90deg, #ffd53f, #ff9500);
+    color: #333; border: none; border-radius: 44rpx;
+    font-size: 32rpx; font-weight: bold; line-height: 88rpx;
+}
 </style>

+ 93 - 17
pages/my/user/detail/index.vue

@@ -1,38 +1,114 @@
 <template>
 	<view class="user-detail-page">
+		<NavBar title="用户详情" bgColor="#ffd53f" color="#5c4314"></NavBar>
 		<view class="user-hero">
-			<image src="https://img.icons8.com/?size=256&id=23235&format=png" class="avatar" mode="aspectFill"></image>
-			<text class="user-name">张先生</text>
-			<text class="user-phone">13800138000</text>
+			<image :src="user.avatarUrl || 'https://img.icons8.com/?size=256&id=23235&format=png'" class="avatar" mode="aspectFill"></image>
+			<view class="hero-info">
+				<text class="user-name">{{ user.name || '-' }}</text>
+				<text class="gender-text" v-if="user.gender !== undefined">({{ user.gender === 1 ? '女' : '男' }})</text>
+			</view>
+			<text class="user-phone">{{ user.phone || '-' }}</text>
+			<view class="tag-list" v-if="user.tags && user.tags.length > 0">
+				<text class="tag-item" v-for="tag in user.tags" :key="tag.id">{{ tag.name }}</text>
+			</view>
 		</view>
 		<view class="info-card">
-			<view class="info-row"><text class="label">性别</text><text class="value">男</text></view>
-			<view class="info-row"><text class="label">住址</text><text class="value">北京市朝阳区三里屯</text></view>
-			<view class="info-row"><text class="label">关联宠物</text><text class="value orange">2只</text></view>
-			<view class="info-row"><text class="label">订单数量</text><text class="value">12单</text></view>
-			<view class="info-row"><text class="label">录入来源</text><text class="value">三里屯店录入</text></view>
-			<view class="info-row"><text class="label">创建时间</text><text class="value">2025-01-15 10:00</text></view>
-			<view class="info-row"><text class="label">备注</text><text class="value">经常周末来</text></view>
+			<view class="section-title">基本信息</view>
+			<view class="info-row"><text class="label">状态</text><text class="value" :class="{ 'orange': user.status === 0 }">{{ user.status === 0 ? '正常' : '禁用' }}</text></view>
+			<view class="info-row"><text class="label">所属区域</text><text class="value">{{ user.areaName || '-' }}</text></view>
+			<view class="info-row"><text class="label">所属站点</text><text class="value">{{ user.stationName || '-' }}</text></view>
+			<view class="info-row"><text class="label">所属品牌</text><text class="value">{{ user.tenantName || '-' }}</text></view>
+			<view class="info-row"><text class="label">录入时间</text><text class="value">{{ user.createTime || '-' }}</text></view>
+			
+			<view class="section-title">居住信息</view>
+			<view class="info-row"><text class="label">详细住址</text><text class="value">{{ user.address || '-' }}</text></view>
+			<view class="info-row"><text class="label">房屋类型</text><text class="value">{{ getHouseTypeLabel(user.houseType) }}</text></view>
+			<view class="info-row"><text class="label">入门方式</text><text class="value">{{ user.entryMethod === 'password' ? '密码' : (user.entryMethod === 'key' ? '钥匙' : user.entryMethod || '-') }}</text></view>
+			<view class="info-row"><text class="label">开门详情</text><text class="value">{{ user.entryMethod === 'password' ? (user.entryPassword || '-') : (user.keyLocation || '-') }}</text></view>
+			
+			<view class="info-row" v-if="user.remark" style="margin-top:20rpx;"><text class="label">备注</text><text class="value">{{ user.remark }}</text></view>
 		</view>
-		<button class="edit-btn" @click="goToEdit">编辑用户</button>
+		<button class="edit-btn" @click="goToEdit">编辑用户资料</button>
 	</view>
 </template>
 
 <script setup>
-const goToEdit = () => uni.navigateTo({ url: '/pages/my/user/edit/index' })
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import NavBar from '@/components/nav-bar/index.vue'
+import { getCustomer } from '@/api/archieves/customer'
+import { listAreaStation } from '@/api/system/areaStation'
+import customerEnums from '@/json/customer.json'
+
+const { houseTypeOptions } = customerEnums
+
+// 将房屋类型 value 转换为中文标签
+const getHouseTypeLabel = (val) => {
+    const item = houseTypeOptions.find(o => o.value === val)
+    return item ? item.label : (val || '-')
+}
+
+const user = ref({})
+
+onLoad((options) => {
+    if (options.id) {
+        loadDetail(options.id)
+    }
+})
+
+const loadDetail = async (id) => {
+    try {
+        uni.showLoading({ title: '加载中...' })
+        const [stationRes, userRes] = await Promise.all([
+            listAreaStation().catch(() => []), 
+            getCustomer(id)
+        ])
+        const data = userRes || {}
+        const allNodes = Array.isArray(stationRes) ? stationRes : (stationRes?.data || [])
+
+        if (data.areaId) {
+            const area = allNodes.find(n => String(n.id) === String(data.areaId))
+            data.areaName = area ? area.name : '-'
+        }
+        if (data.stationId) {
+            const station = allNodes.find(n => String(n.id) === String(data.stationId))
+            data.stationName = station ? station.name : '-'
+        }
+        user.value = data
+    } catch(err) {
+        console.error(err)
+    } finally {
+        uni.hideLoading()
+    }
+}
+
+const goToEdit = () => {
+	if (!user.value.id) return
+	uni.navigateTo({ url: `/pages/my/user/edit/index?id=${user.value.id}` })
+}
 </script>
 
 <style lang="scss" scoped>
 .user-detail-page { min-height: 100vh; background: #f7f8fa; padding-bottom: 160rpx; }
-.user-hero { background: linear-gradient(150deg, #ffd53f, #ff9500); padding: 80rpx 40rpx 60rpx; display: flex; flex-direction: column; align-items: center; }
-.avatar { width: 160rpx; height: 160rpx; border-radius: 50%; border: 8rpx solid #fff; margin-bottom: 20rpx; }
+.user-hero { background: linear-gradient(150deg, #ffd53f, #ff9500); padding: 40rpx 40rpx 60rpx; display: flex; flex-direction: column; align-items: center; }
+.avatar { width: 160rpx; height: 160rpx; border-radius: 50%; border: 8rpx solid #fff; margin-bottom: 20rpx; background:#fff; }
+.hero-info { display: flex; align-items: center; margin-bottom: 8rpx; }
 .user-name { font-size: 40rpx; font-weight: 800; color: #5c4314; }
-.user-phone { font-size: 28rpx; color: #5c4314; opacity: 0.8; margin-top: 8rpx; }
+.gender-text { font-size: 30rpx; color: #5c4314; margin-left: 8rpx; opacity: 0.8; }
+.user-phone { font-size: 28rpx; color: #5c4314; opacity: 0.8; }
+.tag-list { display: flex; flex-wrap: wrap; gap: 12rpx; justify-content: center; margin-top: 16rpx; }
+.tag-item { background: rgba(255,255,255,0.3); color: #5c4314; font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 16rpx; }
+
 .info-card { background: #fff; border-radius: 32rpx 32rpx 0 0; margin-top: -40rpx; padding: 40rpx 32rpx; position: relative; z-index: 3; }
+.section-title { font-size: 32rpx; font-weight: bold; color: #333; margin: 32rpx 0 16rpx; display: flex; align-items: center; }
+.section-title::before { content: ''; display: inline-block; width: 6rpx; height: 28rpx; background: #ffd53f; margin-right: 12rpx; border-radius: 4rpx; }
+.section-title:first-child { margin-top: 0; }
+
 .info-row { display: flex; justify-content: space-between; padding: 24rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .info-row:last-child { border-bottom: none; }
-.label { font-size: 28rpx; color: #999; }
-.value { font-size: 28rpx; color: #333; font-weight: 500; }
+.label { font-size: 28rpx; color: #999; flex-shrink: 0; margin-right: 20rpx; }
+.value { font-size: 28rpx; color: #333; font-weight: 500; text-align: right; word-break: break-all; }
 .value.orange { color: #ff9800; }
+
 .edit-btn { margin: 40rpx 32rpx; width: calc(100% - 64rpx); height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
 </style>

+ 246 - 17
pages/my/user/edit/index.vue

@@ -1,35 +1,264 @@
 <template>
 	<view class="user-edit-page">
+		<NavBar title="编辑用户" bgColor="#fff" color="#000"></NavBar>
+
+		<!-- 基本资料 -->
+		<view class="section-title">基本资料</view>
 		<view class="form-card">
-			<view class="form-item"><text class="form-label">姓名</text><input class="form-input" v-model="form.name" placeholder="请输入姓名" /></view>
-			<view class="form-item"><text class="form-label">手机号</text><input class="form-input" v-model="form.phone" type="number" placeholder="请输入手机号" /></view>
+			<view class="form-item">
+				<text class="form-label require">姓名</text>
+				<input class="form-input" v-model="form.name" placeholder="请输入姓名" />
+			</view>
+			<view class="form-item">
+				<text class="form-label require">手机号</text>
+				<input class="form-input" v-model="form.phone" type="number" placeholder="请输入手机号" />
+			</view>
 			<view class="form-item">
 				<text class="form-label">性别</text>
-				<picker :range="genderOptions" @change="onGenderChange"><view class="picker-value">{{ form.gender || '请选择' }}</view></picker>
+				<picker :range="genderOptions" range-key="label" @change="onGenderChange">
+					<view class="picker-value" :class="{'placeholder': form.gender === undefined}">
+						{{ getGenderLabel }}
+					</view>
+				</picker>
 			</view>
-			<view class="form-item"><text class="form-label">住址</text><input class="form-input" v-model="form.address" placeholder="请输入住址" /></view>
-			<view class="form-item"><text class="form-label">备注</text><textarea class="form-textarea" v-model="form.note" placeholder="请输入备注"></textarea></view>
 		</view>
-		<button class="save-btn" @click="onSave">保存修改</button>
+
+		<!-- 居住信息 -->
+		<view class="section-title">居住信息</view>
+		<view class="form-card">
+			<view class="form-item">
+				<text class="form-label require">所属站点</text>
+				<picker mode="multiSelector" :range="stationOptions" range-key="name" @change="onStationChange" @columnchange="onStationColumnChange" :value="stationIndex">
+					<view class="picker-value" :class="{'placeholder': form.stationId === undefined}">
+						{{ getStationLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">详细住址</text>
+				<input class="form-input" v-model="form.address" placeholder="请输入街道/门牌号" />
+			</view>
+			<view class="form-item">
+				<text class="form-label">房屋类型</text>
+				<picker :range="houseTypeOptions" range-key="label" @change="onHouseTypeChange">
+					<view class="picker-value" :class="{'placeholder': !form.houseType}">
+						{{ getHouseTypeLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item">
+				<text class="form-label require">入门方式</text>
+				<picker :range="entryMethodOptions" range-key="label" @change="onEntryMethodChange">
+					<view class="picker-value" :class="{'placeholder': !form.entryMethod}">
+						{{ getEntryMethodLabel }}
+					</view>
+				</picker>
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'password'">
+				<text class="form-label require">开门密码</text>
+				<input class="form-input" v-model="form.entryPassword" placeholder="请输入密码" />
+			</view>
+			<view class="form-item" v-if="form.entryMethod === 'key'">
+				<text class="form-label require">钥匙位置</text>
+				<input class="form-input" v-model="form.keyLocation" placeholder="如:地毯下" />
+			</view>
+		</view>
+
+		<!-- 其他 -->
+		<view class="section-title">其他</view>
+		<view class="form-card">
+			<view class="form-item vertical">
+				<text class="form-label">备注说明</text>
+				<textarea class="form-textarea" v-model="form.remark" placeholder="请输入备注" />
+			</view>
+		</view>
+
+		<!-- 底部固定操作栏 -->
+		<view class="footer-bar">
+			<button class="save-btn" :loading="saving" @click="onSave">保存修改</button>
+		</view>
 	</view>
 </template>
 
 <script setup>
-import { reactive } from 'vue'
-const genderOptions = ['男', '女']
-const form = reactive({ name: '张先生', phone: '13800138000', gender: '男', address: '北京市朝阳区', note: '' })
-const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value] }
-const onSave = () => { uni.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => uni.navigateBack(), 1000) }
+// @Author: Antigravity
+import { ref, reactive, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import NavBar from '@/components/nav-bar/index.vue'
+import { getCustomer, updateCustomer } from '@/api/archieves/customer'
+import { listAreaStation } from '@/api/system/areaStation'
+import customerEnums from '@/json/customer.json'
+
+const { houseTypeOptions, entryMethodOptions } = customerEnums
+const genderOptions = [{ label: '男', value: 0 }, { label: '女', value: 1 }]
+
+const saving = ref(false)
+const stationOptions = ref([[], [], []])
+const stationIndex = ref([0, 0, 0])
+const allStationNodes = ref([])
+
+const form = reactive({
+    id: undefined,
+    name: '',
+    phone: '',
+    gender: undefined,
+    areaId: undefined,
+    stationId: undefined,
+    address: '',
+    houseType: '',
+    entryMethod: '',
+    entryPassword: '',
+    keyLocation: '',
+    remark: ''
+})
+
+onLoad(async (options) => {
+    try {
+        uni.showLoading({ title: '加载中...' })
+        // 1. 获取站点数据
+        const stationRes = await listAreaStation()
+        allStationNodes.value = Array.isArray(stationRes) ? stationRes : (stationRes?.data || [])
+        
+        // 2. 如果是编辑,获取用户详情
+        if (options.id) {
+            const res = await getCustomer(options.id)
+            if (res) {
+                Object.assign(form, res)
+            }
+        }
+        
+        // 3. 初始化选择器并尝试回定位
+        initStationPicker()
+    } catch(err) {
+        console.error(err)
+    } finally {
+        uni.hideLoading()
+    }
+})
+
+// 初始化三级联动
+const initStationPicker = () => {
+    const nodes = allStationNodes.value
+    if (nodes.length === 0) return
+
+    const cities = nodes.filter(n => String(n.parentId) === '0')
+    
+    // 如果已有 stationId,尝试找索引
+    let cIdx = 0, aIdx = 0, sIdx = 0
+    if (form.stationId) {
+        const targetStation = nodes.find(n => String(n.id) === String(form.stationId))
+        if (targetStation) {
+            const targetArea = nodes.find(n => String(n.id) === String(targetStation.parentId))
+            if (targetArea) {
+                cIdx = cities.findIndex(n => String(n.id) === String(targetArea.parentId))
+                cIdx = cIdx === -1 ? 0 : cIdx
+                
+                const areas = nodes.filter(n => String(n.parentId) === String(cities[cIdx].id))
+                aIdx = areas.findIndex(n => String(n.id) === String(targetArea.id))
+                aIdx = aIdx === -1 ? 0 : aIdx
+                
+                const stations = nodes.filter(n => String(n.parentId) === String(areas[aIdx].id))
+                sIdx = stations.findIndex(n => String(n.id) === String(targetStation.id))
+                sIdx = sIdx === -1 ? 0 : sIdx
+                
+                stationOptions.value = [cities, areas, stations]
+                stationIndex.value = [cIdx, aIdx, sIdx]
+                return
+            }
+        }
+    }
+
+    // 默认初始化
+    const areas = nodes.filter(n => String(n.parentId) === String(cities[0]?.id || ''))
+    const stations = areas.length > 0 ? nodes.filter(n => String(n.parentId) === String(areas[0].id)) : []
+    stationOptions.value = [cities, areas, stations]
+    stationIndex.value = [0, 0, 0]
+}
+
+const onStationColumnChange = (e) => {
+    const column = e.detail.column
+    const value = e.detail.value
+    stationIndex.value[column] = value
+    const nodes = allStationNodes.value
+
+    if (column === 0) {
+        const selectedCity = stationOptions.value[0][value]
+        const newAreas = selectedCity ? nodes.filter(n => String(n.parentId) === String(selectedCity.id)) : []
+        stationOptions.value[1] = newAreas
+        stationOptions.value[2] = newAreas.length > 0 ? nodes.filter(n => String(n.parentId) === String(newAreas[0].id)) : []
+        stationIndex.value[1] = 0
+        stationIndex.value[2] = 0
+    } else if (column === 1) {
+        const selectedArea = stationOptions.value[1][value]
+        const newStations = selectedArea ? nodes.filter(n => String(n.parentId) === String(selectedArea.id)) : []
+        stationOptions.value[2] = newStations
+        stationIndex.value[2] = 0
+    }
+}
+
+const onStationChange = (e) => {
+    stationIndex.value = e.detail.value
+    const stations = stationOptions.value[2]
+    const selectedStation = stations[stationIndex.value[2]]
+    if (selectedStation && String(selectedStation.type) === '2') {
+        form.stationId = selectedStation.id
+        form.areaId = selectedStation.parentId
+    } else {
+        uni.showToast({ title: '请选择到具体的站点层级', icon: 'none' })
+    }
+}
+
+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 || '请选择')
+const getStationLabel = computed(() => {
+    if (!form.stationId) return '请选择'
+    const city = stationOptions.value[0][stationIndex.value[0]]?.name || ''
+    const area = stationOptions.value[1][stationIndex.value[1]]?.name || ''
+    const station = stationOptions.value[2][stationIndex.value[2]]?.name || ''
+    return `${city} - ${area} - ${station}`
+})
+
+const onGenderChange = (e) => { form.gender = genderOptions[e.detail.value].value }
+const onHouseTypeChange = (e) => { form.houseType = houseTypeOptions[e.detail.value].value }
+const onEntryMethodChange = (e) => {
+    form.entryMethod = entryMethodOptions[e.detail.value].value
+    form.entryPassword = ''
+    form.keyLocation = ''
+}
+
+const onSave = async () => {
+    if (!form.name) return uni.showToast({ title: '请输入姓名', icon: 'none' })
+    if (!form.phone) return uni.showToast({ title: '请输入手机号', icon: 'none' })
+    if (!form.stationId) return uni.showToast({ title: '请选择所属站点', icon: 'none' })
+    if (!form.address) return uni.showToast({ title: '请输入详细住址', icon: 'none' })
+    if (!form.entryMethod) return uni.showToast({ title: '请选择入门方式', icon: 'none' })
+    
+    saving.value = true
+    try {
+        await updateCustomer(form)
+        uni.showToast({ title: '保存成功', icon: 'success' })
+        setTimeout(() => uni.navigateBack(), 1000)
+    } catch(err) {
+    } finally {
+        saving.value = false
+    }
+}
 </script>
 
 <style lang="scss" scoped>
-.user-edit-page { min-height: 100vh; background: #f7f8fa; padding: 24rpx; padding-bottom: 160rpx; }
-.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; }
+.user-edit-page { min-height: 100vh; background: #f7f8fa; padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); }
+.section-title { font-size: 28rpx; font-weight: bold; color: #666; padding: 24rpx 32rpx 12rpx; }
+.form-card { background: #fff; border-radius: 24rpx; padding: 8rpx 32rpx; margin: 0 24rpx 24rpx; }
 .form-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 1rpx solid #f5f5f5; }
 .form-item:last-child { border-bottom: none; }
-.form-label { width: 160rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-item.vertical { flex-direction: column; align-items: flex-start; }
+.form-item.vertical .form-textarea { width: 100%; height: 160rpx; margin-top: 16rpx; background: #f9f9f9; padding: 16rpx; box-sizing: border-box; border-radius: 12rpx; font-size: 28rpx; }
+.form-label { width: 200rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
+.form-label.require::before { content: '*'; color: #f56c6c; margin-right: 4rpx; }
 .form-input { flex: 1; font-size: 28rpx; color: #333; text-align: right; }
-.picker-value { font-size: 28rpx; color: #333; }
-.form-textarea { flex: 1; font-size: 28rpx; color: #333; height: 160rpx; }
-.save-btn { margin-top: 48rpx; width: 100%; height: 96rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 48rpx; font-size: 32rpx; font-weight: bold; line-height: 96rpx; }
+.picker-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; min-height: 40rpx; }
+.picker-value.placeholder { color: #ccc; }
+.footer-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); z-index: 100; }
+.save-btn { width: 100%; height: 88rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #333; border: none; border-radius: 44rpx; font-size: 32rpx; font-weight: bold; line-height: 88rpx; }
 </style>

+ 108 - 23
pages/my/user/list/index.vue

@@ -5,11 +5,11 @@
 		<view class="action-bar">
 			<view class="search-box">
 				<uni-icons type="search" size="14" color="#999"></uni-icons>
-				<input type="text" v-model="searchValue" placeholder="搜索姓名/手机号" class="search-input" />
+				<input type="text" v-model="searchValue" placeholder="搜索姓名/手机号" class="search-input" confirm-type="search" @confirm="onSearch" />
 			</view>
-			<picker :range="shopOptions" range-key="text" @change="onShopChange">
+			<picker :range="statusOptions" range-key="label" @change="onStatusFilterChange">
 				<view class="filter-btn">
-					<text>{{ shopOptions[filterShop].text }}</text>
+					<text>{{ statusOptions[currentStatusOption].label }}</text>
 					<uni-icons type="bottom" size="12" color="#666"></uni-icons>
 				</view>
 			</picker>
@@ -20,36 +20,36 @@
 		<view class="list-container">
 			<view class="user-card" v-for="user in users" :key="user.id">
 				<view class="user-header">
-					<image :src="user.avatar" class="user-avatar" mode="aspectFill"></image>
+					<image :src="user.avatarUrl || '/static/default-avatar.png'" class="user-avatar" mode="aspectFill"></image>
 					<view class="user-info-main">
 						<text class="user-name">{{ user.name }}</text>
 						<text class="phone-row">{{ user.phone }}</text>
 					</view>
 					<view class="user-status">
-						<switch :checked="user.isActive" color="#ff9800" style="transform: scale(0.6);"
-							@change="onStatusChange(user)" />
-						<text class="status-text">{{ user.isActive ? '正常' : '禁用' }}</text>
+						<switch :checked="user.status === 0" color="#ff9800" style="transform: scale(0.6);"
+							@change="(e) => onStatusChange(e, user)" />
+						<text class="status-text">{{ user.status === 0 ? '正常' : '禁用' }}</text>
 					</view>
 				</view>
 
 				<view class="user-body">
 					<view class="info-row">
 						<text class="label">住址:</text>
-						<text class="value">{{ user.address }}</text>
+						<text class="value">{{ user.address || '-' }}</text>
 					</view>
 					<view class="info-grid">
 						<view class="grid-cell" @click="goToPetList(user)">
 							<text class="label">关联宠物</text>
-							<text class="value text-warning">{{ user.petCount }}只</text>
+							<text class="value text-warning">{{ user.petCount || 0 }}只</text>
 						</view>
 						<view class="grid-cell" @click="goToOrderList(user)">
 							<text class="label">订单数量</text>
-							<text class="value">{{ user.orderCount }}单</text>
+							<text class="value">{{ user.orderCount || 0 }}单</text>
 						</view>
 					</view>
-					<view class="source-box">
-						<text class="source-tag">{{ user.source }}</text>
-						<text class="create-time">创建时间: {{ user.createTime }}</text>
+					<view class="source-box" v-if="user.source || user.createTime">
+						<text class="source-tag" v-if="user.source">{{ user.source }}</text>
+						<text class="create-time" v-if="user.createTime">创建时间: {{ user.createTime }}</text>
 					</view>
 				</view>
 
@@ -58,31 +58,116 @@
 					<button size="mini" class="action-btn" @click.stop="goToEdit(user)">编辑</button>
 				</view>
 			</view>
+
+			<view v-if="users.length === 0" style="text-align: center; color: #999; padding: 50rpx 0; font-size: 28rpx;">
+				暂无用户数据
+			</view>
 		</view>
 	</view>
 </template>
 
 <script setup>
 import { ref } from 'vue'
+import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
 import NavBar from '@/components/nav-bar/index.vue'
-import userMockData from '@/mock/user.json'
+import { listCustomer, changeCustomerStatus } from '@/api/archieves/customer'
 
 const searchValue = ref('')
-const filterShop = ref(0)
-const shopOptions = [{ text: '全部', value: 0 }, { text: '三里屯店', value: 1 }, { text: '浦东新区店', value: 2 }]
+const users = ref([])
+
+const statusOptions = [{ label: '状态', value: undefined }, { label: '正常', value: 0 }, { label: '停用', value: 1 }]
+const currentStatusOption = ref(0) // Default to 全部状态
+
+const onStatusFilterChange = (e) => {
+    currentStatusOption.value = e.detail.value
+    onSearch()
+}
 
-const onShopChange = (e) => { filterShop.value = Number(e.detail.value) }
+const pageNum = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+
+const loadUsers = async (isLoadMore = false) => {
+	try {
+        uni.showNavigationBarLoading()
+        const res = await listCustomer({
+            pageNum: pageNum.value,
+            pageSize: pageSize.value,
+            keyword: searchValue.value,
+            status: statusOptions[currentStatusOption.value].value
+        })
+        const rows = res?.rows || []
+        if (isLoadMore) {
+            users.value = [...users.value, ...rows]
+        } else {
+            users.value = rows
+        }
+        hasMore.value = users.value.length < (res?.total || 0)
+    } catch (error) {
+        console.error('获取用户列表失败', error)
+    } finally {
+        uni.hideNavigationBarLoading()
+        uni.stopPullDownRefresh()
+    }
+}
 
-const users = ref(userMockData)
+onShow(() => {
+    pageNum.value = 1
+    loadUsers()
+})
+
+onPullDownRefresh(() => {
+    pageNum.value = 1
+    loadUsers()
+})
+
+onReachBottom(() => {
+    if (hasMore.value) {
+        pageNum.value++
+        loadUsers(true)
+    }
+})
+
+const onSearch = () => {
+    pageNum.value = 1
+    loadUsers()
+}
 
 const goToAdd = () => uni.navigateTo({ url: '/pages/my/user/add/index' })
-const goToDetail = (user) => uni.navigateTo({ url: '/pages/my/user/detail/index' })
-const goToEdit = (user) => uni.navigateTo({ url: '/pages/my/user/edit/index' })
+const goToDetail = (user) => uni.navigateTo({ url: `/pages/my/user/detail/index?id=${user.id}` })
+const goToEdit = (user) => uni.navigateTo({ url: `/pages/my/user/edit/index?id=${user.id}` })
 const goToPetList = (user) => uni.navigateTo({ url: `/pages/my/pet/list/index?userId=${user.id}` })
 const goToOrderList = (user) => uni.reLaunch({ url: '/pages/order/list/index' })
-const onStatusChange = (user) => {
-	user.isActive = !user.isActive
-	uni.showToast({ title: user.isActive ? '已启用' : '已禁用', icon: 'none' })
+
+const onStatusChange = (e, user) => {
+    const originalStatus = user.status
+    const targetStatus = e.detail.value ? 0 : 1
+    const text = targetStatus === 0 ? '启用' : '停用'
+
+    uni.showModal({
+        title: '提示',
+        content: `确认要${text}该用户吗?`,
+        success: async (res) => {
+            if (res.confirm) {
+                try {
+                    uni.showLoading({ title: '处理中...' })
+                    await changeCustomerStatus(user.id, targetStatus)
+                    uni.hideLoading()
+                    user.status = targetStatus
+                    uni.showToast({ title: `已${text}`, icon: 'success' })
+                } catch(err) {
+                    uni.hideLoading()
+                    // 恢复源数据渲染,利用 setTimeout 强制触发 vue 的重算
+                    user.status = targetStatus
+                    setTimeout(() => { user.status = originalStatus }, 50)
+                }
+            } else {
+                // 用户取消,恢复 switch
+                user.status = targetStatus
+                setTimeout(() => { user.status = originalStatus }, 50)
+            }
+        }
+    })
 }
 </script>
 

+ 479 - 60
pages/order/apply/index.vue

@@ -35,10 +35,11 @@
 						<text v-else class="placeholder">搜索手机号/姓名</text>
 					</view>
 				</view>
-				<view class="field-row">
+				<view class="field-row" @click="openPetPicker">
 					<text class="field-label">选择宠物</text>
 					<text :class="['field-value', !formData.petName ? 'placeholder' : '']">{{ formData.petName ||
 						'点击选择宠物档案' }}</text>
+					<uni-icons type="right" size="14" color="#ccc"></uni-icons>
 				</view>
 			</view>
 
@@ -59,35 +60,82 @@
 						</view>
 					</view>
 
-					<!-- 接宠路线 -->
+					<!-- 接宠路线 (起点=用户家, 终点=门店) @Author: Antigravity -->
 					<view class="route-box" v-if="formData.transportMode !== 'return_home'">
 						<view class="route-icon pick"><text>接</text></view>
 						<view class="route-fields">
-							<input class="route-input" v-model="formData.pickArea" placeholder="省/市/区" />
-							<input class="route-input" v-model="formData.pickAddress" placeholder="详细地址" />
+							<text class="addr-label">起点</text>
+							<uni-data-picker :localdata="regionTree" v-model="formData.pickArea"
+								:map="{ text: 'name', value: 'code' }" @change="onRegionChange('pick', $event)">
+								<view class="premium-cascader-display">
+									<text :class="['display-text', !formData.pickArea ? 'placeholder' : '']">
+										{{ pickAreaLabel || '选择省/市/区' }}
+									</text>
+									<uni-icons type="right" size="12" color="#ccc"></uni-icons>
+								</view>
+							</uni-data-picker>
+							<input class="route-input" v-model="formData.pickAddress" placeholder="详细地址 (街道/门牌号)" />
+							<text class="addr-label">终点</text>
+							<uni-data-picker :localdata="regionTree" v-model="formData.pickEndArea"
+								:map="{ text: 'name', value: 'code' }" @change="onRegionChange('pickEnd', $event)">
+								<view class="premium-cascader-display">
+									<text :class="['display-text', !formData.pickEndArea ? 'placeholder' : '']">
+										{{ pickEndAreaLabel || '选择省/市/区' }}
+									</text>
+									<uni-icons type="right" size="12" color="#ccc"></uni-icons>
+								</view>
+							</uni-data-picker>
+							<input class="route-input" v-model="formData.pickEndAddress" placeholder="详细地址 (街道/门牌号)" />
 							<view class="contact-row">
 								<input class="route-input half" v-model="formData.pickContact" placeholder="联系人" />
 								<input class="route-input half" v-model="formData.pickPhone" placeholder="电话"
 									type="tel" />
 							</view>
-							<input class="route-input" v-model="formData.pickTime" placeholder="选择接宠时间" />
+							<view class="time-picker-row">
+								<uni-datetime-picker type="datetime" v-model="formData.pickTime" placeholder="选择接宠时间"
+									:border="false" :hide-second="true">
+								</uni-datetime-picker>
+							</view>
 						</view>
 					</view>
 
-					<view class="route-divider"><text class="divider-text">服务门店</text></view>
-
-					<!-- 送宠路线 -->
+					<!-- 送宠路线 (起点=门店, 终点=用户家) @Author: Antigravity -->
 					<view class="route-box" v-if="formData.transportMode !== 'pick_up'">
 						<view class="route-icon send"><text>送</text></view>
 						<view class="route-fields">
-							<input class="route-input" v-model="formData.sendArea" placeholder="省/市/区" />
-							<input class="route-input" v-model="formData.sendAddress" placeholder="详细地址" />
+							<text class="addr-label">起点</text>
+							<uni-data-picker :localdata="regionTree" v-model="formData.sendStartArea"
+								:map="{ text: 'name', value: 'code' }" @change="onRegionChange('sendStart', $event)">
+								<view class="premium-cascader-display">
+									<text :class="['display-text', !formData.sendStartArea ? 'placeholder' : '']">
+										{{ sendStartAreaLabel || '选择省/市/区' }}
+									</text>
+									<uni-icons type="right" size="12" color="#ccc"></uni-icons>
+								</view>
+							</uni-data-picker>
+							<input class="route-input" v-model="formData.sendStartAddress"
+								placeholder="详细地址 (街道/门牌号)" />
+							<text class="addr-label">终点</text>
+							<uni-data-picker :localdata="regionTree" v-model="formData.sendArea"
+								:map="{ text: 'name', value: 'code' }" @change="onRegionChange('send', $event)">
+								<view class="premium-cascader-display">
+									<text :class="['display-text', !formData.sendArea ? 'placeholder' : '']">
+										{{ sendAreaLabel || '选择省/市/区' }}
+									</text>
+									<uni-icons type="right" size="12" color="#ccc"></uni-icons>
+								</view>
+							</uni-data-picker>
+							<input class="route-input" v-model="formData.sendAddress" placeholder="详细地址 (街道/门牌号)" />
 							<view class="contact-row">
 								<input class="route-input half" v-model="formData.sendContact" placeholder="联系人" />
 								<input class="route-input half" v-model="formData.sendPhone" placeholder="电话"
 									type="tel" />
 							</view>
-							<input class="route-input" v-model="formData.sendTime" placeholder="预计送回时间(可选)" />
+							<view class="time-picker-row">
+								<uni-datetime-picker type="datetime" v-model="formData.sendTime"
+									placeholder="预计送还时间(可选)" :border="false" :hide-second="true">
+								</uni-datetime-picker>
+							</view>
 						</view>
 					</view>
 				</view>
@@ -102,8 +150,16 @@
 						<input class="field-input" v-model="formData.packageName" placeholder="请输入套餐名称(选填)" />
 					</view>
 					<text class="address-title">上门服务地址</text>
-					<input class="full-input" v-model="formData.serviceArea" placeholder="省/市/区" />
-					<input class="full-input" v-model="formData.serviceAddress" placeholder="详细地址 (街道/门牌号)" />
+					<uni-data-picker :localdata="regionTree" v-model="formData.serviceArea"
+						:map="{ text: 'name', value: 'code' }" @change="onRegionChange('service', $event)">
+						<view class="premium-full-picker">
+							<text :class="['display-text', !formData.serviceArea ? 'placeholder' : '']">
+								{{ serviceAreaLabel || '请选择省/市/区' }}
+							</text>
+							<uni-icons type="right" size="14" color="#ccc"></uni-icons>
+						</view>
+					</uni-data-picker>
+					<input class="full-input" v-model="formData.serviceAddress" placeholder="详细地址 (街道/路名/门牌号)" />
 
 					<view class="booking-section">
 						<view class="booking-header">
@@ -112,9 +168,15 @@
 						</view>
 						<view class="time-item-row" v-for="(time, index) in formData.feedTimes" :key="index">
 							<text class="index">{{ index + 1 }}.</text>
-							<input class="time-input" v-model="time.start" placeholder="开始时间" />
+							<view class="flex-picker-box">
+								<uni-datetime-picker type="datetime" v-model="time.start" placeholder="开始"
+									:border="false" class="inline-picker" :hide-second="true"></uni-datetime-picker>
+							</view>
 							<text class="to-line">~</text>
-							<input class="time-input" v-model="time.end" placeholder="结束时间(可选)" />
+							<view class="flex-picker-box">
+								<uni-datetime-picker type="datetime" v-model="time.end" placeholder="结束" :border="false"
+									class="inline-picker" :hide-second="true"></uni-datetime-picker>
+							</view>
 							<view class="action-buttons">
 								<view class="circle-btn add" v-if="index === formData.feedTimes.length - 1"
 									@click="addFeedTime">
@@ -159,9 +221,11 @@
 		<view class="popup-mask" v-if="showShopPicker" @click="showShopPicker = false">
 			<view class="popup-content" @click.stop>
 				<text class="popup-title">选择服务门店</text>
-				<view class="popup-item" v-for="shop in shopList" :key="shop" @click="onShopSelect(shop)">
-					<text>{{ shop }}</text>
-				</view>
+				<scroll-view scroll-y="true" class="popup-scroll">
+					<view class="popup-item" v-for="shop in shopList" :key="shop.id" @click="onShopSelect(shop)">
+						<text>{{ shop.name }}</text>
+					</view>
+				</scroll-view>
 			</view>
 		</view>
 
@@ -169,26 +233,70 @@
 		<view class="popup-mask" v-if="showUserPopup" @click="showUserPopup = false">
 			<view class="popup-content user-popup" @click.stop>
 				<text class="popup-title">选择宠主</text>
-				<view class="popup-item" v-for="user in userList" :key="user.id" @click="onUserSelect(user)">
-					<text class="user-item-name">{{ user.name }}</text>
-					<text class="user-item-phone">{{ user.phone }}</text>
+				<view class="search-bar">
+					<input class="search-input" v-model="userSearchKey" placeholder="输入姓名或手机号搜索"
+						@confirm="fetchUsers" />
+					<uni-icons type="search" size="18" color="#999" @click="fetchUsers"></uni-icons>
 				</view>
+				<scroll-view scroll-y="true" class="popup-scroll">
+					<view class="popup-item" v-for="user in userList" :key="user.id" @click="onUserSelect(user)">
+						<text class="user-item-name">{{ user.name }}</text>
+						<text class="user-item-phone">{{ user.phone || user.phoneNumber }}</text>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
+
+		<!-- 宠物选择弹窗 -->
+		<view class="popup-mask" v-if="showPetPopup" @click="showPetPopup = false">
+			<view class="popup-content" @click.stop>
+				<text class="popup-title">选择宠物</text>
+				<scroll-view scroll-y="true" class="popup-scroll">
+					<view class="popup-item" v-for="pet in petList" :key="pet.id" @click="onPetSelect(pet)">
+						<view class="pet-item-cell">
+							<image :src="pet.avatar" class="pet-avatar-mini" mode="aspectFill"></image>
+							<text>{{ pet.name }} ({{ pet.breed }})</text>
+						</view>
+					</view>
+					<view v-if="petList.length === 0" class="empty-tips">该用户下暂无宠物档案</view>
+				</scroll-view>
 			</view>
 		</view>
 	</view>
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue'
+import { ref, reactive, computed, watch } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
+import { listStoreOnOrder } from '@/api/system/store'
+import { listCustomerOnOrder } from '@/api/archieves/customer'
+import { listPetByUser } from '@/api/archieves/pet'
+import { createOrder } from '@/api/order/order'
+import { listRegionTree } from '@/api/system/region'
 
+const showPetPopup = ref(false)
 const activeService = ref('transport')
 const showShopPicker = ref(false)
 const showUserPopup = ref(false)
 const selectedUser = ref(null)
+const selectedShop = ref(null)
+const selectedPet = ref(null)
+const shopList = ref([])
+const userList = ref([])
+const petList = ref([])
+const userSearchKey = ref('')
+const serviceInfo = ref(null)
+const regionTree = ref([])
+const pickAreaLabel = ref('')
+const pickEndAreaLabel = ref('')
+const sendStartAreaLabel = ref('')
+const sendAreaLabel = ref('')
+const serviceAreaLabel = ref('')
 
 const currentServiceName = computed(() => {
+	// @Author: Antigravity
+	if (serviceInfo.value) return serviceInfo.value.name
 	const map = { transport: '宠物接送', feed: '上门喂遛', wash: '上门洗护' }
 	return map[activeService.value]
 })
@@ -197,25 +305,184 @@ const serviceIcon = computed(() => {
 	return map[activeService.value]
 })
 const serviceDesc = computed(() => {
+	// @Author: Antigravity
+	if (serviceInfo.value) return serviceInfo.value.remark
 	const map = { transport: '专车接送 · 全程监护', feed: '喂食添水 · 陪玩遛狗', wash: '专业设备 · 深度清洁' }
 	return map[activeService.value]
 })
 
 onLoad((options) => {
 	if (options.service) activeService.value = options.service
+	// @Author: Antigravity
+	const stored = uni.getStorageSync('currentService')
+	if (stored) {
+		serviceInfo.value = stored
+	}
+	// 初始化获取数据
+	fetchShops()
+	fetchUsers()
+	fetchRegionTree()
 })
 
+const fetchRegionTree = () => {
+	listRegionTree().then(res => {
+		console.log('移动端获取到的地区树数据:', res)
+		regionTree.value = res || []
+	}).catch(err => {
+		console.error('获取地区树异常:', err)
+	})
+}
+
+const onRegionChange = (type, e) => {
+	// @Author: Antigravity
+	const text = e.detail.value.map(v => v.text).join(' / ')
+	if (type === 'pick') pickAreaLabel.value = text
+	else if (type === 'pickEnd') pickEndAreaLabel.value = text
+	else if (type === 'sendStart') sendStartAreaLabel.value = text
+	else if (type === 'send') sendAreaLabel.value = text
+	else if (type === 'service') serviceAreaLabel.value = text
+}
+
+// 根据 code 递归查找地区名称全路径 @Author: Antigravity
+const findRegionLabel = (code, list) => {
+	if (!code || !list || list.length === 0) return ''
+	// 如果是路径格式,取最后一位
+	const targetCode = code.includes('/') ? code.split('/').pop() : code
+
+	const find = (nodes, target) => {
+		for (let item of nodes) {
+			if (item.code === target) return item.name
+			if (item.children && item.children.length > 0) {
+				const childMatch = find(item.children, target)
+				if (childMatch) return item.name + ' / ' + childMatch
+			}
+		}
+		return null
+	}
+	return find(list, targetCode) || ''
+}
+
 const formData = reactive({
-	shopName: '', petName: '', packageName: '',
+	merchantId: '', shopName: '',
+	customerId: '', customerName: '',
+	petId: '', petName: '',
+	packageName: '',
 	transportMode: 'round_trip',
-	pickArea: '', pickAddress: '', pickContact: '', pickPhone: '', pickTime: '',
-	sendArea: '', sendAddress: '', sendContact: '', sendPhone: '', sendTime: '',
+	pickArea: '', pickAddress: '', pickEndArea: '', pickEndAddress: '', pickContact: '', pickPhone: '', pickTime: '',
+	sendStartArea: '', sendStartAddress: '', sendArea: '', sendAddress: '', sendContact: '', sendPhone: '', sendTime: '',
 	serviceArea: '', serviceAddress: '',
 	feedTimes: [{ start: '', end: '' }],
 	otherNote: '',
 	quoteAmount: ''
 })
 
+const fetchShops = () => {
+	// @Author: Antigravity
+	const query = { pageNum: 1, pageSize: 50 }
+	if (serviceInfo.value && serviceInfo.value.id) {
+		query.serviceId = serviceInfo.value.id
+	}
+	listStoreOnOrder(query).then(res => {
+		shopList.value = res.rows || []
+	})
+}
+
+const fetchUsers = () => {
+	listCustomerOnOrder({ pageNum: 1, pageSize: 20, content: userSearchKey.value }).then(res => {
+		userList.value = res.rows || []
+	})
+}
+
+const fetchPets = (userId) => {
+	listPetByUser(userId).then(res => {
+		// @Author: Antigravity
+		// 移动端 request.js 自动解构 data,res 即为宠物列表数组
+		petList.value = Array.isArray(res) ? res : (res.rows || [])
+	})
+}
+
+const onShopSelect = (shop) => {
+	// @Author: Antigravity
+	selectedShop.value = shop
+	formData.merchantId = shop.id
+	formData.shopName = shop.name
+	showShopPicker.value = false
+
+}
+
+const onUserSelect = (user) => {
+	// @Author: Antigravity
+	selectedUser.value = user
+	formData.customerId = user.id
+	formData.customerName = user.name
+
+	// 重置宠物
+	formData.petId = ''
+	formData.petName = ''
+	selectedPet.value = null
+	fetchPets(user.id)
+	showUserPopup.value = false
+}
+
+// 核心回填逻辑:watch 响应门店/用户/地区树任一变化,对齐 Web 端 watch([store, user]) @Author: Antigravity
+watch(
+	[selectedShop, selectedUser, regionTree],
+	([shop, user, tree]) => {
+		// 门店区域码与地址 (areaCode 为逗号分隔) @Author: Antigravity
+		const storeArea = shop?.areaCode || ''
+		const storeAddr = shop?.address || ''
+		const storeLeaf = storeArea.includes(',') ? storeArea.split(',').pop() : storeArea
+
+		// 用户区域码与地址 (regionCode 为斜杠分隔) @Author: Antigravity
+		const userArea = user?.regionCode || ''
+		const userAddr = user?.address || ''
+		const userPhone = user?.phoneNumber || user?.phone || ''
+		const userLeaf = userArea.includes('/') ? userArea.split('/').pop() : userArea
+
+		// 回填接宠路线:起点=用户家,终点=门店 @Author: Antigravity
+		formData.pickArea = userLeaf
+		formData.pickAddress = userAddr
+		formData.pickEndArea = storeLeaf
+		formData.pickEndAddress = storeAddr
+		formData.pickContact = user?.name || ''
+		formData.pickPhone = userPhone
+		pickAreaLabel.value = findRegionLabel(userArea, tree)
+		pickEndAreaLabel.value = findRegionLabel(storeArea.replace(/,/g, '/'), tree)
+
+		// 回填送宠路线:起点=门店,终点=用户家 @Author: Antigravity
+		formData.sendStartArea = storeLeaf
+		formData.sendStartAddress = storeAddr
+		formData.sendArea = userLeaf
+		formData.sendAddress = userAddr
+		formData.sendContact = user?.name || ''
+		formData.sendPhone = userPhone
+		sendStartAreaLabel.value = findRegionLabel(storeArea.replace(/,/g, '/'), tree)
+		sendAreaLabel.value = findRegionLabel(userArea, tree)
+
+		// 回填上门服务地址 @Author: Antigravity
+		formData.serviceArea = userLeaf
+		formData.serviceAddress = userAddr
+		serviceAreaLabel.value = findRegionLabel(userArea, tree)
+	},
+	{ deep: true }
+)
+
+const openPetPicker = () => {
+	if (!formData.customerId) {
+		uni.showToast({ title: '请先选择宠主', icon: 'none' })
+		return
+	}
+	showPetPopup.value = true
+}
+
+const onPetSelect = (pet) => {
+	// @Author: Antigravity
+	selectedPet.value = pet
+	formData.petId = pet.id
+	formData.petName = pet.name
+	showPetPopup.value = false
+}
+
 const totalFulfillmentCommission = computed(() => {
 	if (formData.quoteAmount && !isNaN(parseFloat(formData.quoteAmount))) return parseFloat(formData.quoteAmount).toFixed(2)
 	return '0.00'
@@ -227,30 +494,94 @@ const transportModes = [
 	{ label: '单程送', value: 'return_home' }
 ]
 
-const shopList = ['爱宠生活馆 (三里屯店)', '爱宠生活馆 (国贸店)', '萌宠乐园 (朝阳大悦城店)']
-const userList = [
-	{ id: 1, name: '张先生', phone: '13800138000' },
-	{ id: 2, name: '李小姐', phone: '13900139000' },
-	{ id: 3, name: '王先生', phone: '13612345678' }
-]
-
-const onShopSelect = (shop) => { formData.shopName = shop; showShopPicker.value = false }
-const onUserSelect = (user) => { selectedUser.value = user; showUserPopup.value = false }
-
 const addFeedTime = () => { formData.feedTimes.push({ start: '', end: '' }) }
 const removeFeedTime = (index) => { formData.feedTimes.splice(index, 1) }
 
-const onSubmit = () => {
-	if (!selectedUser.value) { uni.showToast({ title: '请先选择宠主用户', icon: 'none' }); return }
+const onSubmit = async () => {
+	// @Author: Antigravity
+	if (!formData.merchantId) { uni.showToast({ title: '请选择门店', icon: 'none' }); return }
+	if (!formData.customerId) { uni.showToast({ title: '请选择宠主', icon: 'none' }); return }
+	if (!formData.petId) { uni.showToast({ title: '请选择宠物', icon: 'none' }); return }
 	if (!formData.quoteAmount) { uni.showToast({ title: '请输入报价金额', icon: 'none' }); return }
-	uni.showLoading({ title: '正在提交订单...' })
-	setTimeout(() => {
+
+	uni.showLoading({ title: '提交中...', mask: true })
+
+	try {
+		const subOrders = []
+		const baseMode = serviceInfo.value?.mode || 0
+		const defaultContact = selectedUser.value?.name || ''
+		const defaultPhone = selectedUser.value?.phone || selectedUser.value?.phoneNumber || ''
+
+		if (activeService.value === 'transport') {
+			// 接送逻辑:使用表单中回填/编辑后的起终点地址 @Author: Antigravity
+			if (formData.transportMode === 'round_trip' || formData.transportMode === 'pick_up') {
+				subOrders.push({
+					mode: baseMode,
+					type: formData.transportMode === 'round_trip' ? 0 : 2,
+					contact: formData.pickContact || defaultContact,
+					contactPhoneNumber: formData.pickPhone || defaultPhone,
+					serviceTime: formData.pickTime || '',
+					endServiceTime: formData.pickTime || '',
+					fromCode: formData.pickArea || '',
+					fromAddress: formData.pickAddress || '',
+					toCode: formData.pickEndArea || '',
+					toAddress: formData.pickEndAddress || ''
+				})
+			}
+			if (formData.transportMode === 'round_trip' || formData.transportMode === 'return_home') {
+				subOrders.push({
+					mode: baseMode,
+					type: formData.transportMode === 'round_trip' ? 1 : 3,
+					contact: formData.sendContact || defaultContact,
+					contactPhoneNumber: formData.sendPhone || defaultPhone,
+					serviceTime: formData.sendTime || '',
+					endServiceTime: formData.sendTime || '',
+					fromCode: formData.sendStartArea || '',
+					fromAddress: formData.sendStartAddress || '',
+					toCode: formData.sendArea || '',
+					toAddress: formData.sendAddress || ''
+				})
+			}
+		} else {
+			// 上门喂遛或洗护逻辑,补全 fromCode/toCode/toAddress @Author: Antigravity
+			formData.feedTimes.forEach(time => {
+				subOrders.push({
+					mode: baseMode,
+					contact: defaultContact,
+					contactPhoneNumber: defaultPhone,
+					serviceTime: time.start,
+					endServiceTime: time.end || time.start,
+					fromCode: formData.serviceArea || '',
+					fromAddress: formData.serviceAddress,
+					toCode: formData.serviceArea || '',
+					toAddress: formData.serviceAddress
+				})
+			})
+		}
+
+		const payload = {
+			store: formData.merchantId,
+			storeSite: selectedShop.value?.site,
+			customer: formData.customerId,
+			pet: formData.petId,
+			groupPurchasePackageName: formData.packageName || '',
+			service: serviceInfo.value?.id,
+			orderCommission: Math.round(Number(formData.quoteAmount) * 100),
+			remark: formData.otherNote,
+			tenantId: selectedShop.value?.tenantId,
+			subOrders: subOrders
+		}
+
+		await createOrder(payload)
 		uni.hideLoading()
 		uni.showToast({ title: '下单成功', icon: 'success' })
 		setTimeout(() => {
 			uni.reLaunch({ url: '/pages/order/list/index' })
 		}, 1500)
-	}, 1500)
+	} catch (error) {
+		uni.hideLoading()
+		console.error('下单失败:', error)
+	}
 }
 </script>
 
@@ -483,25 +814,13 @@ const onSubmit = () => {
 	flex: 1;
 }
 
-.route-divider {
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	padding: 16rpx 0;
-}
-
-.route-divider::before,
-.route-divider::after {
-	content: '';
-	flex: 1;
-	height: 1rpx;
-	background: #eee;
-}
-
-.divider-text {
-	padding: 0 24rpx;
-	font-size: 22rpx;
-	color: #999;
+/* 起点/终点标签 @Author: Antigravity */
+.addr-label {
+	font-size: 24rpx;
+	color: #606266;
+	font-weight: bold;
+	margin-top: 16rpx;
+	margin-bottom: 4rpx;
 }
 
 .address-title {
@@ -522,10 +841,68 @@ const onSubmit = () => {
 	margin-bottom: 8rpx;
 }
 
+.premium-cascader-display {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 8rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+
+	.display-text {
+		flex: 1;
+		font-size: 26rpx;
+		color: #333;
+
+		&.placeholder {
+			color: #ccc;
+		}
+	}
+}
+
+.premium-full-picker {
+	display: flex;
+	align-items: center;
+	padding: 24rpx 12rpx;
+	background: #f9f9f9;
+	border-radius: 12rpx;
+	margin-bottom: 16rpx;
+
+	.display-text {
+		flex: 1;
+		font-size: 28rpx;
+		color: #333;
+
+		&.placeholder {
+			color: #ccc;
+		}
+	}
+}
+
 .booking-section {
 	margin-top: 24rpx;
 }
 
+.time-picker-row {
+	border-bottom: 1rpx solid #f0f0f0;
+	padding: 4rpx 0;
+}
+
+.flex-picker-box {
+	flex: 1;
+	background: #fcfcfc;
+	border-radius: 12rpx;
+	border: 1rpx solid #f0f0f0;
+	height: 64rpx;
+	display: flex;
+	align-items: center;
+
+	.inline-picker {
+		width: 100%;
+		height: 100%;
+		display: flex;
+		align-items: center;
+	}
+}
+
 .booking-header {
 	display: flex;
 	justify-content: space-between;
@@ -631,10 +1008,11 @@ const onSubmit = () => {
 	right: 0;
 	background: #fff;
 	padding: 20rpx 32rpx;
+	padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
 	display: flex;
 	align-items: center;
 	box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
-	z-index: 100;
+	z-index: 10;
 }
 
 .quotation-fulfillmentCommission-box {
@@ -721,4 +1099,45 @@ const onSubmit = () => {
 .user-item-phone {
 	color: #999;
 }
+
+.popup-scroll {
+	max-height: 600rpx;
+	margin-top: 20rpx;
+}
+
+.search-bar {
+	display: flex;
+	align-items: center;
+	background: #f5f5f5;
+	border-radius: 40rpx;
+	padding: 0 30rpx;
+	margin-bottom: 20rpx;
+	height: 72rpx;
+}
+
+.search-input {
+	flex: 1;
+	font-size: 26rpx;
+	color: #333;
+}
+
+.pet-item-cell {
+	display: flex;
+	align-items: center;
+	gap: 20rpx;
+}
+
+.pet-avatar-mini {
+	width: 60rpx;
+	height: 60rpx;
+	border-radius: 10rpx;
+	background: #f0f0f0;
+}
+
+.empty-tips {
+	text-align: center;
+	color: #999;
+	font-size: 24rpx;
+	padding: 40rpx 0;
+}
 </style>

+ 495 - 88
pages/order/detail/index.vue

@@ -1,10 +1,23 @@
 <template>
 	<view class="order-detail-page">
 		<nav-bar title="订单详情"></nav-bar>
-		<!-- 订单号与状态 -->
-		<view class="order-header">
-			<view class="order-id-row">
-				<text class="order-id">{{ order.id }}</text>
+		<!-- 骨架屏 -->
+		<view class="skeleton-page" v-if="loading">
+			<view class="skeleton-header skeleton-box"></view>
+			<view class="skeleton-progress skeleton-box"></view>
+			<view class="skeleton-row-cards">
+				<view class="skeleton-card skeleton-box"></view>
+				<view class="skeleton-card skeleton-box"></view>
+			</view>
+			<view class="skeleton-content skeleton-box"></view>
+		</view>
+
+		<!-- 真实内容 -->
+		<view class="real-content fade-in" v-else>
+			<!-- 订单号与状态 -->
+			<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>
 			</view>
@@ -31,28 +44,32 @@
 			<view class="info-card pet-card">
 				<text class="card-label">宠物档案</text>
 				<view class="pet-header">
-					<view class="pet-avatar"><text>{{ order.petName[0] }}</text></view>
+					<view class="pet-avatar"><text>{{ (order.petName || '宠')[0] }}</text></view>
 					<view class="pet-basic">
-						<text class="pet-name">{{ order.petName }} ♂</text>
+						<text class="pet-name">
+							{{ order.petName || '-' }} 
+							<text v-if="order.petGender === 1 || order.petGender === '1'">♂</text>
+							<text v-else-if="order.petGender === 0 || order.petGender === '0'">♀</text>
+						</text>
 						<view class="pet-tags">
-							<text class="mini-tag">{{ order.petAge }}岁</text>
-							<text class="mini-tag">{{ order.petWeight }}kg</text>
+							<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>
-					<text class="breed-badge">{{ order.petBreed }}</text>
 				</view>
 				<view class="pet-attrs">
 					<view class="attr-item">
-						<text class="attr-label">绝育状态</text>
-						<text class="attr-val">已绝育</text>
+						<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">已接种</text>
+						<text class="attr-val highlight">{{ order.petVaccine || '-' }}</text>
 					</view>
 					<view class="attr-item full">
 						<text class="attr-label">性格特点</text>
-						<text class="attr-val">活泼好动,喜欢球类玩具</text>
+						<text class="attr-val">{{ order.petCharacter || '-' }}</text>
 					</view>
 				</view>
 			</view>
@@ -106,11 +123,11 @@
 						<view class="task-body">
 							<view class="task-row">
 								<text class="task-label">起点</text>
-								<text class="task-value">{{ order.fromAddress || '获取中...' }}</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>
+								<text class="task-value">{{ order.toAddress || '-' }}</text>
 							</view>
 							<view class="task-row contact-row">
 								<text class="task-value">{{ order.userName }} — {{ order.userPhone }}</text>
@@ -144,8 +161,8 @@
 						</view>
 						<view class="assignee-info">
 							<text class="assignee-name">{{ order.assigneeName }}</text>
-							<text class="assignee-phone">联系电话:13812345678</text>
-							<text class="assignee-zone">归属区域:朝阳服务站</text>
+							<text class="assignee-phone">联系电话:{{ order.assigneePhone }}</text>
+							<text class="assignee-zone">归属区域:{{ order.assigneeZone }}</text>
 						</view>
 					</view>
 				</view>
@@ -153,9 +170,9 @@
 
 			<!-- 服务进度 -->
 			<view class="tab-content" v-if="activeTab === 'progress'">
-				<view class="empty-state" v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey)">
+				<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>
+					<text class="empty-text">服务尚未开始或暂无进度</text>
 				</view>
 				<view class="timeline" v-else>
 					<view class="tl-item" v-for="(tl, i) in serviceTimeline" :key="i">
@@ -164,6 +181,17 @@
 							<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>
@@ -171,7 +199,11 @@
 
 			<!-- 订单日志 -->
 			<view class="tab-content" v-if="activeTab === 'log'">
-				<view class="timeline">
+				<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">
@@ -184,10 +216,37 @@
 			</view>
 		</view>
 
+		</view>
+
+		<!-- 视频全屏预览 -->
+		<view class="video-preview-mask" v-if="videoPreview.visible" @click.stop="closeVideoPreview">
+			<video :src="videoPreview.url" autoplay controls class="preview-video" @click.stop></video>
+			<view class="close-video-btn" @click.stop="closeVideoPreview">
+				<uni-icons type="closeempty" size="24" color="#fff"></uni-icons>
+			</view>
+		</view>
+
 		<!-- 底部取消按钮 -->
-		<view class="cancel-bar safe-bottom" v-if="['wait_dispatch', 'wait_accept'].includes(order.statusKey)">
+		<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>
+
+		<!-- 自定义取消订单弹窗 -->
+		<view class="custom-modal" v-if="showCancelModal">
+			<view class="modal-mask" @click="closeCancelModal"></view>
+			<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>
+				<view class="modal-footer">
+					<view class="modal-btn btn-cancel" @click="closeCancelModal">取消</view>
+					<view class="modal-btn btn-confirm" @click="confirmCancelOrder">确定</view>
+				</view>
+			</view>
+		</view>
+
 	</view>
 </template>
 
@@ -195,16 +254,17 @@
 import { ref, reactive, computed, watch } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import navBar from '@/components/nav-bar/index.vue'
-import { getSubOrderInfo } from '@/api/order/subOrder'
+import { getSubOrderInfo, cancelSubOrder } from '@/api/order/subOrder'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
+import { getFulfiller } from '@/api/fulfiller/fulfiller'
 import { listSubOrderLog } from '@/api/order/subOrderLog'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 
 const activeTab = ref('base')
 const activeService = ref('transport')
 const orderId = ref('')
-const loading = ref(false)
+const loading = ref(true)
 
 const tabList = [
 	{ title: '基础信息', name: 'base' },
@@ -254,7 +314,9 @@ const order = reactive({
 	pet: '',
 	customer: '',
 	fulfiller: '',
-	fulfillerName: ''
+	fulfillerName: '',
+	assigneePhone: '-',
+	assigneeZone: '-'
 })
 
 const orderLogsData = ref([])
@@ -268,26 +330,40 @@ const loadOrderDetail = async (id) => {
 		const res = await getSubOrderInfo(id)
 		console.log('订单详情返回:', res)
 		if (res) {
-			Object.assign(order, res)
+			// 手动逐个赋值,避免 Object.assign 导致的全量字段触发及潜在的递归更新
+			order.id = res.id
 			order.code = res.code || res.id
 			order.status = res.status
 			order.statusKey = getStatusKey(res.status)
 			order.statusText = getStatusName(res.status)
-			order.bookTime = res.serviceTime || ''
-			order.shopName = res.storeName || ''
-			order.userName = res.customerName || ''
-			order.userPhone = res.contactPhoneNumber || ''
-			order.assigneeName = res.fulfillerName || ''
-			order.remark = res.remark || '暂无备注'
-			order.fromAddress = res.fromAddress || ''
-			order.toAddress = res.toAddress || ''
+			order.bookTime = res.serviceTime || '-'
+			order.shopName = res.storeName || '-'
+			order.userName = res.customerName || '-'
+			order.userPhone = res.contactPhoneNumber || '-'
+			order.assigneeName = res.fulfillerName || '-'
+			order.remark = res.remark || '-'
+			order.fromAddress = res.fromAddress || '-'
+			order.toAddress = res.toAddress || '-'
+			order.address = res.address || res.toAddress || '-'
 			order.type = res.type || 'transport'
 			order.subOrderType = res.subOrderType
-
-			if (res.pet) await loadPetInfo(res.pet)
-			if (res.customer) await loadCustomerInfo(res.customer)
-			await loadOrderLogs(id)
-			await loadComplaints(id)
+			order.pet = res.usrPet
+			order.customer = res.usrCustomer
+			order.fulfiller = res.fulfiller
+			order.packageName = res.groupPurchasePackageName || res.packageName || '-'
+			order.createTime = res.createTime || '-'
+
+			// 并行加载关联信息,提升效率并减少串行触发的重演
+			const tasks = []
+			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)
+			])
 		}
 	} catch (error) {
 		console.error('加载订单详情失败:', error)
@@ -301,14 +377,14 @@ const loadPetInfo = async (petId) => {
 	try {
 		const res = await getPet(petId)
 		if (res) {
-			order.petName = res.name || order.petName
-			order.petBreed = res.breed || order.petBreed
-			order.petAge = res.age ? `${res.age}岁` : order.petAge
-			order.petWeight = res.weight ? `${res.weight}kg` : order.petWeight
-			order.petGender = res.gender || order.petGender
-			order.petVaccine = res.vaccineStatus || '未知'
-			order.petCharacter = res.personality || '温顺'
-			order.petHealth = res.healthStatus || '健康'
+			order.petName = res.name || '-'
+			order.petBreed = res.breed || '-'
+			order.petAge = res.age ? `${res.age}岁` : '-'
+			order.petWeight = res.weight ? `${res.weight}kg` : '-'
+			order.petGender = res.gender ?? '-'
+			order.petVaccine = res.vaccineStatus || '-'
+			order.petCharacter = res.personality || '-'
+			order.petHealth = res.healthStatus || '-'
 		}
 	} catch (error) {
 		console.error('加载宠物信息失败:', error)
@@ -319,15 +395,28 @@ const loadCustomerInfo = async (customerId) => {
 	try {
 		const res = await getCustomer(customerId)
 		if (res) {
-			order.userName = res.name || order.userName
-			order.userPhone = res.phone || order.userPhone
-			order.address = res.address || order.address
+			order.userName = res.name || '-'
+			order.userPhone = res.phone || '-'
+			order.address = res.address || '-'
 		}
 	} catch (error) {
 		console.error('加载客户信息失败:', error)
 	}
 }
 
+const loadFulfillerInfo = async (fulfillerId) => {
+	try {
+		const res = await getFulfiller(fulfillerId)
+		if (res) {
+			order.assigneeName = res.name || order.assigneeName
+			order.assigneePhone = res.phone || '-'
+			order.assigneeZone = res.stationName || '-'
+		}
+	} catch (error) {
+		console.error('加载履约者信息失败:', error)
+	}
+}
+
 const loadOrderLogs = async (id) => {
 	try {
 		const res = await listSubOrderLog({ orderId: id })
@@ -358,7 +447,7 @@ const getStatusKey = (status) => {
 
 const getStatusName = (status) => {
 	const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
-	return map[status] || '未知'
+	return map[status] || '-'
 }
 
 const getTransportLabel = (t) => {
@@ -376,7 +465,7 @@ const getTransportClass = (t) => {
 
 const getTypeName = (type) => {
 	const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
-	return map[type] || '未知服务'
+	return map[type] || '-'
 }
 
 onLoad((options) => {
@@ -388,27 +477,86 @@ onLoad((options) => {
 	} else {
 		console.error('订单详情页:缺少有效的订单ID,options =', options)
 		uni.showToast({ title: '订单ID无效', icon: 'none' })
+		loading.value = false
 	}
 	if (options.service) activeService.value = options.service
 })
 
 const progressSteps = computed(() => {
-	const statusOrder = ['wait_dispatch', 'wait_accept', 'serving', 'done']
-	const currentIdx = statusOrder.indexOf(order.statusKey)
-	const allSteps = [
-		{ label: '商户下单', time: '02-04' },
-		{ label: '运营派单', time: '02-04' },
-		{ label: '履约接单', time: '02-05' },
-		{ label: '服务中', time: '02-04' },
+	const status = order.status
+	const steps = [
+		{ label: '商户下单', time: '' },
+		{ label: '运营派单', time: '' },
+		{ label: '履约接单', time: '' },
+		{ label: '服务中', time: '' },
 		{ label: '已完成', time: '' }
 	]
-	if (order.statusKey === 'cancel') return allSteps.map((s, i) => ({ ...s, done: i === 0, active: false }))
-	return allSteps.map((s, i) => ({ ...s, done: i <= currentIdx, active: i === currentIdx + 1 }))
+	const getSystemLogTime = (step) => {
+		const log = (orderLogsData.value || []).find(l => parseInt(l.step) === step)
+		return log ? (log.createTime || log.time) : ''
+	}
+	const getFulfillerLogTime = (step) => {
+		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) : '' }))
+	}
+
+	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
+	}
+
+	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 === 4 || getSystemLogTime(4) || getFulfillerLogTime(99)) {
+		steps[4].time = getSystemLogTime(4) || getFulfillerLogTime(99) || ''
+		active = 5
+	}
+
+	return steps.map((s, i) => ({
+		label: s.label,
+		time: s.time ? s.time.substring(5, 16) : '',
+		done: i < active,
+		active: i === active 
+	}))
 })
 
 const baseInfoList = computed(() => {
 	const list = [
-		{ label: '系统单号', value: order.id },
+		{ label: '系统单号', value: order.code || order.id },
 		{ label: '服务类型', value: currentServiceName.value },
 		{ label: '归属门店', value: order.shopName },
 		{ label: '宠主信息', value: `${order.userName} / ${order.userPhone}` },
@@ -421,34 +569,95 @@ const baseInfoList = computed(() => {
 	return list
 })
 
-const serviceTimeline = [
-	{ time: '2024-02-04 09:30', title: '已接单', desc: '履约者 张小美 已确认接单' },
-	{ time: '2024-02-04 09:50', title: '到达打卡', desc: '已到达指定位置' },
-	{ time: '2024-02-04 10:10', title: '确认出发', desc: '接到宠物,状态良好' }
-]
+// 视频判定辅助函数
+const isVideo = (url) => {
+	if (!url) return false;
+	const videoExts = ['.mp4', '.mov', '.avi', '.wmv', '.webm', '.ogg'];
+	return videoExts.some(ext => String(url).toLowerCase().endsWith(ext));
+}
 
-const orderLogs = [
-	{ time: '2024-02-04 09:30', title: '订单创建', desc: '商户提交订单' },
-	{ time: '2024-02-04 10:00', title: '系统派单', desc: '指派给 张小美' },
-	{ time: '2024-02-04 10:05', title: '接单成功', desc: '履约者已确认接单' },
-	{ time: '2024-02-04 13:55', title: '到达服务点', desc: '履约者到达服务地址' }
-]
+const videoPreview = reactive({
+	visible: false,
+	url: ''
+})
 
-const onCancelOrder = () => {
-	uni.showModal({
-		title: '取消订单',
-		content: `确定要取消订单 [${order.id}] 吗?`,
-		confirmText: '确认取消',
-		confirmColor: '#f44336',
-		success: (res) => {
-			if (res.confirm) {
-				order.statusKey = 'cancel'
-				order.statusText = '已取消'
-				uni.showToast({ title: '订单已取消', icon: 'success' })
-				setTimeout(() => uni.navigateBack(), 1000)
-			}
+const openVideoPreview = (url) => {
+	videoPreview.url = url;
+	videoPreview.visible = true;
+}
+
+const closeVideoPreview = () => {
+	videoPreview.visible = false;
+	videoPreview.url = '';
+}
+
+const previewImage = (url, mediaList) => {
+	const imgUrls = mediaList.filter(m => m.type === 'image').map(m => m.url);
+	uni.previewImage({
+		current: url,
+		urls: imgUrls
+	});
+}
+
+const serviceTimeline = computed(() => {
+	const list = fulfillerLogsData.value || []
+	return list.map((i) => {
+		const rawUrls = i?.photoUrls || [];
+		const urlList = Array.isArray(rawUrls) ? rawUrls : (typeof rawUrls === 'string' ? rawUrls.split(',').filter(Boolean) : []);
+		const media = urlList.map(url => {
+			const type = isVideo(url) ? 'video' : 'image';
+			return { type, url }
+		});
+
+		return {
+			title: i?.title || '-',
+			time: i?.createTime || i?.time || '',
+			desc: i?.content || '',
+			media: media
 		}
 	})
+})
+
+const orderLogs = computed(() => {
+	return (orderLogsData.value || []).map(log => ({
+		time: log.createTime || log.time || '',
+		title: log.title,
+		desc: log.content
+	}))
+})
+
+const showCancelModal = ref(false)
+const cancelReason = ref('')
+
+const onCancelOrder = () => {
+	cancelReason.value = ''
+	showCancelModal.value = true
+}
+
+const closeCancelModal = () => {
+	showCancelModal.value = false
+}
+
+const confirmCancelOrder = async () => {
+	const reason = cancelReason.value.trim()
+	if (!reason) {
+		uni.showToast({ title: '取消原因不能为空', icon: 'none' })
+		return
+	}
+	try {
+		uni.showLoading({ title: '处理中' })
+		await cancelSubOrder({ orderId: order.id, reason })
+		uni.hideLoading()
+		uni.showToast({ title: '订单已取消', icon: 'success' })
+		showCancelModal.value = false
+		
+		// 重新初始化页面数据
+		loadOrderDetail(order.id)
+	} catch (error) {
+		uni.hideLoading()
+		console.error('取消订单失败:', error)
+		uni.showToast({ title: '取消失败', icon: 'none' })
+	}
 }
 </script>
 
@@ -648,16 +857,19 @@ const onCancelOrder = () => {
 
 .pet-tags {
 	display: flex;
-	gap: 8rpx;
-	margin-top: 6rpx;
+	flex-wrap: wrap;
+	gap: 12rpx;
+	margin-top: 10rpx;
+	align-items: center;
 }
 
 .mini-tag {
 	font-size: 20rpx;
 	background: #f5f5f5;
 	border-radius: 8rpx;
-	padding: 2rpx 10rpx;
+	padding: 4rpx 12rpx;
 	color: #666;
+	white-space: nowrap;
 }
 
 .breed-badge {
@@ -666,6 +878,7 @@ const onCancelOrder = () => {
 	color: #e65100;
 	border-radius: 8rpx;
 	padding: 4rpx 12rpx;
+	white-space: nowrap;
 }
 
 .pet-attrs {
@@ -1072,4 +1285,198 @@ const onCancelOrder = () => {
 	border-radius: 48rpx;
 	line-height: 96rpx;
 }
+
+/* 骨架屏动画与样式 */
+.skeleton-page {
+	padding: 24rpx;
+}
+.skeleton-box {
+	background: #e0e0e0;
+	border-radius: 16rpx;
+	margin-bottom: 24rpx;
+	animation: skeleton-shimmer 1.5s infinite linear;
+	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;
+}
+
+/* 渐显进入动画 */
+.fade-in {
+	animation: fadeIn 0.4s ease-out forwards;
+}
+@keyframes fadeIn {
+	from {
+		opacity: 0;
+		transform: translateY(10rpx);
+	}
+	to {
+		opacity: 1;
+		transform: translateY(0);
+	}
+}
+
+/* 日志媒体相关样式 */
+.tl-media {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 16rpx;
+	margin-top: 16rpx;
+}
+.media-item {
+	width: 160rpx;
+	height: 160rpx;
+	border-radius: 12rpx;
+	overflow: hidden;
+	background: #f0f0f0;
+	position: relative;
+}
+.p-img, .p-video-box {
+	width: 100%;
+	height: 100%;
+}
+.play-icon-overlay {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	width: 60rpx;
+	height: 60rpx;
+	background: rgba(0, 0, 0, 0.4);
+	border-radius: 50%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	pointer-events: none;
+}
+
+/* 视频预览弹窗 */
+.video-preview-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: #000;
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.preview-video {
+	width: 100%;
+	height: 60vh;
+}
+.close-video-btn {
+	position: absolute;
+	top: 100rpx;
+	right: 40rpx;
+	width: 64rpx;
+	height: 64rpx;
+	border-radius: 50%;
+	background: rgba(255, 255, 255, 0.2);
+	display: flex;
+	justify-content: center;
+	z-index: 1000;
+}
+
+/* 自定义弹窗样式 */
+.custom-modal {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.modal-mask {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background-color: rgba(0, 0, 0, 0.5);
+}
+.modal-content {
+	position: relative;
+	width: 80%;
+	background-color: #fff;
+	border-radius: 16rpx;
+	overflow: hidden;
+	z-index: 1000;
+}
+.modal-title {
+	padding: 30rpx 0 20rpx;
+	text-align: center;
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+.modal-body {
+	padding: 10rpx 40rpx 30rpx;
+}
+.cancel-input {
+	width: 100%;
+	height: 160rpx;
+	background-color: #f8f8f8;
+	border-radius: 8rpx;
+	padding: 20rpx;
+	font-size: 28rpx;
+	box-sizing: border-box;
+	color: #333;
+}
+.ph-color {
+	color: #999;
+}
+.modal-footer {
+	display: flex;
+	border-top: 1rpx solid #eee;
+}
+.modal-btn {
+	flex: 1;
+	height: 90rpx;
+	line-height: 90rpx;
+	text-align: center;
+	font-size: 30rpx;
+	font-weight: 500;
+}
+.btn-cancel {
+	color: #666;
+	border-right: 1rpx solid #eee;
+}
+.btn-confirm {
+	color: #2196f3;
+}
 </style>

+ 121 - 20
pages/order/list/index.vue

@@ -80,9 +80,8 @@
 						<text class="create-time">下单: {{ order.createTime }}</text>
 						<view class="assign-info">
 							<text class="assign-label">履约信息:</text>
-							<text class="assign-none" v-if="order.statusText === '待派单'">暂未派单</text>
-							<text class="assign-none" v-else-if="order.statusText === '已取消'">订单已关闭</text>
-							<text class="assign-name" v-else>{{ order.assigneeName }}</text>
+							<text class="assign-name" v-if="order.assigneeName">{{ order.assigneeName }}</text>
+							<text class="assign-none" v-else>暂未指派</text>
 						</view>
 						<text class="cancel-time" v-if="order.statusText === '已取消' && order.cancelTime">取消: {{
 							order.cancelTime }}</text>
@@ -120,6 +119,22 @@
 			</view>
 		</view>
 
+		<!-- 自定义取消订单弹窗 -->
+		<view class="custom-modal" v-if="showCancelModal">
+			<view class="modal-mask" @click="closeCancelModal"></view>
+			<view class="modal-content">
+				<view class="modal-title">提示</view>
+				<view class="modal-body">
+					<view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ currentCancelOrder?.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>
+					<view class="modal-btn btn-confirm" @click="confirmCancelOrder">确定</view>
+				</view>
+			</view>
+		</view>
+
 		<custom-tabbar></custom-tabbar>
 	</view>
 </template>
@@ -345,24 +360,39 @@ const onSearch = () => {
 	loadOrders()
 }
 
+const showCancelModal = ref(false)
+const cancelReason = ref('')
+const currentCancelOrder = ref(null)
+
 // 取消订单
 const onCancelOrder = (order) => {
-	uni.showModal({
-		title: '提示',
-		content: `确定要取消订单 [${order.id}] 吗?`,
-		success: async (res) => {
-			if (res.confirm) {
-				try {
-					await cancelSubOrder({ orderId: order.rawId })
-					uni.showToast({ title: '订单已取消', icon: 'success' })
-					loadOrders()
-				} catch (error) {
-					console.error('取消订单失败:', error)
-					uni.showToast({ title: '取消失败', icon: 'none' })
-				}
-			}
-		}
-	})
+	currentCancelOrder.value = order
+	cancelReason.value = ''
+	showCancelModal.value = true
+}
+
+const closeCancelModal = () => {
+	showCancelModal.value = false
+}
+
+const confirmCancelOrder = async () => {
+	const reason = cancelReason.value.trim()
+	if (!reason) {
+		uni.showToast({ title: '取消原因不能为空', icon: 'none' })
+		return
+	}
+	try {
+		uni.showLoading({ title: '处理中' })
+		await cancelSubOrder({ orderId: currentCancelOrder.value.rawId, reason })
+		uni.hideLoading()
+		uni.showToast({ title: '订单已取消', icon: 'success' })
+		showCancelModal.value = false
+		loadOrders()
+	} catch (error) {
+		uni.hideLoading()
+		console.error('取消订单失败:', error)
+		uni.showToast({ title: '取消失败', icon: 'none' })
+	}
 }
 
 // 跳转到订单详情
@@ -375,7 +405,7 @@ const goToDetail = (order) => {
 // 投诉
 const onComplaint = (order) => {
 	uni.navigateTo({
-		url: `/pages/my/complaint-submit/index?orderId=${order.id}`
+		url: `/pages/my/complaint/submit/index?orderId=${order.rawId}&fulfillerId=${order.fulfiller}&orderCode=${order.id}`
 	})
 }
 </script>
@@ -709,4 +739,75 @@ const onComplaint = (order) => {
 	font-size: 28rpx;
 	color: #999;
 }
+
+/* 自定义弹窗样式 */
+.custom-modal {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+.modal-mask {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background-color: rgba(0, 0, 0, 0.5);
+}
+.modal-content {
+	position: relative;
+	width: 80%;
+	background-color: #fff;
+	border-radius: 16rpx;
+	overflow: hidden;
+	z-index: 1000;
+}
+.modal-title {
+	padding: 30rpx 0 20rpx;
+	text-align: center;
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+.modal-body {
+	padding: 10rpx 40rpx 30rpx;
+}
+.cancel-input {
+	width: 100%;
+	height: 160rpx;
+	background-color: #f8f8f8;
+	border-radius: 8rpx;
+	padding: 20rpx;
+	font-size: 28rpx;
+	box-sizing: border-box;
+	color: #333;
+}
+.ph-color {
+	color: #999;
+}
+.modal-footer {
+	display: flex;
+	border-top: 1rpx solid #eee;
+}
+.modal-btn {
+	flex: 1;
+	height: 90rpx;
+	line-height: 90rpx;
+	text-align: center;
+	font-size: 30rpx;
+	font-weight: 500;
+}
+.btn-cancel {
+	color: #666;
+	border-right: 1rpx solid #eee;
+}
+.btn-confirm {
+	color: #2196f3;
+}
 </style>

+ 15 - 1
pages/service/detail/index.vue

@@ -110,8 +110,22 @@ const serviceData = computed(() => {
 })
 
 const goToOrderApply = () => {
+	// @Author: Antigravity
+	if (!serviceInfo.value) return;
+
+	// 根据服务的模式(mode为1代表接送)或名称关键字判断跳转类型,以匹配下单页的分类逻辑
+	let serviceType = 'feed';
+	const name = serviceInfo.value.name || '';
+	const mode = String(serviceInfo.value.mode);
+
+	if (mode === '1' || name.includes('接送') || name.includes('托运')) {
+		serviceType = 'transport';
+	} else if (name.includes('洗') || name.includes('护')) {
+		serviceType = 'wash';
+	}
+
 	uni.navigateTo({
-		url: `/pages/order/apply/index?service=${serviceData.value.type}`
+		url: `/pages/order/apply/index?service=${serviceType}&serviceId=${serviceInfo.value.id}`
 	})
 }
 </script>

BIN
unpackage/cache/apk/__UNI__F19BBAD_cm.apk


+ 1 - 0
unpackage/cache/apk/apkurl

@@ -0,0 +1 @@
+https://app.liuyingyong.cn/build/download/85f4f0a0-380a-11f1-8001-47d9cfd6ab79

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/apk/cmManifestCache.json


+ 4 - 0
unpackage/cache/certdata

@@ -0,0 +1,4 @@
+andrCertfile=D:/Code/HBuilderX/plugins/app-safe-pack/Test.keystore
+andrCertAlias=android
+andrCertPass=ep/Tdjka4Y7WYqDB6/S7dw==
+storePassword=ep/Tdjka4Y7WYqDB6/S7dw==

+ 4 - 0
unpackage/cache/cloudcertificate/certini

@@ -0,0 +1,4 @@
+[General]
+andrCertfile=package.keystore
+andrCertAlias=__UNI__F19BBAD
+andrCertPass="4B/euC8Ksdujm+4E7Nw5zw=="

BIN
unpackage/cache/cloudcertificate/package.keystore


BIN
unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-hdpi.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xhdpi.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xxhdpi.png


BIN
unpackage/cache/wgt/__UNI__F19BBAD/.manifest/icon-android-xxxhdpi.png


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 15 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappautomator.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 31 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappchooselocation.js


BIN
unpackage/cache/wgt/__UNI__F19BBAD/__uniapperror.png


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 31 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappopenlocation.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 31 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniapppicker.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 6 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappquill.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappquillimageresize.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 31 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappscan.js


BIN
unpackage/cache/wgt/__UNI__F19BBAD/__uniappsuccess.png


+ 24 - 0
unpackage/cache/wgt/__UNI__F19BBAD/__uniappview.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>View</title>
+    <link rel="icon" href="data:,">
+    <link rel="stylesheet" href="app.css" />
+    <script>var __uniConfig = {"globalStyle":{},"darkmode":false}</script>
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="uni-app-view.umd.js"></script>
+    
+    
+    
+  </body>
+</html>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/app-config-service.js


+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/app-config.js

@@ -0,0 +1 @@
+(function(){})();

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/app-service.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/app.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/manifest.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/index/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/login/index.css


+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/agreement/detail/index.css

@@ -0,0 +1 @@
+.nav-bar[data-v-ee6d93a8]{position:-webkit-sticky;position:sticky;top:0;z-index:999;width:100%}.nav-content[data-v-ee6d93a8]{height:44px;display:flex;align-items:center;padding:0 1rem;position:relative}.left-box[data-v-ee6d93a8]{position:absolute;left:1rem;z-index:10}.title-box[data-v-ee6d93a8]{flex:1;display:flex;justify-content:center;align-items:center}.title-text[data-v-ee6d93a8]{font-size:1rem;font-weight:700}.right-box[data-v-ee6d93a8]{position:absolute;right:1rem}.agreement-detail-page[data-v-6dfd6b26]{min-height:100vh;background:#f7f8fa;padding-bottom:calc(1.25rem + env(safe-area-inset-bottom))}.content-card[data-v-6dfd6b26]{background:#fff;margin:.75rem;border-radius:.75rem;padding:1.25rem 1rem;box-shadow:0 .125rem .375rem rgba(0,0,0,.03)}.title[data-v-6dfd6b26]{display:block;font-size:1.125rem;font-weight:800;color:#333;margin-bottom:1rem;position:relative;padding-bottom:.75rem;border-bottom:.03125rem solid #f5f5f5}.rich-content[data-v-6dfd6b26]{display:block;font-size:.875rem;color:#555;line-height:1.8;word-break:break-all}.loading-state[data-v-6dfd6b26]{text-align:center;padding:3.125rem 0;color:#999;font-size:.875rem}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/agreement/list/index.css

@@ -0,0 +1 @@
+.nav-bar[data-v-ee6d93a8]{position:-webkit-sticky;position:sticky;top:0;z-index:999;width:100%}.nav-content[data-v-ee6d93a8]{height:44px;display:flex;align-items:center;padding:0 1rem;position:relative}.left-box[data-v-ee6d93a8]{position:absolute;left:1rem;z-index:10}.title-box[data-v-ee6d93a8]{flex:1;display:flex;justify-content:center;align-items:center}.title-text[data-v-ee6d93a8]{font-size:1rem;font-weight:700}.right-box[data-v-ee6d93a8]{position:absolute;right:1rem}.agreement-list-page[data-v-71f3f0af]{min-height:100vh;background:#f7f8fa}.list-container[data-v-71f3f0af]{padding:.75rem}.agreement-item[data-v-71f3f0af]{display:flex;align-items:center;background:#fff;border-radius:.625rem;padding:1rem;margin-bottom:.625rem;box-shadow:0 .125rem .375rem rgba(0,0,0,.03)}.item-info[data-v-71f3f0af]{flex:1}.item-title[data-v-71f3f0af]{display:block;font-size:.9375rem;color:#333;font-weight:500}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/list/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/complaint/submit/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/fee/statistics/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/add/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/detail/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/edit/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/pet/list/index.css


+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/review/list/index.css

@@ -0,0 +1 @@
+.review-list-page[data-v-29390f0a]{min-height:100vh;background:#f7f8fa}.list-container[data-v-29390f0a]{padding:.75rem}.review-card[data-v-29390f0a]{background:#fff;border-radius:.75rem;padding:1rem;margin-bottom:.75rem}.card-header[data-v-29390f0a]{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.order-no[data-v-29390f0a]{font-size:.75rem;color:#999}.stars[data-v-29390f0a]{color:#f7ca3e;font-size:.875rem}.review-content[data-v-29390f0a]{font-size:.875rem;color:#333;line-height:1.6;margin-bottom:.625rem}.card-footer[data-v-29390f0a]{display:flex;justify-content:space-between;padding-top:.5rem;border-top:.03125rem solid #f5f5f5}.time[data-v-29390f0a]{font-size:.75rem;color:#999}.service-type[data-v-29390f0a]{font-size:.75rem;color:#ff9500}.empty-state[data-v-29390f0a]{text-align:center;padding:3.125rem 0;color:#999;font-size:.875rem}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/account-delete/index.css

@@ -0,0 +1 @@
+.account-delete-page[data-v-7ff0a085]{min-height:100vh;background:#f7f8fa;padding:1.25rem 1rem}.warning-card[data-v-7ff0a085]{background:#fff;border-radius:.75rem;padding:1.5rem 1rem;display:flex;flex-direction:column;align-items:center;text-align:center}.warning-title[data-v-7ff0a085]{font-size:1.125rem;font-weight:800;color:#f44336;margin:.75rem 0 .5rem}.warning-text[data-v-7ff0a085]{font-size:.875rem;color:#666;margin-bottom:.75rem}.warning-list[data-v-7ff0a085]{align-self:flex-start}.warning-item[data-v-7ff0a085]{display:block;font-size:.8125rem;color:#999;line-height:2}.confirm-section[data-v-7ff0a085]{margin-top:1.5rem}.check-row[data-v-7ff0a085]{margin-bottom:1rem}.check-label[data-v-7ff0a085]{display:flex;align-items:center;font-size:.8125rem;color:#666}.delete-btn[data-v-7ff0a085]{width:100%;height:3rem;background:#f44336;color:#fff;border:none;border-radius:1.5rem;font-size:1rem;font-weight:700;line-height:3rem}.delete-btn[disabled][data-v-7ff0a085]{background:#ccc}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/change-password/index.css

@@ -0,0 +1 @@
+.nav-bar[data-v-ee6d93a8]{position:-webkit-sticky;position:sticky;top:0;z-index:999;width:100%}.nav-content[data-v-ee6d93a8]{height:44px;display:flex;align-items:center;padding:0 1rem;position:relative}.left-box[data-v-ee6d93a8]{position:absolute;left:1rem;z-index:10}.title-box[data-v-ee6d93a8]{flex:1;display:flex;justify-content:center;align-items:center}.title-text[data-v-ee6d93a8]{font-size:1rem;font-weight:700}.right-box[data-v-ee6d93a8]{position:absolute;right:1rem}.change-password-page[data-v-37cd2ed1]{min-height:100vh;background-color:#f7f8fa;padding-top:.625rem}.form-group[data-v-37cd2ed1]{background:#fff;margin:0 .75rem;border-radius:.625rem;padding:0 1rem}.line[data-v-37cd2ed1]{height:.03125rem;background:#f5f5f5;margin:0}.btn-group[data-v-37cd2ed1]{margin:1.875rem 1rem 1rem}.submit-btn[data-v-37cd2ed1]{height:2.75rem;line-height:2.75rem;background:linear-gradient(90deg,#ffd53f,#ff9500);color:#333;border:none;border-radius:1.375rem;font-size:.9375rem;font-weight:700}.tip-text[data-v-37cd2ed1]{padding:0 1.25rem;display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#999}[data-v-37cd2ed1] .uni-forms-item{border:none!important;padding:.3125rem 0}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/index.css

@@ -0,0 +1 @@
+.nav-bar[data-v-ee6d93a8]{position:-webkit-sticky;position:sticky;top:0;z-index:999;width:100%}.nav-content[data-v-ee6d93a8]{height:44px;display:flex;align-items:center;padding:0 1rem;position:relative}.left-box[data-v-ee6d93a8]{position:absolute;left:1rem;z-index:10}.title-box[data-v-ee6d93a8]{flex:1;display:flex;justify-content:center;align-items:center}.title-text[data-v-ee6d93a8]{font-size:1rem;font-weight:700}.right-box[data-v-ee6d93a8]{position:absolute;right:1rem}.settings-page[data-v-e65bcf38]{min-height:100vh;background:#f7f8fa}.menu-list[data-v-e65bcf38]{padding-top:.75rem}.cell-group[data-v-e65bcf38]{background:#fff;margin:0 .75rem .75rem;border-radius:.75rem;overflow:hidden}.cell-item[data-v-e65bcf38]{display:flex;align-items:center;padding:1rem;border-bottom:.03125rem solid #f5f5f5}.cell-item[data-v-e65bcf38]:last-child{border-bottom:none}.cell-title[data-v-e65bcf38]{flex:1;font-size:.875rem;color:#333}.cell-title.danger[data-v-e65bcf38]{color:#ee0a24}.cell-value[data-v-e65bcf38]{font-size:.8125rem;color:#999;margin-right:.25rem}.danger-group[data-v-e65bcf38]{margin-top:1.25rem}.logout-btn-wrapper[data-v-e65bcf38]{padding:2.5rem 1rem}.logout-btn[data-v-e65bcf38]{width:100%;height:2.75rem;background:#fff;color:#333;border:none;border-radius:1.375rem;font-size:.9375rem;line-height:2.75rem}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/settings/profile/index.css

@@ -0,0 +1 @@
+.nav-bar[data-v-ee6d93a8]{position:-webkit-sticky;position:sticky;top:0;z-index:999;width:100%}.nav-content[data-v-ee6d93a8]{height:44px;display:flex;align-items:center;padding:0 1rem;position:relative}.left-box[data-v-ee6d93a8]{position:absolute;left:1rem;z-index:10}.title-box[data-v-ee6d93a8]{flex:1;display:flex;justify-content:center;align-items:center}.title-text[data-v-ee6d93a8]{font-size:1rem;font-weight:700}.right-box[data-v-ee6d93a8]{position:absolute;right:1rem}.profile-page[data-v-b8525a77]{min-height:100vh;background-color:#f7f8fa}.form-container[data-v-b8525a77]{padding-top:.625rem}.form-group[data-v-b8525a77]{background:#fff;margin:0 .75rem;border-radius:.625rem;padding:0 1rem}.line[data-v-b8525a77]{height:.03125rem;background:#f5f5f5;margin:0}.btn-group[data-v-b8525a77]{margin:1.875rem 1rem 1rem}.submit-btn[data-v-b8525a77]{height:2.75rem;line-height:2.75rem;background:linear-gradient(90deg,#ffd53f,#ff9500);color:#333;border:none;border-radius:1.375rem;font-size:.9375rem;font-weight:700}.picker-value[data-v-b8525a77]{flex:1;font-size:.8125rem;color:#666;text-align:right;min-height:1.25rem;line-height:2.25rem;padding-right:.625rem}.picker-value.placeholder[data-v-b8525a77]{color:#999}[data-v-b8525a77] .uni-forms-item{border:none!important;padding:.3125rem 0}[data-v-b8525a77] .uni-forms-item__label{font-size:.875rem;color:#333}[data-v-b8525a77] .uni-easyinput__content-input{text-align:right;font-size:.8125rem}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/add/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/detail/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/edit/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/my/user/list/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/apply/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/detail/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/order/list/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/service/all/index.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/service/detail/index.css


+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/service/review/index.css

@@ -0,0 +1 @@
+.service-review-page[data-v-4a1e617f]{min-height:100vh;background:#f7f8fa}.summary-bar[data-v-4a1e617f]{background:#fff;padding:1.25rem;display:flex;align-items:baseline;gap:.5rem;border-bottom:.03125rem solid #f5f5f5}.avg-score[data-v-4a1e617f]{font-size:1.75rem;font-weight:900;color:#ff9500}.avg-label[data-v-4a1e617f]{font-size:.8125rem;color:#333}.total-count[data-v-4a1e617f]{font-size:.75rem;color:#999;margin-left:auto}.review-list[data-v-4a1e617f]{padding:.75rem}.review-card[data-v-4a1e617f]{background:#fff;border-radius:.625rem;padding:.875rem;margin-bottom:.625rem}.review-header[data-v-4a1e617f]{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.user-info[data-v-4a1e617f]{display:flex;align-items:center;gap:.5rem}.user-avatar[data-v-4a1e617f]{width:2rem;height:2rem;border-radius:50%;background:#e3f2fd;color:#2196f3;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.875rem}.user-name[data-v-4a1e617f]{font-size:.875rem;color:#333;font-weight:600}.stars[data-v-4a1e617f]{color:#f7ca3e;font-size:.8125rem}.review-content[data-v-4a1e617f]{display:block;font-size:.875rem;color:#555;line-height:1.6;margin-bottom:.375rem}.review-time[data-v-4a1e617f]{display:block;font-size:.75rem;color:#bbb}

+ 1 - 0
unpackage/cache/wgt/__UNI__F19BBAD/pages/store/apply/index.css

@@ -0,0 +1 @@
+.store-apply-page[data-v-8c5be8e5]{min-height:100vh;background:#f7f8fa;padding-bottom:5rem}.hero-banner[data-v-8c5be8e5]{background:linear-gradient(135deg,#ffd53f,#ff9500);padding:2.5rem 1.25rem 1.875rem}.hero-title[data-v-8c5be8e5]{display:block;font-size:1.375rem;font-weight:900;color:#fff}.hero-desc[data-v-8c5be8e5]{display:block;font-size:.875rem;color:rgba(255,255,255,.85);margin-top:.375rem}.form-card[data-v-8c5be8e5]{background:#fff;border-radius:1rem 1rem 0 0;margin-top:-.9375rem;padding:1.25rem 1rem;position:relative;z-index:3}.form-item[data-v-8c5be8e5]{display:flex;align-items:center;padding:.875rem 0;border-bottom:.03125rem solid #f5f5f5}.form-item.column[data-v-8c5be8e5]{flex-direction:column;align-items:flex-start}.form-item[data-v-8c5be8e5]:last-child{border-bottom:none}.form-label[data-v-8c5be8e5]{width:5.625rem;font-size:.875rem;color:#333;flex-shrink:0;margin-bottom:.5rem}.form-input[data-v-8c5be8e5]{flex:1;font-size:.875rem;color:#333;text-align:right}.picker-value[data-v-8c5be8e5]{font-size:.875rem;color:#333}.form-textarea[data-v-8c5be8e5]{width:100%;font-size:.875rem;color:#333;height:6.25rem;background:#f9f9f9;border-radius:.5rem;padding:.625rem}.submit-btn[data-v-8c5be8e5]{margin:1.25rem 1rem;width:calc(100% - 2rem);height:3rem;background:linear-gradient(90deg,#ffd53f,#ff9500);color:#333;border:none;border-radius:1.5rem;font-size:1rem;font-weight:700;line-height:3rem}

+ 6 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/agreement.svg

@@ -0,0 +1,6 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x="14" y="10" width="32" height="40" rx="4" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="22" y1="24" x2="38" y2="24" stroke="#000000" stroke-width="4.5" stroke-linecap="round"/>
+  <line x1="22" y1="32" x2="38" y2="32" stroke="#000000" stroke-width="4.5" stroke-linecap="round"/>
+  <line x1="22" y1="40" x2="30" y2="40" stroke="#000000" stroke-width="4.5" stroke-linecap="round"/>
+</svg>

+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-accept.svg

@@ -0,0 +1,4 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x="14" y="12" width="32" height="36" rx="4" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M22 30L28 36L38 24" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-cancel.svg

@@ -0,0 +1,4 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="30" cy="30" r="22" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M22 22L38 38M38 22L22 38" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-done.svg

@@ -0,0 +1,3 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M12 32L24 44L48 16" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-service.svg

@@ -0,0 +1,4 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="30" cy="30" r="22" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M26 22L38 30L26 38V22Z" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/order-wait.svg

@@ -0,0 +1,4 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="30" cy="30" r="22" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <path d="M30 16V30L40 34" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
unpackage/cache/wgt/__UNI__F19BBAD/static/icon/pet-archive.svg

@@ -0,0 +1,4 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x="10" y="16" width="40" height="28" rx="4" stroke="#000000" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
+  <line x1="18" y1="30" x2="32" y2="30" stroke="#000000" stroke-width="4.5" stroke-linecap="round"/>
+</svg>

Vissa filer visades inte eftersom för många filer har ändrats