index.vue 58 KB

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