index.vue 60 KB

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