index.vue 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053
  1. <template>
  2. <view class="container">
  3. <!-- 自定义导航栏标题 -->
  4. <view class="custom-nav-bar" :style="{ backgroundColor: scrollTop > 20 ? '#fff' : 'transparent' }">
  5. <text class="nav-title">任务中心</text>
  6. </view>
  7. <!-- 顶部自定义导航栏背景 -->
  8. <!-- 顶部自定义导航栏背景 -->
  9. <view class="nav-bg">
  10. <view class="bg-circle-left"></view>
  11. <view class="bg-circle-right"></view>
  12. </view>
  13. <!-- 头部用户信息与统计 -->
  14. <view class="header-section">
  15. <view class="user-info">
  16. <image class="avatar" :src="profile?.avatarUrl || '/static/touxiang.png'" mode="aspectFill"></image>
  17. <view class="info-content">
  18. <view class="top-row">
  19. <text class="name">{{ profile?.name || '未登录' }}</text>
  20. <view class="status-pill" :class="{ 'resting': workStatus !== 'busy' }" @click="goToWorkStatus">
  21. <view class="status-dot-bg"
  22. :class="{ 'busy': workStatus === 'busy', 'disabled': workStatus === 'disabled' }">
  23. <text class="check-mark" v-if="workStatus === 'busy'">✔</text>
  24. <text class="check-mark" v-else style="font-size: 16rpx; line-height: 20rpx;">✕</text>
  25. </view>
  26. <text class="status-text">{{ workStatus === 'busy' ? '接单中' : (workStatus === 'resting' ?
  27. '正在休息' : '已禁用') }}</text>
  28. <text class="arrow-down">▼</text>
  29. </view>
  30. </view>
  31. <view class="bottom-row" @click="handleManualLocation">
  32. <text class="city-label">接单城市:{{ profile?.cityName || '暂无' }}</text>
  33. <text class="city-arrow">></text>
  34. </view>
  35. </view>
  36. <!-- <view class="notification-box">
  37. <image class="bell-img" src="/static/icons/bell.svg"></image>
  38. <view class="badge-count">2</view>
  39. </view> -->
  40. </view>
  41. <!-- 统计卡片 -->
  42. <view class="stats-card">
  43. <view class="stat-item">
  44. <text class="num">{{ orderStats.total }}</text>
  45. <text class="label">全部订单</text>
  46. </view>
  47. <view class="divider"></view>
  48. <view class="stat-item">
  49. <text class="num">{{ orderStats.reject }}</text>
  50. <text class="label">拒接订单</text>
  51. </view>
  52. <view class="divider"></view>
  53. <view class="stat-item">
  54. <text class="num">{{ orderStats.completed }}</text>
  55. <text class="label">完成订单</text>
  56. </view>
  57. <view class="divider"></view>
  58. <view class="stat-item">
  59. <text class="num">{{ (orderStats.fulfillmentCommission / 100).toFixed(2) }}</text>
  60. <text class="label">服务总得</text>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- 任务大厅标题栏 (Sticky Container) -->
  65. <view class="task-header">
  66. <!-- 标题栏内容 (Inner) -->
  67. <view class="header-inner"
  68. :style="{ backgroundColor: (scrollTop > 300 || isFilterShow) ? '#fff' : 'transparent' }">
  69. <view class="left-title">
  70. <view class="orange-bar"></view>
  71. <text class="title">任务大厅</text>
  72. <text class="count">- ({{ taskList.length }}单)</text>
  73. </view>
  74. <view class="filter-options">
  75. <view class="dropdown" @click="showFilterDropdown">
  76. <text>筛选条件</text>
  77. <text class="arrow-down">﹀</text>
  78. </view>
  79. </view>
  80. </view>
  81. <!-- 筛选遮罩 (Absolute Child: Starts below header) -->
  82. <view class="filter-mask" v-if="isFilterShow" @click="closeFilter"></view>
  83. <!-- 筛选下拉面板 (Absolute Child) -->
  84. <view class="filter-panel" :class="{ show: isFilterShow }">
  85. <view class="filter-section">
  86. <text class="section-title">服务类型</text>
  87. <view class="options-grid">
  88. <view class="option-btn" :class="{ active: tempFilter.service === null }"
  89. @click="selectService(null)">全部</view>
  90. <view class="option-btn" v-for="item in serviceList" :key="item.id"
  91. :class="{ active: tempFilter.service === item.id }" @click="selectService(item.id)">{{
  92. item.name }}</view>
  93. </view>
  94. </view>
  95. <view class="filter-section">
  96. <text class="section-title">金额</text>
  97. <view class="options-grid">
  98. <view class="option-btn" :class="{ active: tempFilter.amount === '全部' }"
  99. @click="selectAmount('全部')">全部</view>
  100. <view class="option-btn" :class="{ active: tempFilter.amount === '100以下' }"
  101. @click="selectAmount('100以下')">100以下</view>
  102. <view class="option-btn" :class="{ active: tempFilter.amount === '100-200' }"
  103. @click="selectAmount('100-200')">100-200</view>
  104. <view class="option-btn" :class="{ active: tempFilter.amount === '200-500' }"
  105. @click="selectAmount('200-500')">200-500</view>
  106. <view class="option-btn" :class="{ active: tempFilter.amount === '500以上' }"
  107. @click="selectAmount('500以上')">500以上</view>
  108. </view>
  109. </view>
  110. <view class="filter-actions">
  111. <button class="action-btn reset" @click="resetFilter">重置</button>
  112. <button class="action-btn confirm" @click="confirmFilter">确认</button>
  113. </view>
  114. </view>
  115. </view>
  116. <!-- 列表容器 -->
  117. <view class="task-list-container">
  118. <!-- 任务列表 -->
  119. <view class="task-list">
  120. <view class="task-card" v-for="item in taskList" :key="item.id" @click="goToDetail(item)">
  121. <view class="card-header">
  122. <view class="type-badge">
  123. <image class="type-icon" :src="item.typeIcon"></image>
  124. <text class="type-text">{{ item.typeText }}</text>
  125. </view>
  126. <text class="price">¥{{ item.price }}</text>
  127. </view>
  128. <view class="card-body">
  129. <view class="time-row">
  130. <text class="label">{{ item.timeLabel }}:</text>
  131. <text class="value">{{ item.time }}</text>
  132. </view>
  133. <view class="pet-card">
  134. <image class="pet-avatar" :src="item.petAvatarUrl || item.petAvatar" mode="aspectFill">
  135. </image>
  136. <view class="pet-info">
  137. <text class="pet-name">{{ item.petName }}</text>
  138. <text class="pet-breed">品种: {{ item.petBreed }}</text>
  139. </view>
  140. </view>
  141. <view class="route-info">
  142. <template v-if="item.type === 1">
  143. <view class="route-item" @click.stop="openNavigation(item, 'start')">
  144. <view class="icon-circle start">起</view>
  145. <view class="route-line-vertical"></view>
  146. <view class="address-box">
  147. <text class="addr-title">{{ item.startLocation }}</text>
  148. <text class="addr-desc">{{ item.startAddress }}</text>
  149. </view>
  150. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  151. style="flex-shrink: 0; align-self: center;"></image>
  152. </view>
  153. <view class="route-item" @click.stop="openNavigation(item, 'end')">
  154. <view class="icon-circle end">终</view>
  155. <view class="address-box">
  156. <text class="addr-title">{{ item.endLocation }}</text>
  157. <text class="addr-desc">{{ item.endAddress }}</text>
  158. </view>
  159. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  160. style="flex-shrink: 0; align-self: center;"></image>
  161. </view>
  162. </template>
  163. <template v-else>
  164. <view class="route-item" @click.stop="openNavigation(item, 'end')">
  165. <view class="icon-circle service">服</view>
  166. <view class="address-box">
  167. <text class="addr-title">{{ item.endLocation }}</text>
  168. <text class="addr-desc">{{ item.endAddress }}</text>
  169. </view>
  170. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  171. style="flex-shrink: 0; align-self: center;"></image>
  172. </view>
  173. <view class="service-content" v-if="item.serviceContent">
  174. <text class="content-label">服务内容:</text>
  175. <text>{{ item.serviceContent }}</text>
  176. </view>
  177. </template>
  178. </view>
  179. <view class="remark-box" v-if="item.remark">
  180. <text>备注:{{ item.remark }}</text>
  181. </view>
  182. </view>
  183. <!-- action-btns class is in style.css but template used card-footer. style.css: .action-btns, index.vue: .card-footer -->
  184. <!-- Wait, style.css has .action-btns { display: flex; ... } and .btn.reject/accept -->
  185. <!-- I should use .action-btns instead of .card-footer -->
  186. <view class="action-btns">
  187. <button class="btn reject" @click.stop="openRejectModal(item)">拒绝</button>
  188. <button class="btn accept" @click.stop="openAcceptModal(item)">接单</button>
  189. </view>
  190. </view>
  191. <!-- Padding for safe area/tabbar -->
  192. <view style="height: 120rpx;"></view>
  193. </view>
  194. </view>
  195. <!-- 自定义确认弹窗 -->
  196. <view class="modal-mask" v-if="showConfirmModal">
  197. <view class="custom-modal">
  198. <text class="modal-title">提示</text>
  199. <text class="modal-content">是否确定结束休息,开始接单?</text>
  200. <view class="modal-btns">
  201. <button class="modal-btn cancel" @click="closeConfirmModal">取消</button>
  202. <button class="modal-btn confirm" @click="confirmStartWork">确定</button>
  203. </view>
  204. </view>
  205. </view>
  206. <!-- 宠物档案弹窗 -->
  207. <view class="pet-modal-mask" v-if="showPetModal" @click="closePetProfile">
  208. <view class="pet-modal-content" @click.stop>
  209. <view class="pet-modal-header">
  210. <text class="pet-modal-title">宠物档案</text>
  211. <view class="close-icon-btn" @click="closePetProfile">×</view>
  212. </view>
  213. <scroll-view scroll-y class="pet-modal-scroll">
  214. <!-- Basic Info -->
  215. <view class="pet-base-info">
  216. <image class="pm-avatar" :src="currentPetInfo.petAvatar" mode="aspectFill"></image>
  217. <view class="pm-info-text">
  218. <view class="pm-name-row">
  219. <text class="pm-name">{{ currentPetInfo.petName }}</text>
  220. <!-- Gender badge -->
  221. <view class="pm-gender" v-if="currentPetInfo.petGender === 'M'">
  222. <text class="gender-icon">♂</text>
  223. <text>公</text>
  224. </view>
  225. <view class="pm-gender female" v-else-if="currentPetInfo.petGender === 'F'">
  226. <text class="gender-icon">♀</text>
  227. <text>母</text>
  228. </view>
  229. </view>
  230. <text class="pm-breed">品种:{{ currentPetInfo.petBreed }}</text>
  231. </view>
  232. </view>
  233. <!-- Details Grid -->
  234. <view class="pm-detail-grid">
  235. <view class="pm-grid-item half">
  236. <text class="pm-label">年龄</text>
  237. <text class="pm-val">{{ currentPetInfo.petAge || '未知' }}</text>
  238. </view>
  239. <view class="pm-grid-item half">
  240. <text class="pm-label">体重</text>
  241. <text class="pm-val">{{ currentPetInfo.petWeight || '未知' }}</text>
  242. </view>
  243. <view class="pm-grid-item full">
  244. <text class="pm-label">性格</text>
  245. <text class="pm-val">{{ currentPetInfo.petPersonality || '无' }}</text>
  246. </view>
  247. <view class="pm-grid-item full">
  248. <text class="pm-label">爱好</text>
  249. <text class="pm-val">{{ currentPetInfo.petHobby || '无' }}</text>
  250. </view>
  251. <view class="pm-grid-item full">
  252. <text class="pm-label">备注</text>
  253. <text class="pm-val">{{ currentPetInfo.petRemark || '无特殊过敏史' }}</text>
  254. </view>
  255. </view>
  256. <!-- Tags -->
  257. <view class="pm-tags" v-if="currentPetInfo.petTags && currentPetInfo.petTags.length > 0">
  258. <view class="pm-tag" v-for="(tag, index) in currentPetInfo.petTags" :key="index">{{ tag }}
  259. </view>
  260. </view>
  261. <!-- Log Section -->
  262. <view class="pm-section-title">
  263. <view class="orange-bar"></view>
  264. <text>备注日志</text>
  265. </view>
  266. <view class="pm-log-list">
  267. <view class="pm-log-item" v-for="(log, lIndex) in currentPetInfo.petLogs" :key="lIndex">
  268. <text class="pm-log-date">{{ log.date }}</text>
  269. <text class="pm-log-text">{{ log.content }}</text>
  270. <text class="pm-log-recorder">{{ log.recorder === '系统记录' ? '' : '记录人: ' }}{{ log.recorder
  271. }}</text>
  272. </view>
  273. </view>
  274. <view style="height: 40rpx;"></view>
  275. <button class="pm-bottom-close" @click="closePetProfile">关闭</button>
  276. <view style="height: 20rpx;"></view>
  277. </scroll-view>
  278. </view>
  279. </view>
  280. </view>
  281. <!-- 拒绝接单弹窗 -->
  282. <view class="modal-mask" v-if="showRejectModal">
  283. <view class="custom-modal">
  284. <text class="modal-title">拒绝接单</text>
  285. <textarea class="reject-textarea" v-model="rejectReason" placeholder="请输入拒绝理由(必填)"
  286. maxlength="100"></textarea>
  287. <view class="modal-btns mt-30">
  288. <button class="modal-btn cancel" @click="closeRejectModal">取消</button>
  289. <button class="modal-btn confirm" @click="confirmReject">提交</button>
  290. </view>
  291. </view>
  292. </view>
  293. <!-- 确认接单弹窗 -->
  294. <view class="modal-mask" v-if="showAcceptConfirmModal">
  295. <view class="custom-modal">
  296. <text class="modal-title">接单确认</text>
  297. <view class="modal-content-box">
  298. <text class="modal-content-main">请确认是否接收此订单?</text>
  299. <text class="modal-content-sub">接单后请尽快通过电话联系用户</text>
  300. </view>
  301. <view class="modal-btns mt-30">
  302. <button class="modal-btn cancel" @click="closeAcceptModal">再想想</button>
  303. <button class="modal-btn confirm" @click="confirmAccept">确认接单</button>
  304. </view>
  305. </view>
  306. </view>
  307. <!-- 选择地图导航弹窗 -->
  308. <view class="nav-modal-mask" v-if="showNavModal" @click="closeNavModal">
  309. <view class="nav-action-sheet" @click.stop>
  310. <view class="nav-sheet-title">选择地图进行导航</view>
  311. <view class="nav-sheet-item" @click="chooseMap('高德')">高德地图</view>
  312. <view class="nav-sheet-item" @click="chooseMap('腾讯')">腾讯地图</view>
  313. <view class="nav-sheet-item" @click="chooseMap('百度')">百度地图</view>
  314. <view class="nav-sheet-gap"></view>
  315. <view class="nav-sheet-item cancel" @click="closeNavModal">取消</view>
  316. </view>
  317. </view>
  318. <custom-tabbar currentPath="pages/home/index"></custom-tabbar>
  319. </template>
  320. <script>
  321. import { getMyProfile } from '@/api/fulfiller/fulfiller'
  322. import { getPendingOrders, acceptOrder, getOrderCount, rejectOrderApi } from '@/api/order/subOrder'
  323. import { listAllService } from '@/api/service/list'
  324. import { getAreaStationList } from '@/api/system/areaStation'
  325. import { isLoggedIn } from '@/utils/auth'
  326. import { reportGps } from '@/utils/gps'
  327. import customTabbar from '@/components/custom-tabbar/index.vue'
  328. export default {
  329. components: {
  330. customTabbar
  331. },
  332. data() {
  333. return {
  334. taskList: [],
  335. currentFilter: 'default', // default, distance, time
  336. filterCondition: '筛选条件',
  337. sortDistance: 'asc', // asc, desc
  338. sortTime: 'asc',
  339. scrollTop: 0, // Track scroll position
  340. isFilterShow: false,
  341. tempFilter: {
  342. service: null,
  343. distance: '全部',
  344. amount: '全部'
  345. },
  346. activeFilter: {
  347. service: null,
  348. distance: '全部',
  349. amount: '全部'
  350. },
  351. workStatus: 'resting', // resting | busy | disabled
  352. showConfirmModal: false,
  353. showPetModal: false,
  354. currentPetInfo: {},
  355. showRejectModal: false,
  356. rejectReason: '',
  357. currentOrder: null,
  358. showAcceptConfirmModal: false,
  359. showNavModal: false,
  360. navTargetItem: null,
  361. navTargetPointType: '',
  362. profile: null,
  363. profileLoading: false,
  364. serviceList: [],
  365. orderStats: {
  366. total: 0,
  367. reject: 0,
  368. completed: 0,
  369. price: 0
  370. }
  371. }
  372. },
  373. onPageScroll(e) {
  374. this.scrollTop = e.scrollTop;
  375. },
  376. async onLoad() {
  377. // Initial load
  378. this.checkWorkStatus();
  379. await this.loadServiceList();
  380. this.loadTaskList();
  381. // 显式请求一次定位授权
  382. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  383. },
  384. onShow() {
  385. uni.hideTabBar()
  386. this.checkWorkStatus();
  387. if (isLoggedIn()) {
  388. // 每次进入页面强制刷新所有展示数据
  389. this.loadProfile()
  390. this.loadOrderStats()
  391. this.loadTaskList()
  392. this.loadServiceList() // 确保服务配置也是最新的
  393. }
  394. },
  395. async onPullDownRefresh() {
  396. this.checkWorkStatus();
  397. try {
  398. await this.loadServiceList();
  399. const tasks = [
  400. this.loadTaskList()
  401. ];
  402. if (isLoggedIn()) {
  403. tasks.push(this.loadProfile());
  404. tasks.push(this.loadOrderStats());
  405. }
  406. await Promise.all(tasks);
  407. } catch (err) {
  408. console.error('刷新异常:', err);
  409. } finally {
  410. uni.stopPullDownRefresh();
  411. uni.showToast({ title: '刷新成功', icon: 'success' });
  412. }
  413. },
  414. methods: {
  415. async loadProfile() {
  416. if (this.profileLoading) return
  417. this.profileLoading = true
  418. try {
  419. const res = await getMyProfile()
  420. const data = res.data || null
  421. if (data) {
  422. // 以服务器返回的状态为准进行更新
  423. if (data.status) {
  424. this.workStatus = data.status;
  425. uni.setStorageSync('workStatus', data.status);
  426. }
  427. // 需求:头部的接单城市使用站点往上找到城市,而非直接显示站点
  428. if (data.stationId) {
  429. try {
  430. const stationRes = await getAreaStationList();
  431. const list = stationRes.data || [];
  432. const currentStation = list.find(i => i.id === data.stationId);
  433. if (currentStation) {
  434. // 向上溯源:直到找到 parentId 为 0 的节点(即顶层城市)
  435. let node = currentStation;
  436. while (node && node.parentId !== 0) {
  437. let parent = list.find(i => i.id === node.parentId);
  438. if (parent) {
  439. node = parent;
  440. } else {
  441. break;
  442. }
  443. }
  444. // 将溯源到的节点名称作为显示城市
  445. data.cityName = node.name;
  446. }
  447. } catch (e) {
  448. console.error('溯源城市失败:', e);
  449. }
  450. }
  451. }
  452. this.profile = data
  453. } catch (err) {
  454. console.error('获取个人信息失败:', err)
  455. } finally {
  456. this.profileLoading = false
  457. }
  458. },
  459. async loadServiceList() {
  460. try {
  461. const res = await listAllService()
  462. this.serviceList = res.data || []
  463. } catch (err) {
  464. console.error('获取服务类型失败:', err)
  465. }
  466. },
  467. async loadOrderStats() {
  468. try {
  469. const res = await getOrderCount()
  470. this.orderStats = res.data || { total: 0, reject: 0, completed: 0, fulfillmentCommission: 0 }
  471. } catch (err) {
  472. console.error('获取订单统计失败:', err)
  473. }
  474. },
  475. checkWorkStatus() {
  476. const status = uni.getStorageSync('workStatus');
  477. if (status) {
  478. this.workStatus = status;
  479. } else {
  480. // 默认状态为休息
  481. this.workStatus = 'resting';
  482. uni.setStorageSync('workStatus', 'resting');
  483. }
  484. },
  485. toggleFilter() {
  486. if (this.workStatus === 'resting') return; // Disable filter when resting? Or keep it? User didn't specify, but usually disabled. Let's keep it enabled for now as they might look at filters before working.
  487. this.isFilterShow = !this.isFilterShow;
  488. },
  489. goToWorkStatus() {
  490. uni.navigateTo({
  491. url: '/pages/home/work-status/index'
  492. });
  493. },
  494. async handleManualLocation() {
  495. try {
  496. uni.showLoading({ title: '定位获取中...', mask: true });
  497. await reportGps(true);
  498. uni.showToast({ title: '位置已更新', icon: 'success' });
  499. } catch (e) {
  500. console.error('Manual location failed', e);
  501. } finally {
  502. uni.hideLoading();
  503. }
  504. },
  505. startWork() {
  506. this.showConfirmModal = true;
  507. },
  508. confirmStartWork() {
  509. this.workStatus = 'busy';
  510. uni.setStorageSync('workStatus', 'busy');
  511. this.loadTaskList();
  512. this.showConfirmModal = false;
  513. uni.showToast({ title: '已开始接单', icon: 'success' });
  514. },
  515. closeConfirmModal() {
  516. this.showConfirmModal = false;
  517. },
  518. showPetProfile(item) {
  519. this.currentPetInfo = item;
  520. this.showPetModal = true;
  521. },
  522. closePetProfile() {
  523. this.showPetModal = false;
  524. },
  525. openRejectModal(item) {
  526. this.currentOrder = item;
  527. this.rejectReason = '';
  528. this.showRejectModal = true;
  529. },
  530. closeRejectModal() {
  531. this.showRejectModal = false;
  532. this.currentOrder = null;
  533. },
  534. async confirmReject() {
  535. if (!this.rejectReason.trim()) {
  536. uni.showToast({ title: '请输入拒绝理由', icon: 'none' });
  537. return;
  538. }
  539. if (!this.currentOrder?.id) return
  540. try {
  541. uni.showLoading({ title: '提交中...', mask: true });
  542. await rejectOrderApi({
  543. orderId: this.currentOrder.id,
  544. rejectReason: this.rejectReason
  545. });
  546. uni.showToast({ title: '已拒绝接单', icon: 'success' });
  547. this.showRejectModal = false;
  548. this.currentOrder = null;
  549. this.loadTaskList();
  550. this.loadOrderStats();
  551. } catch (err) {
  552. console.error('拒绝接单失败:', err);
  553. uni.showToast({ title: '操作失败', icon: 'none' });
  554. } finally {
  555. uni.hideLoading();
  556. }
  557. },
  558. openAcceptModal(item) {
  559. this.currentOrder = item;
  560. this.showAcceptConfirmModal = true;
  561. },
  562. closeAcceptModal() {
  563. this.showAcceptConfirmModal = false;
  564. this.currentOrder = null;
  565. },
  566. async confirmAccept() {
  567. if (!this.currentOrder?.id) return
  568. try {
  569. await acceptOrder(this.currentOrder.id)
  570. uni.showToast({ title: '接单成功', icon: 'success' })
  571. this.showAcceptConfirmModal = false
  572. this.currentOrder = null
  573. this.loadTaskList()
  574. this.loadProfile()
  575. this.loadOrderStats()
  576. } catch (err) {
  577. console.error('接单失败:', err)
  578. uni.showToast({ title: '接单失败', icon: 'none' })
  579. }
  580. },
  581. openNavigation(item, pointType) {
  582. this.navTargetItem = item;
  583. this.navTargetPointType = pointType;
  584. this.showNavModal = true;
  585. },
  586. closeNavModal() {
  587. this.showNavModal = false;
  588. },
  589. chooseMap(mapType) {
  590. let item = this.navTargetItem;
  591. let pointType = this.navTargetPointType;
  592. // 起 -> fromAddress ; 终 -> toAddress
  593. let name = pointType === 'start' ? (item.fromAddress || '起点') : (item.toAddress || '终点');
  594. let address = pointType === 'start' ? (item.fromAddress || '起点地址') : (item.toAddress || '终点地址');
  595. let latitude = pointType === 'start' ? Number(item.fromLat) : Number(item.toLat);
  596. let longitude = pointType === 'start' ? Number(item.fromLng) : Number(item.toLng);
  597. this.showNavModal = false;
  598. // 统一定义打开地图的函数
  599. const navigateTo = (lat, lng, addrName, addrDesc) => {
  600. uni.openLocation({
  601. latitude: lat,
  602. longitude: lng,
  603. name: addrName,
  604. address: addrDesc || '无法获取详细地址',
  605. success: function () {
  606. console.log('打开导航成功: ' + mapType);
  607. },
  608. fail: function (err) {
  609. console.error('打开导航失败:', err);
  610. uni.showToast({ title: '打开地图失败', icon: 'none' });
  611. }
  612. });
  613. };
  614. // 如果有目标经纬度,直接打开
  615. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  616. navigateTo(latitude, longitude, name, address);
  617. } else {
  618. // 如果没有经纬度,按照需求:使用自己当前的经纬度,然后搜索 fromAddress 或者 toAddress
  619. uni.showLoading({ title: '获取当前位置...', mask: true });
  620. reportGps(true).then(res => {
  621. uni.hideLoading();
  622. // 使用用户当前经纬度作为锚点打开地图,展示目标地址信息
  623. navigateTo(res.latitude, res.longitude, name, address);
  624. }).catch(err => {
  625. uni.hideLoading();
  626. console.error('获取地理位置失败:', err);
  627. // 具体的授权引导已在 reportGps 内部处理
  628. });
  629. }
  630. },
  631. selectService(type) {
  632. this.tempFilter.service = type;
  633. },
  634. selectDistance(type) {
  635. this.tempFilter.distance = type;
  636. },
  637. selectAmount(type) {
  638. this.tempFilter.amount = type;
  639. },
  640. resetFilter() {
  641. this.tempFilter = {
  642. service: null,
  643. distance: '全部',
  644. amount: '全部'
  645. };
  646. },
  647. confirmFilter() {
  648. this.activeFilter = { ...this.tempFilter };
  649. this.isFilterShow = false;
  650. this.loadTaskList();
  651. },
  652. closeFilter() {
  653. this.isFilterShow = false;
  654. },
  655. goToDetail(item) {
  656. console.log('Go to detail', item);
  657. },
  658. async loadTaskList() {
  659. try {
  660. const params = {
  661. service: this.activeFilter.service,
  662. minPrice: this.getMinPrice(),
  663. maxPrice: this.getMaxPrice(),
  664. pageNum: 1,
  665. pageSize: 20
  666. }
  667. const res = await getPendingOrders(params)
  668. this.taskList = (res.rows || []).map(item => this.transformOrder(item))
  669. } catch (err) {
  670. console.error('获取订单列表失败:', err)
  671. uni.showToast({ title: '加载失败', icon: 'none' })
  672. this.taskList = []
  673. }
  674. },
  675. getMinPrice() {
  676. const amount = this.activeFilter.amount
  677. if (amount === '100以下') return 0
  678. if (amount === '100-200') return 10000
  679. if (amount === '200-500') return 20000
  680. if (amount === '500以上') return 50000
  681. return undefined
  682. },
  683. getMaxPrice() {
  684. const amount = this.activeFilter.amount
  685. if (amount === '100以下') return 10000
  686. if (amount === '100-200') return 20000
  687. if (amount === '200-500') return 50000
  688. return undefined
  689. },
  690. transformOrder(item) {
  691. const service = this.serviceList.find(s => s.id === item.service)
  692. const serviceText = service?.name || '未知'
  693. const serviceIcon = service?.iconUrl || ''
  694. const mode = service?.mode || 0
  695. const isRoundTrip = mode === 1
  696. return {
  697. id: item.id,
  698. type: isRoundTrip ? 1 : item.service,
  699. typeText: serviceText,
  700. typeIcon: serviceIcon,
  701. price: (item.fulfillmentCommission / 100).toFixed(2),
  702. timeLabel: '服务时间',
  703. time: item.serviceTime,
  704. petAvatar: item.petAvatar || '/static/dog.png',
  705. petAvatarUrl: item.petAvatarUrl || '',
  706. petName: item.petName,
  707. petBreed: item.breed,
  708. petGender: 'M',
  709. petAge: '',
  710. petWeight: '',
  711. petPersonality: '',
  712. petHobby: '',
  713. petRemark: '',
  714. petTags: [],
  715. petLogs: [],
  716. startLocation: item.fromAddress || '暂无起点',
  717. startAddress: item.fromAddress || '',
  718. fromAddress: item.fromAddress || '',
  719. fromLat: item.fromLat,
  720. fromLng: item.fromLng,
  721. startDistance: '0km',
  722. endLocation: (item.customerName || item.contact || '') + ' ' + (item.customerPhone || ''),
  723. endAddress: item.toAddress || '',
  724. toAddress: item.toAddress || '',
  725. toLat: item.toLat,
  726. toLng: item.toLng,
  727. endDistance: '0km',
  728. serviceContent: '',
  729. remark: item.remark || ''
  730. }
  731. },
  732. setFilter(type) {
  733. this.currentFilter = type;
  734. if (type === 'distance') {
  735. this.sortDistance = this.sortDistance === 'asc' ? 'desc' : 'asc';
  736. uni.showToast({ title: `按距离${this.sortDistance === 'asc' ? '升序' : '降序'}`, icon: 'none' });
  737. } else if (type === 'time') {
  738. this.sortTime = this.sortTime === 'asc' ? 'desc' : 'asc';
  739. uni.showToast({ title: `按时间${this.sortTime === 'asc' ? '升序' : '降序'}`, icon: 'none' });
  740. }
  741. },
  742. showFilterDropdown() {
  743. this.toggleFilter();
  744. }
  745. }
  746. }
  747. </script>
  748. <style>
  749. /* Global Box Sizing Fix */
  750. view,
  751. text,
  752. image,
  753. scroll-view,
  754. button {
  755. box-sizing: border-box;
  756. }
  757. /* 页面背景 */
  758. page {
  759. background-color: #F8F8F8;
  760. }
  761. .container {
  762. padding-bottom: 30rpx;
  763. }
  764. /* 顶部背景 */
  765. .nav-bg {
  766. position: absolute;
  767. top: 0;
  768. left: 0;
  769. width: 100%;
  770. height: 360rpx;
  771. /* Reduced by another 20rpx */
  772. background: linear-gradient(180deg, #FFE0B2 0%, #FFF3E0 100%);
  773. border-bottom-left-radius: 60rpx;
  774. border-bottom-right-radius: 60rpx;
  775. z-index: 1;
  776. overflow: hidden;
  777. }
  778. /* Custom Navigation Bar */
  779. .custom-nav-bar {
  780. position: fixed;
  781. top: 0;
  782. left: 0;
  783. width: 100%;
  784. z-index: 100;
  785. padding-top: var(--status-bar-height);
  786. height: 100rpx;
  787. display: flex;
  788. align-items: center;
  789. justify-content: center;
  790. }
  791. .nav-title {
  792. font-size: 34rpx;
  793. font-weight: bold;
  794. color: #333;
  795. }
  796. /* 头部区域 */
  797. .header-section {
  798. position: relative;
  799. z-index: 2;
  800. padding: 140rpx 30rpx 0;
  801. }
  802. /* 用户信息 */
  803. .user-info {
  804. display: flex;
  805. align-items: center;
  806. margin-bottom: 40rpx;
  807. }
  808. .avatar {
  809. width: 100rpx;
  810. height: 100rpx;
  811. border-radius: 50%;
  812. margin-right: 24rpx;
  813. border: 4rpx solid #fff;
  814. box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.1);
  815. }
  816. .info-content {
  817. flex: 1;
  818. display: flex;
  819. flex-direction: column;
  820. }
  821. .top-row {
  822. display: flex;
  823. align-items: center;
  824. margin-bottom: 8rpx;
  825. }
  826. .name {
  827. font-size: 36rpx;
  828. font-weight: bold;
  829. color: #333;
  830. margin-right: 16rpx;
  831. }
  832. /* 状态胶囊 */
  833. .status-pill {
  834. background-color: #4E4E4E;
  835. border-radius: 24rpx;
  836. display: flex;
  837. align-items: center;
  838. padding: 2rpx 20rpx 2rpx 6rpx;
  839. /* Increased right/left padding to elongate */
  840. }
  841. .status-dot-bg {
  842. width: 24rpx;
  843. /* Smaller icon bg */
  844. height: 24rpx;
  845. background-color: #00E676;
  846. border-radius: 50%;
  847. display: flex;
  848. align-items: center;
  849. justify-content: center;
  850. margin-right: 6rpx;
  851. }
  852. .check-mark {
  853. color: #fff;
  854. font-size: 14rpx;
  855. /* Smaller check */
  856. font-weight: bold;
  857. }
  858. .status-text {
  859. color: #fff;
  860. font-size: 22rpx;
  861. /* Smaller font (11pt) */
  862. margin-right: 6rpx;
  863. font-weight: normal;
  864. }
  865. .status-pill .arrow-down {
  866. color: #fff;
  867. font-size: 10rpx;
  868. /* Half size */
  869. }
  870. .bottom-row {
  871. display: flex;
  872. align-items: center;
  873. font-size: 24rpx;
  874. color: #333;
  875. }
  876. .city-label {
  877. margin-right: 4rpx;
  878. }
  879. .city-arrow {
  880. font-family: monospace;
  881. }
  882. /* 通知铃铛 */
  883. .notification-box {
  884. position: relative;
  885. width: 60rpx;
  886. height: 60rpx;
  887. display: flex;
  888. align-items: center;
  889. justify-content: center;
  890. }
  891. .bell-img {
  892. width: 44rpx;
  893. height: 44rpx;
  894. }
  895. .badge-count {
  896. position: absolute;
  897. top: -4rpx;
  898. right: -4rpx;
  899. background-color: #FF5252;
  900. color: #fff;
  901. font-size: 20rpx;
  902. width: 32rpx;
  903. height: 32rpx;
  904. line-height: 32rpx;
  905. text-align: center;
  906. border-radius: 50%;
  907. border: 2rpx solid #fff;
  908. }
  909. /* 统计卡片 - 悬浮样式 */
  910. .stats-card {
  911. background-color: #fff;
  912. border-radius: 30rpx;
  913. padding: 30rpx 0;
  914. display: flex;
  915. justify-content: space-around;
  916. align-items: center;
  917. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
  918. margin-bottom: 10rpx;
  919. /* 10rpx spacing to task header */
  920. margin-top: -10rpx;
  921. position: relative;
  922. width: 100%;
  923. }
  924. .stat-item {
  925. display: flex;
  926. flex-direction: column;
  927. align-items: center;
  928. flex: 1;
  929. }
  930. .num {
  931. font-size: 40rpx;
  932. /* Increased font size */
  933. font-weight: 800;
  934. color: #333;
  935. margin-bottom: 10rpx;
  936. font-family: Arial, sans-serif;
  937. }
  938. .label {
  939. font-size: 24rpx;
  940. color: #888;
  941. }
  942. .divider {
  943. width: 1px;
  944. height: 40rpx;
  945. background-color: #EEEEEE;
  946. }
  947. /* 任务大厅标题栏 - 吸顶容器 */
  948. .task-header {
  949. position: sticky;
  950. top: calc(100rpx + var(--status-bar-height));
  951. z-index: 90;
  952. margin-top: 0;
  953. margin-bottom: 10rpx;
  954. width: 100%;
  955. /* Flex removed, padding removed, handled by inner */
  956. }
  957. /* 标题栏内部内容 */
  958. .header-inner {
  959. position: relative;
  960. z-index: 3;
  961. /* Layer 3: Topmost */
  962. display: flex;
  963. justify-content: space-between;
  964. align-items: center;
  965. height: 94rpx;
  966. padding: 0 30rpx;
  967. background-color: transparent;
  968. transition: background-color 0.2s;
  969. }
  970. .left-title {
  971. display: flex;
  972. align-items: center;
  973. }
  974. .orange-bar {
  975. width: 8rpx;
  976. height: 32rpx;
  977. background-color: #FF5722;
  978. border-radius: 4rpx;
  979. margin-right: 15rpx;
  980. }
  981. .left-title .title {
  982. font-size: 28rpx;
  983. font-weight: bold;
  984. color: #333;
  985. }
  986. .left-title .count {
  987. font-size: 24rpx;
  988. color: #999;
  989. margin-left: 8rpx;
  990. font-weight: normal;
  991. }
  992. .filter-options {
  993. display: flex;
  994. align-items: center;
  995. font-size: 24rpx;
  996. color: #666;
  997. }
  998. .filter-item {
  999. margin-left: 30rpx;
  1000. transition: color 0.3s;
  1001. display: flex;
  1002. align-items: center;
  1003. }
  1004. .filter-item.active {
  1005. color: #FF5722;
  1006. font-weight: bold;
  1007. }
  1008. /* Sort Icon Design: Up and Down arrows */
  1009. .sort-icon {
  1010. display: flex;
  1011. flex-direction: column;
  1012. margin-left: 6rpx;
  1013. justify-content: center;
  1014. height: 20rpx;
  1015. }
  1016. .sort-icon .up {
  1017. width: 0;
  1018. height: 0;
  1019. border-left: 6rpx solid transparent;
  1020. border-right: 6rpx solid transparent;
  1021. border-bottom: 6rpx solid #ccc;
  1022. margin-bottom: 2rpx;
  1023. }
  1024. .sort-icon .down {
  1025. width: 0;
  1026. height: 0;
  1027. border-left: 6rpx solid transparent;
  1028. border-right: 6rpx solid transparent;
  1029. border-top: 6rpx solid #ccc;
  1030. }
  1031. .filter-item.active .sort-icon .up.active {
  1032. border-bottom-color: #FF5722;
  1033. }
  1034. .filter-item.active .sort-icon .down.active {
  1035. border-top-color: #FF5722;
  1036. }
  1037. .dropdown {
  1038. display: flex;
  1039. align-items: center;
  1040. margin-left: 30rpx;
  1041. background-color: transparent;
  1042. padding: 6rpx 20rpx;
  1043. border-radius: 30rpx;
  1044. border: none;
  1045. box-shadow: none;
  1046. }
  1047. .dropdown text {
  1048. margin-left: 0;
  1049. font-size: 24rpx;
  1050. color: #333;
  1051. }
  1052. .dropdown .arrow-down {
  1053. font-size: 18rpx;
  1054. margin-left: 6rpx;
  1055. color: #999;
  1056. }
  1057. /* 任务大厅列表 */
  1058. .task-list {
  1059. padding: 0 30rpx;
  1060. width: 100%;
  1061. }
  1062. /* 任务卡片优化 */
  1063. .task-card {
  1064. background-color: #fff;
  1065. border-radius: 24rpx;
  1066. padding: 20rpx 20rpx;
  1067. margin-bottom: 20rpx;
  1068. box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.04);
  1069. width: 100%;
  1070. }
  1071. .card-header {
  1072. display: flex;
  1073. justify-content: space-between;
  1074. align-items: center;
  1075. margin-bottom: 15rpx;
  1076. }
  1077. .type-badge {
  1078. display: flex;
  1079. align-items: center;
  1080. }
  1081. .type-icon {
  1082. width: 44rpx;
  1083. height: 44rpx;
  1084. margin-right: 15rpx;
  1085. background-color: #FFF3E0;
  1086. border-radius: 50%;
  1087. padding: 6rpx;
  1088. box-sizing: border-box;
  1089. }
  1090. .type-text {
  1091. font-size: 28rpx;
  1092. font-weight: bold;
  1093. color: #333;
  1094. }
  1095. .price {
  1096. font-size: 28rpx;
  1097. font-weight: bold;
  1098. color: #FF5252;
  1099. }
  1100. .time-row {
  1101. font-size: 26rpx;
  1102. color: #666;
  1103. margin-bottom: 20rpx;
  1104. }
  1105. .time-row .value {
  1106. color: #333;
  1107. margin-left: 10rpx;
  1108. }
  1109. /* 宠物卡片 */
  1110. .pet-card {
  1111. background-color: #FFF8F0;
  1112. border-radius: 16rpx;
  1113. padding: 15rpx 20rpx;
  1114. display: flex;
  1115. align-items: center;
  1116. margin-bottom: 30rpx;
  1117. }
  1118. .pet-avatar {
  1119. width: 80rpx;
  1120. height: 80rpx;
  1121. border-radius: 50%;
  1122. margin-right: 20rpx;
  1123. }
  1124. .pet-info {
  1125. flex: 1;
  1126. display: flex;
  1127. flex-direction: column;
  1128. }
  1129. .pet-name {
  1130. font-size: 28rpx;
  1131. font-weight: bold;
  1132. color: #333;
  1133. margin-bottom: 5rpx;
  1134. }
  1135. .pet-breed {
  1136. font-size: 24rpx;
  1137. color: #999;
  1138. }
  1139. .pet-profile-btn {
  1140. font-size: 24rpx;
  1141. color: #FF9800;
  1142. border: 1px solid #FF9800;
  1143. padding: 6rpx 20rpx;
  1144. border-radius: 50rpx;
  1145. background-color: #fff;
  1146. }
  1147. /* 路线信息 */
  1148. .route-info {
  1149. margin-bottom: 20rpx;
  1150. }
  1151. .route-item {
  1152. display: flex;
  1153. align-items: flex-start;
  1154. padding-bottom: 20rpx;
  1155. position: relative;
  1156. width: 100%;
  1157. }
  1158. /* 路线项底部的间隔 */
  1159. .route-item:not(:last-child) {
  1160. margin-bottom: 16rpx;
  1161. }
  1162. .route-item:last-child {
  1163. padding-bottom: 0;
  1164. margin-bottom: 0;
  1165. }
  1166. .route-line-vertical {
  1167. position: absolute;
  1168. left: 19rpx;
  1169. top: 46rpx;
  1170. bottom: -15rpx;
  1171. /* Adjusted to connect the now-closer nodes */
  1172. border-left: 2rpx dashed #E0E0E0;
  1173. width: 0;
  1174. z-index: 0;
  1175. }
  1176. .icon-circle {
  1177. width: 40rpx;
  1178. height: 40rpx;
  1179. border-radius: 50%;
  1180. color: #fff;
  1181. font-size: 22rpx;
  1182. display: flex;
  1183. align-items: center;
  1184. justify-content: center;
  1185. margin-right: 20rpx;
  1186. flex-shrink: 0;
  1187. font-weight: bold;
  1188. margin-top: 6rpx;
  1189. position: relative;
  1190. z-index: 1;
  1191. /* Above line */
  1192. }
  1193. .icon-circle.start {
  1194. background-color: #FFB74D;
  1195. }
  1196. .icon-circle.end {
  1197. background-color: #81C784;
  1198. }
  1199. .icon-circle.service {
  1200. background-color: #81C784;
  1201. }
  1202. .address-box {
  1203. flex: 1;
  1204. display: flex;
  1205. flex-direction: column;
  1206. padding-right: 20rpx;
  1207. width: 0;
  1208. }
  1209. .addr-title-row {
  1210. display: flex;
  1211. align-items: center;
  1212. justify-content: space-between;
  1213. }
  1214. .addr-title {
  1215. font-size: 28rpx;
  1216. font-weight: bold;
  1217. color: #333;
  1218. margin-bottom: 6rpx;
  1219. flex: 1;
  1220. }
  1221. .phone-call-btn {
  1222. width: 48rpx;
  1223. height: 48rpx;
  1224. display: flex;
  1225. align-items: center;
  1226. justify-content: center;
  1227. background-color: #E8F5E9;
  1228. border-radius: 50%;
  1229. margin-left: 10rpx;
  1230. transition: transform 0.1s;
  1231. }
  1232. .phone-call-btn:active {
  1233. transform: scale(0.9);
  1234. background-color: #C8E6C9;
  1235. }
  1236. .phone-icon-item {
  1237. width: 28rpx;
  1238. height: 28rpx;
  1239. }
  1240. .addr-desc {
  1241. font-size: 24rpx;
  1242. color: #999;
  1243. line-height: normal;
  1244. }
  1245. .distance-tag {
  1246. display: flex;
  1247. align-items: center;
  1248. background-color: #FFF3E0;
  1249. padding: 6rpx 6rpx 6rpx 12rpx;
  1250. border-radius: 30rpx;
  1251. flex-shrink: 0;
  1252. }
  1253. .distance-tag text {
  1254. font-size: 24rpx;
  1255. color: #FF5722;
  1256. margin-right: 5rpx;
  1257. font-weight: normal;
  1258. }
  1259. .nav-arrow {
  1260. width: 32rpx;
  1261. height: 32rpx;
  1262. }
  1263. .service-content {
  1264. margin-top: -10rpx;
  1265. /* Shifted up using negative margin for max compactness */
  1266. font-size: 24rpx;
  1267. /* 12pt */
  1268. color: #666;
  1269. padding-left: 60rpx;
  1270. }
  1271. .content-label {
  1272. color: #999;
  1273. margin-right: 10rpx;
  1274. }
  1275. /* 备注 */
  1276. .remark-box {
  1277. background-color: #F8F8F8;
  1278. padding: 15rpx 20rpx;
  1279. border-radius: 8rpx;
  1280. font-size: 24rpx;
  1281. color: #666;
  1282. margin-bottom: 20rpx;
  1283. }
  1284. /* 按钮组 */
  1285. .action-btns {
  1286. display: flex;
  1287. justify-content: space-between;
  1288. }
  1289. .btn {
  1290. height: 64rpx;
  1291. /* Increased height */
  1292. line-height: 64rpx;
  1293. border-radius: 32rpx;
  1294. /* Matches height/2 */
  1295. font-size: 28rpx;
  1296. /* Slightly larger */
  1297. font-weight: normal;
  1298. width: 48%;
  1299. }
  1300. .btn::after {
  1301. border: none;
  1302. }
  1303. .btn.reject {
  1304. background-color: #F5F5F5;
  1305. color: #999;
  1306. box-shadow: none;
  1307. }
  1308. .btn.accept {
  1309. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1310. color: #fff;
  1311. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1312. }
  1313. .bg-circle-right {
  1314. position: absolute;
  1315. top: -20rpx;
  1316. right: -20rpx;
  1317. width: 185rpx;
  1318. /* +20rpx */
  1319. height: 185rpx;
  1320. /* +20rpx */
  1321. border-radius: 50%;
  1322. background-color: rgba(255, 218, 185, 0.8);
  1323. /* 80% opacity */
  1324. }
  1325. /* 筛选面板 (Absolute Child) */
  1326. .filter-panel {
  1327. position: absolute;
  1328. /* Relative to .task-header */
  1329. top: 94rpx;
  1330. /* Start right below header */
  1331. left: 0;
  1332. width: 100%;
  1333. background-color: #fff;
  1334. z-index: 2;
  1335. /* Layer 2: Below inner(3), Above mask(1) */
  1336. padding: 30rpx 30rpx 40rpx;
  1337. border-bottom-left-radius: 30rpx;
  1338. border-bottom-right-radius: 30rpx;
  1339. box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
  1340. transform: translateY(-20rpx);
  1341. /* Slightly tucked start */
  1342. opacity: 0;
  1343. transition: all 0.25s ease-out;
  1344. visibility: hidden;
  1345. }
  1346. .filter-panel.show {
  1347. transform: translateY(0);
  1348. opacity: 1;
  1349. visibility: visible;
  1350. }
  1351. /* 筛选遮罩 (Absolute Child) */
  1352. .filter-mask {
  1353. position: absolute;
  1354. top: 94rpx;
  1355. /* Start below header */
  1356. left: 0;
  1357. right: 0;
  1358. height: 100vh;
  1359. /* Cover visible area below */
  1360. background-color: rgba(0, 0, 0, 0.5);
  1361. z-index: 1;
  1362. /* Layer 1: Lowest in header */
  1363. }
  1364. .filter-section {
  1365. margin-bottom: 40rpx;
  1366. }
  1367. .section-title {
  1368. font-size: 28rpx;
  1369. font-weight: bold;
  1370. color: #333;
  1371. margin-bottom: 20rpx;
  1372. display: block;
  1373. }
  1374. .options-grid {
  1375. display: flex;
  1376. flex-wrap: wrap;
  1377. gap: 20rpx;
  1378. }
  1379. .option-btn {
  1380. width: calc(33.33% - 14rpx);
  1381. /* 3 cols with gap */
  1382. height: 64rpx;
  1383. line-height: 64rpx;
  1384. text-align: center;
  1385. border-radius: 32rpx;
  1386. font-size: 26rpx;
  1387. color: #666;
  1388. background-color: #fff;
  1389. border: 2rpx solid #E0E0E0;
  1390. margin-bottom: 10rpx;
  1391. }
  1392. .option-btn.active {
  1393. color: #FF5722;
  1394. background-color: #FFF3E0;
  1395. border-color: #FF5722;
  1396. font-weight: bold;
  1397. }
  1398. .filter-actions {
  1399. display: flex;
  1400. justify-content: space-between;
  1401. margin-top: 50rpx;
  1402. }
  1403. .action-btn {
  1404. width: 48%;
  1405. height: 64rpx;
  1406. /* Match .btn */
  1407. line-height: 64rpx;
  1408. border-radius: 32rpx;
  1409. /* Match .btn */
  1410. font-size: 28rpx;
  1411. /* Match .btn */
  1412. font-weight: normal;
  1413. /* Match .btn */
  1414. }
  1415. .action-btn::after {
  1416. border: none;
  1417. }
  1418. .action-btn.reset {
  1419. background-color: #F5F5F5;
  1420. color: #999;
  1421. /* Match light gray */
  1422. }
  1423. .action-btn.confirm {
  1424. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1425. color: #fff;
  1426. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1427. }
  1428. /* 状态胶囊 */
  1429. .status-pill {
  1430. display: flex;
  1431. align-items: center;
  1432. background-color: #333333;
  1433. /* Dark background */
  1434. border-radius: 20rpx;
  1435. padding: 4rpx 16rpx 4rpx 8rpx;
  1436. margin-left: 16rpx;
  1437. transition: background-color 0.3s;
  1438. }
  1439. .status-pill.resting {
  1440. background-color: #424242;
  1441. /* Slightly lighter dark */
  1442. }
  1443. .status-dot-bg {
  1444. width: 24rpx;
  1445. height: 24rpx;
  1446. background-color: #4CAF50;
  1447. border-radius: 50%;
  1448. margin-right: 6rpx;
  1449. display: flex;
  1450. justify-content: center;
  1451. align-items: center;
  1452. }
  1453. .check-mark {
  1454. color: #fff;
  1455. font-size: 14rpx;
  1456. font-weight: bold;
  1457. }
  1458. .status-pill.resting .status-dot-bg {
  1459. background-color: #FF5722;
  1460. /* Warning color for resting */
  1461. }
  1462. .status-icon {
  1463. width: 24rpx;
  1464. height: 24rpx;
  1465. margin-right: 6rpx;
  1466. }
  1467. .status-text {
  1468. font-size: 22rpx;
  1469. color: #fff;
  1470. margin-right: 6rpx;
  1471. }
  1472. .status-pill .arrow-down {
  1473. color: #fff;
  1474. font-size: 10rpx;
  1475. /* Reduced size */
  1476. margin-left: 4rpx;
  1477. /* Adjust spacing */
  1478. }
  1479. /* 空状态 - 休息中 */
  1480. .empty-state {
  1481. display: flex;
  1482. flex-direction: column;
  1483. align-items: center;
  1484. justify-content: center;
  1485. padding-top: 150rpx;
  1486. }
  1487. .empty-icon {
  1488. width: 200rpx;
  1489. height: 200rpx;
  1490. margin-bottom: 40rpx;
  1491. opacity: 0.5;
  1492. }
  1493. .empty-text {
  1494. font-size: 28rpx;
  1495. color: #666;
  1496. margin-bottom: 60rpx;
  1497. }
  1498. .start-work-btn {
  1499. width: 300rpx;
  1500. height: 80rpx;
  1501. line-height: 80rpx;
  1502. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1503. border-radius: 40rpx;
  1504. color: #fff;
  1505. font-size: 30rpx;
  1506. font-weight: bold;
  1507. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1508. }
  1509. /* 自定义确认弹窗 */
  1510. .modal-mask {
  1511. position: fixed;
  1512. top: 0;
  1513. left: 0;
  1514. right: 0;
  1515. bottom: 0;
  1516. background-color: rgba(0, 0, 0, 0.5);
  1517. z-index: 999;
  1518. display: flex;
  1519. align-items: center;
  1520. justify-content: center;
  1521. }
  1522. .custom-modal {
  1523. width: 600rpx;
  1524. background-color: #fff;
  1525. border-radius: 24rpx;
  1526. padding: 40rpx 50rpx;
  1527. display: flex;
  1528. flex-direction: column;
  1529. align-items: center;
  1530. }
  1531. .modal-title {
  1532. font-size: 36rpx;
  1533. font-weight: bold;
  1534. color: #333;
  1535. margin-bottom: 30rpx;
  1536. }
  1537. .modal-content {
  1538. font-size: 30rpx;
  1539. color: #666;
  1540. margin-bottom: 50rpx;
  1541. text-align: center;
  1542. }
  1543. .modal-btns {
  1544. width: 100%;
  1545. display: flex;
  1546. justify-content: space-between;
  1547. }
  1548. .modal-btn {
  1549. width: 45%;
  1550. height: 80rpx;
  1551. line-height: 80rpx;
  1552. border-radius: 40rpx;
  1553. font-size: 30rpx;
  1554. font-weight: bold;
  1555. margin: 0;
  1556. }
  1557. .modal-btn::after {
  1558. border: none;
  1559. }
  1560. .modal-btn.cancel {
  1561. background-color: #F5F5F5;
  1562. color: #999;
  1563. }
  1564. .modal-btn.confirm {
  1565. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1566. color: #fff;
  1567. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1568. }
  1569. /* 宠物档案弹窗 */
  1570. .pet-modal-mask {
  1571. position: fixed;
  1572. top: 0;
  1573. left: 0;
  1574. right: 0;
  1575. bottom: 0;
  1576. background-color: rgba(0, 0, 0, 0.4);
  1577. z-index: 1000;
  1578. display: flex;
  1579. align-items: center;
  1580. justify-content: center;
  1581. }
  1582. .pet-modal-content {
  1583. width: 680rpx;
  1584. height: 85vh;
  1585. background-color: #fff;
  1586. border-radius: 20rpx;
  1587. display: flex;
  1588. flex-direction: column;
  1589. overflow: hidden;
  1590. }
  1591. .pet-modal-header {
  1592. display: flex;
  1593. align-items: center;
  1594. justify-content: space-between;
  1595. padding: 30rpx;
  1596. border-bottom: 1rpx solid #F0F0F0;
  1597. }
  1598. .pet-modal-title {
  1599. font-size: 34rpx;
  1600. font-weight: bold;
  1601. color: #333;
  1602. }
  1603. .pet-modal-scroll {
  1604. flex: 1;
  1605. height: 0;
  1606. padding: 30rpx;
  1607. box-sizing: border-box;
  1608. }
  1609. .pet-base-info {
  1610. display: flex;
  1611. align-items: center;
  1612. margin-bottom: 40rpx;
  1613. }
  1614. .pm-avatar {
  1615. width: 120rpx;
  1616. height: 120rpx;
  1617. border-radius: 50%;
  1618. margin-right: 30rpx;
  1619. border: 2rpx solid #f5f5f5;
  1620. }
  1621. .pm-info-text {
  1622. flex: 1;
  1623. display: flex;
  1624. flex-direction: column;
  1625. }
  1626. .pm-name-row {
  1627. display: flex;
  1628. align-items: center;
  1629. margin-bottom: 15rpx;
  1630. }
  1631. .pm-name {
  1632. font-size: 36rpx;
  1633. font-weight: bold;
  1634. color: #333;
  1635. margin-right: 20rpx;
  1636. }
  1637. .pm-gender {
  1638. display: flex;
  1639. align-items: center;
  1640. background-color: #E3F2FD;
  1641. padding: 4rpx 12rpx;
  1642. border-radius: 20rpx;
  1643. }
  1644. .pm-gender text {
  1645. font-size: 22rpx;
  1646. color: #1E88E5;
  1647. }
  1648. .pm-gender .gender-icon {
  1649. font-weight: bold;
  1650. margin-right: 4rpx;
  1651. }
  1652. .pm-gender.female {
  1653. background-color: #FCE4EC;
  1654. }
  1655. .pm-gender.female text {
  1656. color: #D81B60;
  1657. }
  1658. .pm-breed {
  1659. font-size: 26rpx;
  1660. color: #999;
  1661. }
  1662. .pm-detail-grid {
  1663. display: flex;
  1664. flex-wrap: wrap;
  1665. justify-content: space-between;
  1666. }
  1667. .pm-grid-item {
  1668. background-color: #F8F8F8;
  1669. border-radius: 16rpx;
  1670. padding: 24rpx;
  1671. margin-bottom: 20rpx;
  1672. display: flex;
  1673. flex-direction: column;
  1674. }
  1675. .pm-grid-item.half {
  1676. width: 48%;
  1677. box-sizing: border-box;
  1678. }
  1679. .pm-grid-item.full {
  1680. width: 100%;
  1681. box-sizing: border-box;
  1682. }
  1683. .pm-label {
  1684. font-size: 24rpx;
  1685. color: #999;
  1686. margin-bottom: 10rpx;
  1687. }
  1688. .pm-val {
  1689. font-size: 28rpx;
  1690. color: #333;
  1691. font-weight: 500;
  1692. }
  1693. .pm-tags {
  1694. display: flex;
  1695. flex-wrap: wrap;
  1696. gap: 20rpx;
  1697. margin-bottom: 40rpx;
  1698. }
  1699. .pm-tag {
  1700. background-color: #FFF8EB;
  1701. border: 2rpx solid #FFCC80;
  1702. color: #FF9800;
  1703. font-size: 22rpx;
  1704. padding: 8rpx 24rpx;
  1705. border-radius: 30rpx;
  1706. }
  1707. .pm-section-title {
  1708. display: flex;
  1709. align-items: center;
  1710. margin-bottom: 30rpx;
  1711. padding-top: 30rpx;
  1712. border-top: 2rpx dashed #F0F0F0;
  1713. }
  1714. .pm-section-title .orange-bar {
  1715. width: 8rpx;
  1716. height: 32rpx;
  1717. background-color: #FF9800;
  1718. margin-right: 16rpx;
  1719. border-radius: 4rpx;
  1720. }
  1721. .pm-section-title text {
  1722. font-size: 30rpx;
  1723. font-weight: bold;
  1724. color: #333;
  1725. }
  1726. .pm-log-list {
  1727. display: flex;
  1728. flex-direction: column;
  1729. }
  1730. .pm-log-item {
  1731. display: flex;
  1732. flex-direction: column;
  1733. padding: 24rpx 0;
  1734. border-bottom: 1rpx solid #F0F0F0;
  1735. }
  1736. .pm-log-item:last-child {
  1737. border-bottom: none;
  1738. }
  1739. .pm-log-date {
  1740. font-size: 24rpx;
  1741. color: #999;
  1742. margin-bottom: 16rpx;
  1743. }
  1744. .pm-log-text {
  1745. font-size: 28rpx;
  1746. color: #333;
  1747. line-height: 1.6;
  1748. margin-bottom: 20rpx;
  1749. }
  1750. .pm-log-recorder {
  1751. font-size: 24rpx;
  1752. color: #FF9800;
  1753. align-self: flex-end;
  1754. }
  1755. /* 拒绝接单弹窗输入框 */
  1756. .reject-textarea {
  1757. width: 100%;
  1758. height: 200rpx;
  1759. background-color: #F8F8F8;
  1760. border-radius: 12rpx;
  1761. padding: 24rpx;
  1762. font-size: 28rpx;
  1763. line-height: 1.5;
  1764. box-sizing: border-box;
  1765. margin-bottom: 20rpx;
  1766. }
  1767. .mt-30 {
  1768. margin-top: 30rpx;
  1769. }
  1770. /* 宠物档案底部关闭按钮 */
  1771. .pm-bottom-close {
  1772. width: 100%;
  1773. height: 80rpx;
  1774. line-height: 80rpx;
  1775. background-color: #F5F5F5;
  1776. color: #666;
  1777. border-radius: 40rpx;
  1778. font-size: 30rpx;
  1779. font-weight: bold;
  1780. margin: 0;
  1781. }
  1782. .pm-bottom-close::after {
  1783. border: none;
  1784. }
  1785. /* 宠物档案原生关闭图标 */
  1786. .close-icon-btn {
  1787. font-size: 48rpx;
  1788. color: #999;
  1789. line-height: 1;
  1790. padding: 0 10rpx;
  1791. }
  1792. /* 确认接单弹窗内容 */
  1793. .modal-content-box {
  1794. display: flex;
  1795. flex-direction: column;
  1796. align-items: center;
  1797. margin-bottom: 20rpx;
  1798. }
  1799. .modal-content-main {
  1800. font-size: 30rpx;
  1801. color: #333;
  1802. margin-bottom: 16rpx;
  1803. }
  1804. .modal-content-sub {
  1805. font-size: 24rpx;
  1806. color: #999;
  1807. }
  1808. /* 地图导航 Action Sheet */
  1809. .nav-modal-mask {
  1810. position: fixed;
  1811. top: 0;
  1812. left: 0;
  1813. right: 0;
  1814. bottom: 0;
  1815. background-color: rgba(0, 0, 0, 0.5);
  1816. z-index: 1000;
  1817. display: flex;
  1818. flex-direction: column;
  1819. justify-content: flex-end;
  1820. }
  1821. .nav-action-sheet {
  1822. background-color: #fff;
  1823. width: 100%;
  1824. border-top-left-radius: 24rpx;
  1825. border-top-right-radius: 24rpx;
  1826. overflow: hidden;
  1827. padding-bottom: constant(safe-area-inset-bottom);
  1828. padding-bottom: env(safe-area-inset-bottom);
  1829. }
  1830. .nav-sheet-title {
  1831. text-align: center;
  1832. padding: 30rpx 0;
  1833. font-size: 13px;
  1834. color: #999;
  1835. border-bottom: 1rpx solid #efefef;
  1836. }
  1837. .nav-sheet-item {
  1838. text-align: center;
  1839. padding: 30rpx 0;
  1840. font-size: 13px;
  1841. color: #333;
  1842. background-color: #fff;
  1843. border-bottom: 1rpx solid #efefef;
  1844. }
  1845. .nav-sheet-item.cancel {
  1846. border-bottom: none;
  1847. color: #666;
  1848. }
  1849. .nav-sheet-gap {
  1850. height: 16rpx;
  1851. background-color: #F8F8F8;
  1852. }
  1853. </style>