index.vue 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009
  1. <template>
  2. <view class="container">
  3. <!-- 吸顶固定层:状态tab + 搜索 + 筛选 -->
  4. <view class="sticky-header">
  5. <!-- 顶部 Tab (待接送/服务中...) -->
  6. <view class="status-tabs">
  7. <view class="tab-item" v-for="(tab, index) in tabs" :key="index"
  8. :class="{ active: currentTab === index }" @click="currentTab = index">
  9. <text>{{ tab }}</text>
  10. <view class="indicator" v-if="currentTab === index"></view>
  11. </view>
  12. </view>
  13. <!-- 搜索栏 -->
  14. <view class="search-bar">
  15. <view class="search-input-box">
  16. <input class="search-input" v-model="searchContent" placeholder="搜索地址/电话/姓名"
  17. placeholder-class="ph-style" />
  18. </view>
  19. </view>
  20. <!-- 筛选栏 (支持自定义下拉) -->
  21. <view class="filter-wrapper">
  22. <view class="filter-bar">
  23. <!-- 订单类型下拉视图 -->
  24. <view class="filter-item" :class="{ 'active': activeDropdown === 1 }" @click="toggleDropdown(1)">
  25. <text :class="{ 'active-text': activeDropdown === 1 || currentTypeFilterIdx > 0 }">
  26. {{ currentTypeFilterIdx > 0 ? typeFilterOptions[currentTypeFilterIdx] : '全部类型' }}
  27. </text>
  28. <view class="triangle" :class="activeDropdown === 1 ? 'up' : 'down'"></view>
  29. </view>
  30. <!-- 服务时间下拉视图 -->
  31. <view class="filter-item" :class="{ 'active': activeDropdown === 2 }" @click="toggleDropdown(2)">
  32. <text :class="{ 'active-text': activeDropdown === 2 || hasTimeFilter }">服务时间</text>
  33. <view class="triangle" :class="activeDropdown === 2 ? 'up' : 'down'"></view>
  34. </view>
  35. </view>
  36. <!-- 下拉内容面板与遮罩 -->
  37. <view class="dropdown-mask" v-if="activeDropdown !== 0" @click="closeDropdown"></view>
  38. <view class="dropdown-panel" v-if="activeDropdown === 1">
  39. <view class="type-option" v-for="(item, index) in typeFilterOptions" :key="index"
  40. :class="{ 'selected': currentTypeFilterIdx === index }" @click="selectType(index)">
  41. <text>{{ item }}</text>
  42. </view>
  43. </view>
  44. <view class="dropdown-panel calendar-panel" v-if="activeDropdown === 2">
  45. <view class="custom-calendar-container">
  46. <!-- 头部 -->
  47. <view class="cal-header">
  48. <text class="cal-nav-btn" @click="prevMonth">‹</text>
  49. <text class="cal-title">{{ currentMonth }}</text>
  50. <text class="cal-nav-btn" @click="nextMonth">›</text>
  51. </view>
  52. <!-- 星期条 -->
  53. <view class="cal-weekdays">
  54. <text v-for="(wk, idx) in weekDays" :key="idx" class="wk-item">{{ wk }}</text>
  55. </view>
  56. <!-- 日期网格 -->
  57. <view class="cal-body">
  58. <view v-for="(day, idx) in calendarDays" :key="idx" class="cal-day-box"
  59. :class="day ? getDateClass(day) : ''" @click="day && selectDateItem(day)">
  60. <view class="cal-day-text" v-if="day">{{ day }}</view>
  61. </view>
  62. </view>
  63. </view>
  64. <view class="calendar-actions">
  65. <button class="cal-btn reset" @click="resetTimeFilter">重置</button>
  66. <button class="cal-btn confirm" @click="confirmTimeFilter">确定</button>
  67. </view>
  68. </view>
  69. </view><!-- end filter-wrapper -->
  70. </view><!-- end sticky-header -->
  71. <!-- 订单列表 -->
  72. <view class="order-list">
  73. <view class="order-card" v-for="(item, index) in orderList" :key="index" :class="{ 'disabled-card': !item.serviceFlag }">
  74. <view class="card-header">
  75. <view class="type-badge">
  76. <image class="type-icon" :src="item.typeIcon"></image>
  77. <text class="type-text">{{ item.typeText }}</text>
  78. </view>
  79. <text class="status-badge" :class="getStatusClass(item)">{{ getDisplayStatus(item) }}</text>
  80. </view>
  81. <view class="card-body">
  82. <view class="time-row">
  83. <view class="time-col">
  84. <text class="label">{{ item.timeLabel }}:</text>
  85. <text class="value">{{ item.time }}</text>
  86. </view>
  87. <text class="fulfillmentCommission">¥{{ item.fulfillmentCommission }}</text>
  88. </view>
  89. <!-- 宠物信息 -->
  90. <view class="pet-card">
  91. <image class="pet-avatar" :src="item.petAvatarUrl || item.petAvatar" mode="aspectFill"></image>
  92. <view class="pet-info">
  93. <text class="pet-name">{{ item.petName }}</text>
  94. <text class="pet-breed">品种: {{ item.petBreed }}</text>
  95. </view>
  96. </view>
  97. <!-- 路线信息 (完全复用 Home 样式) -->
  98. <view class="route-info">
  99. <template v-if="item.type === 1">
  100. <view class="route-item" @click.stop="openNavigation(item, 'start')">
  101. <view class="icon-circle start">起</view>
  102. <view class="route-line-vertical"></view>
  103. <view class="address-box">
  104. <text class="addr-title">{{ item.startLocation }}</text>
  105. <text class="addr-desc">{{ item.startAddress }}</text>
  106. </view>
  107. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  108. style="flex-shrink: 0; align-self: center;"></image>
  109. </view>
  110. <view class="route-item" @click.stop="openNavigation(item, 'end')">
  111. <view class="icon-circle end">终</view>
  112. <view class="address-box">
  113. <text class="addr-title">{{ item.endLocation }}</text>
  114. <text class="addr-desc">{{ item.endAddress }}</text>
  115. </view>
  116. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  117. style="flex-shrink: 0; align-self: center;"></image>
  118. </view>
  119. </template>
  120. <template v-else>
  121. <view class="route-item" @click.stop="openNavigation(item, 'end')">
  122. <view class="icon-circle service">服</view>
  123. <view class="address-box">
  124. <text class="addr-title">{{ item.endLocation }}</text>
  125. <text class="addr-desc">{{ item.endAddress }}</text>
  126. </view>
  127. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"
  128. style="flex-shrink: 0; align-self: center;"></image>
  129. </view>
  130. <view class="service-content" v-if="item.serviceContent">
  131. <text class="content-label">服务内容:</text>
  132. <text>{{ item.serviceContent }}</text>
  133. </view>
  134. </template>
  135. </view>
  136. <!-- 备注 -->
  137. <view class="remark-box">
  138. <text>备注:{{ item.remark || '-' }}</text>
  139. </view>
  140. </view><!-- End of card-body -->
  141. <!-- 按钮组 (重新排版) -->
  142. <view class="action-btns" v-if="['接单', '到达', '出发', '开始', '送达', '结束'].includes(item.statusText)">
  143. <view class="action-row">
  144. <button class="btn normal danger" v-if="item.status === 2"
  145. @click.stop="handleCancelOrder(item)">取消订单</button>
  146. <button class="btn normal" @click.stop="reportAbnormal(item)">异常上报</button>
  147. <button class="btn normal" @click.stop="addOrUpdateService(item)">增改服务项</button>
  148. </view>
  149. <view class="action-row">
  150. <button class="btn normal" @click.stop="doCall('customer', item)">拨号</button>
  151. <button class="btn primary" @click.stop="mainAction(item)">到达打卡</button>
  152. </view>
  153. </view>
  154. </view>
  155. <!-- 已加载完提示文字 -->
  156. <view class="loading-text">已加载完</view>
  157. <view style="height: 160rpx;"></view>
  158. </view>
  159. <view class="call-mask" v-if="activeCallItem" @click="closeCallMenu"></view>
  160. </view>
  161. <!-- 宠物档案弹窗 (复用Home) -->
  162. <view class="pet-modal-mask" v-if="showPetModal" @click="closePetProfile">
  163. <view class="pet-modal-content" @click.stop>
  164. <view class="pet-modal-header">
  165. <text class="pet-modal-title">宠物档案</text>
  166. <view class="pm-header-actions">
  167. <view class="pm-remark-btn" @click="openRemarkInput">备注</view>
  168. <view class="close-icon-btn" @click="closePetProfile">×</view>
  169. </view>
  170. </view>
  171. <scroll-view scroll-y class="pet-modal-scroll">
  172. <!-- Basic Info -->
  173. <view class="pet-base-info">
  174. <image class="pm-avatar" :src="currentPetInfo.petAvatar" mode="aspectFill"></image>
  175. <view class="pm-info-text">
  176. <view class="pm-name-row">
  177. <text class="pm-name">{{ currentPetInfo.petName }}</text>
  178. <view class="pm-gender" v-if="currentPetInfo.petGender === 'M'">
  179. <text class="gender-icon">♂</text>
  180. <text>公</text>
  181. </view>
  182. <view class="pm-gender female" v-else-if="currentPetInfo.petGender === 'F'">
  183. <text class="gender-icon">♀</text>
  184. <text>母</text>
  185. </view>
  186. </view>
  187. <text class="pm-breed">品种:{{ currentPetInfo.petBreed }}</text>
  188. </view>
  189. </view>
  190. <!-- Details Grid -->
  191. <view class="pm-detail-grid">
  192. <view class="pm-grid-item half">
  193. <text class="pm-label">年龄</text>
  194. <text class="pm-val">{{ currentPetInfo.petAge || '未知' }}</text>
  195. </view>
  196. <view class="pm-grid-item half">
  197. <text class="pm-label">体重</text>
  198. <text class="pm-val">{{ currentPetInfo.petWeight || '未知' }}</text>
  199. </view>
  200. <view class="pm-grid-item full">
  201. <text class="pm-label">性格</text>
  202. <text class="pm-val">{{ currentPetInfo.petPersonality || '无' }}</text>
  203. </view>
  204. <view class="pm-grid-item full">
  205. <text class="pm-label">爱好</text>
  206. <text class="pm-val">{{ currentPetInfo.petHobby || '无' }}</text>
  207. </view>
  208. <view class="pm-grid-item full">
  209. <text class="pm-label">备注</text>
  210. <text class="pm-val">{{ currentPetInfo.petRemark || '无特殊过敏史' }}</text>
  211. </view>
  212. </view>
  213. <!-- Tags -->
  214. <view class="pm-tags" v-if="currentPetInfo.petTags && currentPetInfo.petTags.length > 0">
  215. <view class="pm-tag" v-for="(tag, index) in currentPetInfo.petTags" :key="index">{{ tag }}</view>
  216. </view>
  217. <!-- Log Section -->
  218. <view class="pm-section-title">
  219. <view class="orange-bar"></view>
  220. <text>备注日志</text>
  221. </view>
  222. <view class="pm-log-list">
  223. <view class="pm-log-item" v-for="(log, lIndex) in currentPetInfo.petLogs" :key="lIndex">
  224. <text class="pm-log-date">{{ log.date }}</text>
  225. <text class="pm-log-text">{{ log.content }}</text>
  226. <text class="pm-log-recorder">{{ log.recorder === '系统记录' ? '' : '记录人: ' }}{{ log.recorder
  227. }}</text>
  228. </view>
  229. </view>
  230. <view style="height: 30rpx;"></view>
  231. </scroll-view>
  232. </view>
  233. </view>
  234. <!-- 备注输入弹窗 -->
  235. <view class="remark-mask" v-if="showRemarkInput" @click="closeRemarkInput">
  236. <view class="remark-sheet" @click.stop>
  237. <view class="remark-sheet-header">
  238. <text class="remark-sheet-title">添加备注</text>
  239. <view class="close-icon-btn" @click="closeRemarkInput">×</view>
  240. </view>
  241. <textarea class="remark-textarea" v-model="remarkText" placeholder="请输入备注内容..." maxlength="500"
  242. auto-height />
  243. <view class="remark-submit-btn" @click="submitRemark">提交备注</view>
  244. </view>
  245. </view>
  246. <!-- 选择地图导航弹窗 (复用Home) -->
  247. <view class="nav-modal-mask" v-if="showNavModal" @click="closeNavModal">
  248. <view class="nav-action-sheet" @click.stop>
  249. <view class="nav-sheet-title">选择地图进行导航</view>
  250. <view class="nav-sheet-item" @click="chooseMap('高德')">高德地图</view>
  251. <view class="nav-sheet-item" @click="chooseMap('腾讯')">腾讯地图</view>
  252. <view class="nav-sheet-item" @click="chooseMap('百度')">百度地图</view>
  253. <view class="nav-sheet-gap"></view>
  254. <view class="nav-sheet-item cancel" @click="closeNavModal">取消</view>
  255. </view>
  256. </view>
  257. <!-- 取消订单确认弹窗 -->
  258. <view class="modal-mask" v-if="showCancelModal">
  259. <view class="custom-modal">
  260. <text class="modal-title">取消订单</text>
  261. <view class="textarea-container">
  262. <textarea class="reject-textarea" v-model="cancelReason" placeholder="请输入取消原因(必填)"
  263. maxlength="100"></textarea>
  264. <text class="char-count">{{ cancelReason.length }}/100</text>
  265. </view>
  266. <view class="modal-btns mt-30">
  267. <button class="modal-btn cancel" @click="closeCancelModal">再想想</button>
  268. <button class="modal-btn confirm" :class="{ 'disabled': !cancelReason.trim() }"
  269. @click="confirmCancel">确认取消</button>
  270. </view>
  271. </view>
  272. </view>
  273. <custom-tabbar currentPath="pages/orders/index"></custom-tabbar>
  274. </template>
  275. <script>
  276. import { getMyOrders, cancelOrderApi } from '@/api/order/subOrder'
  277. import { listAllService } from '@/api/service/list'
  278. import { reportGps } from '@/utils/gps'
  279. import customTabbar from '@/components/custom-tabbar/index.vue'
  280. export default {
  281. components: {
  282. customTabbar
  283. },
  284. data() {
  285. return {
  286. currentTab: 0,
  287. tabs: ['待接送/服务', '配送/服务中', '已完成', '已取消'],
  288. typeFilterOptions: ['全部类型'],
  289. currentTypeFilterIdx: 0,
  290. activeDropdown: 0,
  291. hasTimeFilter: false,
  292. currentMonth: '',
  293. viewDate: new Date(),
  294. weekDays: ['日', '一', '二', '三', '四', '五', '六'],
  295. calendarDays: [],
  296. selectedDateRange: [],
  297. allOrderList: [],
  298. serviceList: [],
  299. searchContent: '',
  300. startServiceTime: '',
  301. endServiceTime: '',
  302. showPetModal: false,
  303. currentPetInfo: {},
  304. showNavModal: false,
  305. navTargetItem: null,
  306. navTargetPointType: '',
  307. activeCallItem: null,
  308. showRemarkInput: false,
  309. remarkText: '',
  310. showCancelModal: false,
  311. cancelReason: '',
  312. currentOrder: null
  313. }
  314. },
  315. created() {
  316. this.initCalendar();
  317. },
  318. async onLoad() {
  319. await this.loadServiceList()
  320. await this.loadOrders()
  321. // 显式请求一次定位授权
  322. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  323. },
  324. onShow() {
  325. uni.hideTabBar()
  326. // 此处不需要重复调用,因为逻辑可能在onLoad已处理,
  327. // 或者如果需要每次进入都刷新,可以保留,但需确保顺序
  328. this.loadOrders()
  329. },
  330. async onPullDownRefresh() {
  331. try {
  332. await this.loadServiceList()
  333. await this.loadOrders()
  334. } finally {
  335. uni.stopPullDownRefresh()
  336. }
  337. },
  338. watch: {
  339. currentTab() {
  340. this.loadOrders()
  341. },
  342. currentTypeFilterIdx() {
  343. this.loadOrders()
  344. },
  345. searchContent() {
  346. // 搜索内容变化时,自动重新加载订单
  347. this.loadOrders()
  348. }
  349. },
  350. computed: {
  351. orderList() {
  352. return this.allOrderList;
  353. }
  354. },
  355. methods: {
  356. async loadServiceList() {
  357. try {
  358. const res = await listAllService()
  359. this.serviceList = res.data || []
  360. this.typeFilterOptions = ['全部类型', ...this.serviceList.map(s => s.name)]
  361. } catch (err) {
  362. console.error('获取服务类型失败:', err)
  363. uni.showToast({ title: err.message || err.msg || '获取服务失败', icon: 'none' })
  364. }
  365. },
  366. async loadOrders() {
  367. try {
  368. const statusMap = { 0: 2, 1: 3, 2: 4, 3: 5 }
  369. const serviceId = this.currentTypeFilterIdx > 0 ? this.serviceList[this.currentTypeFilterIdx - 1]?.id : undefined
  370. const params = {
  371. status: statusMap[this.currentTab],
  372. content: this.searchContent || undefined,
  373. service: serviceId,
  374. startServiceTime: this.startServiceTime || undefined,
  375. endServiceTime: this.endServiceTime || undefined
  376. }
  377. console.log('订单列表请求参数:', params)
  378. const res = await getMyOrders(params)
  379. console.log('订单列表响应:', res)
  380. const orders = res.rows || []
  381. console.log('订单数量:', orders.length)
  382. this.allOrderList = orders.map(order => this.transformOrder(order, this.currentTab))
  383. } catch (err) {
  384. console.error('获取订单列表失败:', err)
  385. this.allOrderList = []
  386. uni.showToast({ title: err.message || err.msg || '加载订单失败', icon: 'none' })
  387. }
  388. },
  389. transformOrder(order, tabIndex) {
  390. const service = this.serviceList.find(s => s.id === order.service)
  391. const serviceText = service?.name || '未知'
  392. const serviceIcon = service?.iconUrl || ''
  393. const mode = service?.mode || 0
  394. const isRoundTrip = mode === 1
  395. // 根据 Tab 索引强制指定状态文字,忽略后端缺失的 status 字段
  396. let statusText = '接单'
  397. if (tabIndex === 0) {
  398. statusText = '接单' // 待接送/服务
  399. } else if (tabIndex === 1) {
  400. statusText = isRoundTrip ? '出发' : '开始' // 配送/服务中
  401. } else if (tabIndex === 2) {
  402. statusText = '已完成' // 已完成
  403. } else if (tabIndex === 3) {
  404. statusText = '已拒绝' // 已拒绝
  405. }
  406. return {
  407. id: order.id,
  408. status: order.status, // 保存原始 status 用于判断权限
  409. type: isRoundTrip ? 1 : 2,
  410. typeText: serviceText,
  411. typeIcon: serviceIcon,
  412. statusText: statusText,
  413. fulfillmentCommission: (order.fulfillmentCommission / 100).toFixed(2),
  414. timeLabel: '服务时间',
  415. time: order.serviceTime || '',
  416. petAvatar: order.petAvatar || '/static/dog.png',
  417. petAvatarUrl: order.petAvatarUrl || '',
  418. petName: order.petName || '',
  419. petBreed: order.breed || '',
  420. startLocation: order.fromAddress || '暂无起点',
  421. startAddress: order.fromAddress || '',
  422. fromAddress: order.fromAddress || '',
  423. fromLat: order.fromLat,
  424. fromLng: order.fromLng,
  425. startDistance: '0km',
  426. endLocation: (order.customerName || '') + ' ' + (order.customerPhone || ''),
  427. endAddress: order.toAddress || '',
  428. toAddress: order.toAddress || '',
  429. toLat: order.toLat,
  430. toLng: order.toLng,
  431. customerPhone: order.customerPhone || '',
  432. endDistance: '0km',
  433. serviceContent: order.remark || '',
  434. remark: order.remark || '',
  435. serviceFlag: !!order.serviceFlag // 是否允许服务(点击跳转)
  436. }
  437. },
  438. getDisplayStatus(item) {
  439. if (item.statusText === '已完成') return '已完成';
  440. if (item.statusText === '已拒绝') return '已拒绝';
  441. if (item.statusText === '接单') {
  442. return item.type === 1 ? '待接送' : '待服务';
  443. }
  444. return item.type === 1 ? '配送中' : '服务中';
  445. },
  446. getStatusClass(item) {
  447. let display = this.getDisplayStatus(item);
  448. if (display === '已完成') return 'finish';
  449. if (display === '已拒绝') return 'reject';
  450. if (display === '配送中' || display === '服务中') return 'processing';
  451. return 'highlight';
  452. },
  453. goToDetail(item) {
  454. uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
  455. },
  456. showPetProfile(item) {
  457. this.currentPetInfo = {
  458. ...item,
  459. petGender: 'M',
  460. petAge: '2岁',
  461. petWeight: '15kg',
  462. petPersonality: '活泼亲人,精力旺盛',
  463. petHobby: '喜欢追飞盘,爱吃肉干',
  464. petRemark: '肠胃较弱,不能乱喂零食;出门易爆冲,请拉紧牵引绳。',
  465. petTags: ['拉响警报', '不能吃鸡肉', '精力旺盛'],
  466. petLogs: [
  467. { date: '2026-02-09 14:00', content: '今天遛弯拉了两次粑粑,精神状态很好。', recorder: '王阿姨' },
  468. { date: '2026-02-08 10:30', content: '有些挑食,剩了小半碗狗粮。', recorder: '李师傅' },
  469. { date: '2026-02-05 09:00', content: '建档。', recorder: '系统记录' }
  470. ]
  471. };
  472. this.showPetModal = true;
  473. },
  474. closePetProfile() {
  475. this.showPetModal = false;
  476. },
  477. openNavigation(item, pointType) {
  478. this.navTargetItem = item;
  479. this.navTargetPointType = pointType;
  480. this.showNavModal = true;
  481. },
  482. closeNavModal() {
  483. this.showNavModal = false;
  484. },
  485. chooseMap(mapType) {
  486. let item = this.navTargetItem;
  487. let pointType = this.navTargetPointType;
  488. // 起 -> fromAddress ; 终 -> toAddress
  489. let name = pointType === 'start' ? (item.fromAddress || '起点') : (item.toAddress || '终点');
  490. let address = pointType === 'start' ? (item.fromAddress || '起点地址') : (item.toAddress || '终点地址');
  491. let latitude = pointType === 'start' ? Number(item.fromLat) : Number(item.toLat);
  492. let longitude = pointType === 'start' ? Number(item.fromLng) : Number(item.toLng);
  493. this.showNavModal = false;
  494. // 统一定义打开地图的函数
  495. const navigateTo = (lat, lng, addrName, addrDesc) => {
  496. uni.openLocation({
  497. latitude: lat,
  498. longitude: lng,
  499. name: addrName,
  500. address: addrDesc || '无法获取详细地址',
  501. success: function () {
  502. console.log('打开导航成功: ' + mapType);
  503. },
  504. fail: function (err) {
  505. console.error('打开导航失败:', err);
  506. uni.showToast({ title: '打开地图失败', icon: 'none' });
  507. }
  508. });
  509. };
  510. // 如果有目标经纬度,直接打开
  511. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  512. navigateTo(latitude, longitude, name, address);
  513. } else {
  514. // 如果没有经纬度,按照需求:使用自己当前的经纬度,然后搜索 fromAddress 或者 toAddress
  515. uni.showLoading({ title: '获取当前位置...', mask: true });
  516. reportGps(true).then(res => {
  517. uni.hideLoading();
  518. // 使用用户当前经纬度作为锚点打开地图,展示目标地址信息
  519. navigateTo(res.latitude, res.longitude, name, address);
  520. }).catch(err => {
  521. uni.hideLoading();
  522. console.error('获取地理位置失败:', err);
  523. // 具体的授权引导已在 reportGps 内部处理
  524. });
  525. }
  526. },
  527. toggleCallMenu(item) {
  528. if (this.activeCallItem === item) {
  529. this.activeCallItem = null;
  530. } else {
  531. this.activeCallItem = item;
  532. }
  533. },
  534. closeCallMenu() {
  535. this.activeCallItem = null;
  536. },
  537. doCall(type, item) {
  538. let phoneNum = '';
  539. const targetItem = item || this.activeCallItem;
  540. // 1. 获取电话号码
  541. if (type === 'merchant') {
  542. phoneNum = '18900008451';
  543. } else if (type === 'customer') {
  544. phoneNum = targetItem?.customerPhone;
  545. }
  546. // 2. 基础校验
  547. if (!phoneNum) {
  548. uni.showToast({ title: '未找到电话号码', icon: 'none' });
  549. this.activeCallItem = null;
  550. return;
  551. }
  552. // 3. 清洗号码 (去除空格、横杠等非数字字符)
  553. phoneNum = phoneNum.replace(/[^\d]/g, '');
  554. // 二次校验:确保清洗后仍有数字
  555. if (phoneNum.length < 3) {
  556. uni.showToast({ title: '电话号码格式错误', icon: 'none' });
  557. this.activeCallItem = null;
  558. return;
  559. }
  560. console.log('正在发起直接呼叫:', phoneNum);
  561. // 4. 核心逻辑:区分环境处理
  562. // #ifdef APP-PLUS
  563. // App 端:使用 uni.makePhoneCall 直接发起呼叫
  564. uni.makePhoneCall({
  565. phoneNumber: phoneNum,
  566. success: () => {
  567. console.log('成功唤起系统拨号盘');
  568. },
  569. fail: (err) => {
  570. console.error('拨号失败:', err);
  571. // 常见错误:Permission denied (权限被拒) 或 Activity not found
  572. let msg = '拨号失败';
  573. if (err.message && err.message.includes('permission')) {
  574. msg = '请在手机设置中允许"电话"权限';
  575. }
  576. uni.showToast({ title: msg, icon: 'none', duration: 3000 });
  577. // 如果失败,尝试引导用户去设置页 (仅限 Android)
  578. // #ifdef APP-ANDROID
  579. if (err.message && err.message.includes('permission')) {
  580. uni.showModal({
  581. title: '权限提示',
  582. content: '拨打电话需要电话权限,是否前往设置开启?',
  583. success: (res) => {
  584. if (res.confirm) {
  585. plus.runtime.openURL("app-settings:");
  586. }
  587. }
  588. });
  589. }
  590. // #endif
  591. },
  592. complete: () => {
  593. this.activeCallItem = null; // 关闭弹窗
  594. }
  595. });
  596. // #endif
  597. // #ifdef H5
  598. // H5 端:使用 tel: 协议
  599. window.location.href = `tel:${phoneNum}`;
  600. this.activeCallItem = null;
  601. // #endif
  602. // #ifdef MP-WEIXIN
  603. // 小程序端:直接调用 makePhoneCall (微信小程序支持直接弹框确认拨打)
  604. uni.makePhoneCall({
  605. phoneNumber: phoneNum,
  606. fail: () => {
  607. uni.showToast({ title: '拨号失败', icon: 'none' });
  608. },
  609. complete: () => {
  610. this.activeCallItem = null;
  611. }
  612. });
  613. // #endif
  614. },
  615. reportAbnormal(item) {
  616. uni.navigateTo({ url: '/pages/orders/anomaly/index?orderId=' + (item.id || '') });
  617. },
  618. toggleDropdown(idx) {
  619. if (this.activeDropdown === idx) {
  620. this.activeDropdown = 0;
  621. } else {
  622. this.activeDropdown = idx;
  623. }
  624. },
  625. closeDropdown() {
  626. this.activeDropdown = 0;
  627. },
  628. selectType(index) {
  629. this.currentTypeFilterIdx = index;
  630. this.closeDropdown();
  631. },
  632. initCalendar() {
  633. const year = this.viewDate.getFullYear();
  634. const month = this.viewDate.getMonth();
  635. this.currentMonth = `${year}年${month + 1}月`;
  636. // 获取该月第一天是周几 (0-6)
  637. const firstDay = new Date(year, month, 1).getDay();
  638. // 获取该月有多少天
  639. const daysInMonth = new Date(year, month + 1, 0).getDate();
  640. let days = [];
  641. // 填充开头的空白
  642. for (let i = 0; i < firstDay; i++) {
  643. days.push(0);
  644. }
  645. // 填充真实日期
  646. for (let i = 1; i <= daysInMonth; i++) {
  647. days.push(i);
  648. }
  649. this.calendarDays = days;
  650. },
  651. prevMonth() {
  652. this.viewDate.setMonth(this.viewDate.getMonth() - 1);
  653. // 切换月份时强制重新创建 Date 对象以触发 Vue 响应式(如果需要)或者简单调用 init
  654. this.viewDate = new Date(this.viewDate);
  655. this.initCalendar();
  656. },
  657. nextMonth() {
  658. this.viewDate.setMonth(this.viewDate.getMonth() + 1);
  659. this.viewDate = new Date(this.viewDate);
  660. this.initCalendar();
  661. },
  662. selectDateItem(day) {
  663. if (this.selectedDateRange.length === 2) {
  664. this.selectedDateRange = [day];
  665. } else if (this.selectedDateRange.length === 1) {
  666. let start = this.selectedDateRange[0];
  667. if (day > start) {
  668. this.selectedDateRange = [start, day];
  669. } else if (day < start) {
  670. this.selectedDateRange = [day, start];
  671. } else {
  672. this.selectedDateRange = [];
  673. }
  674. } else {
  675. this.selectedDateRange = [day];
  676. }
  677. },
  678. getDateClass(day) {
  679. if (!day || this.selectedDateRange.length === 0) return '';
  680. if (this.selectedDateRange.length === 1) {
  681. return day === this.selectedDateRange[0] ? 'is-start' : '';
  682. }
  683. let start = this.selectedDateRange[0];
  684. let end = this.selectedDateRange[1];
  685. if (day === start) return 'is-start';
  686. if (day === end) return 'is-end';
  687. if (day > start && day < end) return 'is-between';
  688. return '';
  689. },
  690. resetTimeFilter() {
  691. this.hasTimeFilter = false;
  692. this.selectedDateRange = [];
  693. this.startServiceTime = '';
  694. this.endServiceTime = '';
  695. this.closeDropdown();
  696. this.loadOrders();
  697. },
  698. confirmTimeFilter() {
  699. if (this.selectedDateRange.length === 0) {
  700. uni.showToast({ title: '请先选择日期', icon: 'none' });
  701. return;
  702. }
  703. // 构建时间范围参数
  704. const year = this.currentMonth.replace(/[^0-9]/g, '').substring(0, 4);
  705. const month = this.currentMonth.replace(/[^0-9]/g, '').substring(4);
  706. const pad = (n) => String(n).padStart(2, '0');
  707. if (this.selectedDateRange.length === 2) {
  708. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  709. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[1])} 23:59:59`;
  710. } else {
  711. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  712. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 23:59:59`;
  713. }
  714. this.hasTimeFilter = true;
  715. this.closeDropdown();
  716. this.loadOrders();
  717. },
  718. getMainActionText(item) {
  719. return '查看详情';
  720. },
  721. mainAction(item) {
  722. uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
  723. },
  724. addOrUpdateService(item) {
  725. // 跳转到申诉(增改服务项)页面
  726. uni.navigateTo({ url: `/pages/orders/appeal/index?id=${item.id}` });
  727. },
  728. openRemarkInput() {
  729. this.remarkText = '';
  730. this.showRemarkInput = true;
  731. },
  732. closeRemarkInput() {
  733. this.showRemarkInput = false;
  734. this.remarkText = '';
  735. },
  736. submitRemark() {
  737. const text = this.remarkText.trim();
  738. if (!text) {
  739. uni.showToast({ title: '备注内容不能为空', icon: 'none' });
  740. return;
  741. }
  742. const now = new Date();
  743. const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  744. if (!this.currentPetInfo.petLogs) {
  745. this.$set(this.currentPetInfo, 'petLogs', []);
  746. }
  747. this.currentPetInfo.petLogs.unshift({
  748. date: dateStr,
  749. content: text,
  750. recorder: '我'
  751. });
  752. uni.showToast({ title: '备注已添加', icon: 'success' });
  753. this.closeRemarkInput();
  754. },
  755. /**
  756. * 取消订单处理逻辑 - 打开弹窗
  757. * @param {Object} item - 订单项
  758. */
  759. handleCancelOrder(item) {
  760. this.currentOrder = item;
  761. this.cancelReason = '';
  762. this.showCancelModal = true;
  763. },
  764. closeCancelModal() {
  765. this.showCancelModal = false;
  766. this.currentOrder = null;
  767. },
  768. async confirmCancel() {
  769. if (!this.cancelReason.trim()) {
  770. uni.showToast({ title: '请输入取消原因', icon: 'none' });
  771. return;
  772. }
  773. try {
  774. uni.showLoading({ title: '取消中...', mask: true });
  775. await cancelOrderApi({
  776. orderId: this.currentOrder.id,
  777. reason: this.cancelReason
  778. });
  779. uni.showToast({ title: '订单已取消', icon: 'success' });
  780. this.showCancelModal = false;
  781. this.currentOrder = null;
  782. // 延时刷新列表,防止提示框闪现
  783. setTimeout(() => {
  784. this.loadOrders();
  785. }, 1000);
  786. } catch (err) {
  787. console.error('取消订单失败:', err);
  788. uni.showToast({ title: err.message || err.msg || '取消失败', icon: 'none' });
  789. } finally {
  790. uni.hideLoading();
  791. }
  792. }
  793. }
  794. }
  795. </script>
  796. <style>
  797. page {
  798. background-color: #F8F8F8;
  799. }
  800. .custom-nav-bar {
  801. padding: 80rpx 30rpx 20rpx;
  802. background-color: #fff;
  803. display: flex;
  804. align-items: center;
  805. justify-content: center;
  806. }
  807. .nav-title {
  808. font-size: 34rpx;
  809. font-weight: bold;
  810. color: #333;
  811. }
  812. .sticky-header {
  813. position: sticky;
  814. top: 0;
  815. z-index: 999;
  816. background-color: #F8F8F8;
  817. }
  818. .container {
  819. background-color: #F8F8F8;
  820. display: flex;
  821. flex-direction: column;
  822. min-height: 100vh;
  823. }
  824. .status-tabs {
  825. display: flex;
  826. background-color: #fff;
  827. padding: 0 30rpx;
  828. justify-content: space-between;
  829. }
  830. .tab-item {
  831. position: relative;
  832. padding: 20rpx 0;
  833. font-size: 26rpx;
  834. color: #666;
  835. font-weight: 500;
  836. }
  837. .tab-item.active {
  838. color: #FF5722;
  839. font-weight: bold;
  840. }
  841. .indicator {
  842. position: absolute;
  843. bottom: 0;
  844. left: 50%;
  845. transform: translateX(-50%);
  846. width: 40rpx;
  847. height: 6rpx;
  848. background-color: #FF5722;
  849. border-radius: 3rpx;
  850. }
  851. .search-bar {
  852. padding: 10rpx 30rpx;
  853. background-color: #fff;
  854. }
  855. .search-input-box {
  856. display: flex;
  857. align-items: center;
  858. background-color: #F8F8F8;
  859. height: 64rpx;
  860. border-radius: 32rpx;
  861. padding: 0 30rpx;
  862. }
  863. .search-input {
  864. flex: 1;
  865. font-size: 26rpx;
  866. color: #333;
  867. padding-left: 20rpx;
  868. }
  869. .ph-style {
  870. font-size: 26rpx;
  871. color: #999;
  872. }
  873. .filter-wrapper {
  874. position: relative;
  875. z-index: 998;
  876. }
  877. .filter-bar {
  878. display: flex;
  879. background-color: #fff;
  880. padding: 5rpx 30rpx 10rpx 30rpx;
  881. justify-content: space-between;
  882. position: relative;
  883. z-index: 998;
  884. }
  885. .filter-item {
  886. width: 48%;
  887. display: flex;
  888. align-items: center;
  889. justify-content: center;
  890. font-size: 26rpx;
  891. color: #666;
  892. background-color: #F8F8F8;
  893. height: 56rpx;
  894. border-radius: 12rpx;
  895. transition: all 0.2s;
  896. }
  897. .filter-item.active {
  898. background-color: #FFF3E0;
  899. }
  900. .active-text {
  901. color: #FF5722;
  902. font-weight: 500;
  903. }
  904. .triangle {
  905. width: 0;
  906. height: 0;
  907. border-left: 8rpx solid transparent;
  908. border-right: 8rpx solid transparent;
  909. margin-left: 10rpx;
  910. transition: all 0.2s;
  911. }
  912. .triangle.down {
  913. border-top: 10rpx solid #dcdcdc;
  914. }
  915. .filter-item.active .triangle.down,
  916. .active-text+.triangle.down {
  917. border-top-color: #FF5722;
  918. }
  919. .triangle.up {
  920. border-bottom: 10rpx solid #FF5722;
  921. }
  922. .dropdown-mask {
  923. position: absolute;
  924. top: 100%;
  925. left: 0;
  926. right: 0;
  927. height: 100vh;
  928. background-color: rgba(0, 0, 0, 0.4);
  929. z-index: 80;
  930. }
  931. .dropdown-panel {
  932. position: absolute;
  933. top: 100%;
  934. left: 0;
  935. right: 0;
  936. background-color: #fff;
  937. z-index: 90;
  938. border-radius: 0 0 20rpx 20rpx;
  939. box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.05);
  940. overflow: hidden;
  941. }
  942. .type-option {
  943. padding: 30rpx 40rpx;
  944. font-size: 28rpx;
  945. color: #333;
  946. border-bottom: 1px solid #f5f5f5;
  947. }
  948. .type-option:last-child {
  949. border-bottom: none;
  950. }
  951. .type-option.selected text {
  952. color: #FF5722;
  953. font-weight: bold;
  954. }
  955. .calendar-panel {
  956. padding-bottom: 30rpx;
  957. }
  958. .custom-calendar-container {
  959. padding: 20rpx 30rpx 0;
  960. }
  961. .cal-header {
  962. display: flex;
  963. justify-content: space-between;
  964. align-items: center;
  965. padding: 20rpx 0;
  966. }
  967. .cal-title {
  968. font-size: 32rpx;
  969. font-weight: bold;
  970. color: #333;
  971. }
  972. .cal-weekdays {
  973. display: flex;
  974. justify-content: space-around;
  975. padding: 20rpx 0;
  976. border-bottom: 1px solid #f5f5f5;
  977. }
  978. .wk-item {
  979. font-size: 24rpx;
  980. color: #999;
  981. width: 14.28%;
  982. text-align: center;
  983. }
  984. .cal-body {
  985. display: flex;
  986. flex-wrap: wrap;
  987. padding-top: 20rpx;
  988. }
  989. .cal-day-box {
  990. width: 14.28%;
  991. height: 80rpx;
  992. display: flex;
  993. align-items: center;
  994. justify-content: center;
  995. margin-bottom: 10rpx;
  996. position: relative;
  997. }
  998. .cal-day-text {
  999. width: 64rpx;
  1000. height: 64rpx;
  1001. line-height: 64rpx;
  1002. text-align: center;
  1003. font-size: 28rpx;
  1004. color: #333;
  1005. border-radius: 8rpx;
  1006. position: relative;
  1007. z-index: 2;
  1008. }
  1009. .cal-day-box.is-start .cal-day-text,
  1010. .cal-day-box.is-end .cal-day-text {
  1011. background-color: #FF5722;
  1012. color: #fff;
  1013. font-weight: bold;
  1014. }
  1015. .cal-day-box.is-start::after {
  1016. content: '';
  1017. position: absolute;
  1018. right: 0;
  1019. top: 8rpx;
  1020. bottom: 8rpx;
  1021. width: 50%;
  1022. background-color: #FFF3E0;
  1023. z-index: 1;
  1024. }
  1025. .cal-day-box.is-end::after {
  1026. content: '';
  1027. position: absolute;
  1028. left: 0;
  1029. top: 8rpx;
  1030. bottom: 8rpx;
  1031. width: 50%;
  1032. background-color: #FFF3E0;
  1033. z-index: 1;
  1034. }
  1035. .cal-day-box.is-start.is-end::after {
  1036. display: none;
  1037. }
  1038. .cal-day-box.is-between {
  1039. background-color: #FFF3E0;
  1040. margin-top: 8rpx;
  1041. height: 64rpx;
  1042. margin-bottom: 18rpx;
  1043. }
  1044. .cal-day-box.is-between .cal-day-text {
  1045. color: #FF5722;
  1046. }
  1047. .calendar-actions {
  1048. display: flex;
  1049. justify-content: space-between;
  1050. padding: 0 30rpx;
  1051. margin-top: 20rpx;
  1052. }
  1053. .cal-btn {
  1054. width: 48%;
  1055. height: 70rpx;
  1056. line-height: 70rpx;
  1057. text-align: center;
  1058. border-radius: 10rpx;
  1059. font-size: 28rpx;
  1060. margin: 0;
  1061. }
  1062. .cal-btn.reset {
  1063. background-color: #f5f5f5;
  1064. color: #666;
  1065. }
  1066. .cal-btn.confirm {
  1067. background-color: #FF5722;
  1068. color: #fff;
  1069. }
  1070. .order-list {
  1071. padding: 0 30rpx;
  1072. width: 100%;
  1073. box-sizing: border-box;
  1074. }
  1075. .order-card {
  1076. background-color: #fff;
  1077. border-radius: 24rpx;
  1078. padding: 20rpx 20rpx;
  1079. margin-bottom: 20rpx;
  1080. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
  1081. }
  1082. .order-card:first-child {
  1083. margin-top: 20rpx;
  1084. }
  1085. .card-header {
  1086. display: flex;
  1087. justify-content: space-between;
  1088. align-items: center;
  1089. margin-bottom: 15rpx;
  1090. }
  1091. .type-badge {
  1092. display: flex;
  1093. align-items: center;
  1094. }
  1095. .type-icon {
  1096. width: 44rpx;
  1097. height: 44rpx;
  1098. margin-right: 15rpx;
  1099. background-color: #FFF3E0;
  1100. border-radius: 50%;
  1101. padding: 6rpx;
  1102. box-sizing: border-box;
  1103. }
  1104. .type-text {
  1105. font-size: 30rpx;
  1106. font-weight: bold;
  1107. color: #333;
  1108. }
  1109. .status-badge {
  1110. font-size: 28rpx;
  1111. }
  1112. .status-badge.highlight {
  1113. color: #FF5722;
  1114. }
  1115. .status-badge.processing {
  1116. color: #2196F3;
  1117. }
  1118. .status-badge.finish {
  1119. color: #4CAF50;
  1120. }
  1121. .status-badge.reject {
  1122. color: #9E9E9E;
  1123. }
  1124. .time-row {
  1125. display: flex;
  1126. justify-content: space-between;
  1127. align-items: center;
  1128. margin-bottom: 25rpx;
  1129. }
  1130. .time-row .time-col {
  1131. display: flex;
  1132. align-items: center;
  1133. font-size: 26rpx;
  1134. color: #333;
  1135. }
  1136. .time-row .label {
  1137. color: #666;
  1138. margin-right: 10rpx;
  1139. }
  1140. .fulfillmentCommission {
  1141. font-size: 36rpx;
  1142. font-weight: bold;
  1143. color: #FF5722;
  1144. }
  1145. .pet-card {
  1146. background-color: #FFF8F0;
  1147. border-radius: 16rpx;
  1148. padding: 15rpx 20rpx;
  1149. display: flex;
  1150. align-items: center;
  1151. margin-bottom: 20rpx;
  1152. }
  1153. .pet-avatar {
  1154. width: 80rpx;
  1155. height: 80rpx;
  1156. border-radius: 50%;
  1157. margin-right: 20rpx;
  1158. }
  1159. .pet-info {
  1160. flex: 1;
  1161. display: flex;
  1162. flex-direction: column;
  1163. }
  1164. .pet-name {
  1165. font-size: 28rpx;
  1166. font-weight: bold;
  1167. color: #333;
  1168. margin-bottom: 5rpx;
  1169. }
  1170. .pet-breed {
  1171. font-size: 24rpx;
  1172. color: #999;
  1173. }
  1174. .pet-profile-btn {
  1175. font-size: 24rpx;
  1176. color: #FF9800;
  1177. border: 1px solid #FF9800;
  1178. padding: 6rpx 20rpx;
  1179. border-radius: 50rpx;
  1180. background-color: #fff;
  1181. }
  1182. .route-info {
  1183. margin-bottom: 25rpx;
  1184. }
  1185. .route-item {
  1186. display: flex;
  1187. align-items: flex-start;
  1188. padding-bottom: 12rpx;
  1189. position: relative;
  1190. width: 100%;
  1191. }
  1192. .route-item:not(:last-child) {
  1193. margin-bottom: 5rpx;
  1194. }
  1195. .route-item:last-child {
  1196. padding-bottom: 0;
  1197. margin-bottom: 0;
  1198. }
  1199. .route-line-vertical {
  1200. position: absolute;
  1201. left: 19rpx;
  1202. top: 46rpx;
  1203. bottom: -15rpx;
  1204. border-left: 2rpx dashed #E0E0E0;
  1205. width: 0;
  1206. z-index: 0;
  1207. }
  1208. .icon-circle {
  1209. width: 40rpx;
  1210. height: 40rpx;
  1211. border-radius: 50%;
  1212. color: #fff;
  1213. font-size: 22rpx;
  1214. display: flex;
  1215. align-items: center;
  1216. justify-content: center;
  1217. margin-right: 20rpx;
  1218. flex-shrink: 0;
  1219. font-weight: bold;
  1220. margin-top: 6rpx;
  1221. position: relative;
  1222. z-index: 1;
  1223. }
  1224. .icon-circle.service {
  1225. background-color: #81C784;
  1226. }
  1227. .icon-circle.start {
  1228. background-color: #FFB74D;
  1229. }
  1230. .icon-circle.end {
  1231. background-color: #81C784;
  1232. }
  1233. .address-box {
  1234. flex: 1;
  1235. display: flex;
  1236. flex-direction: column;
  1237. margin-right: 20rpx;
  1238. }
  1239. .addr-title {
  1240. font-size: 28rpx;
  1241. font-weight: bold;
  1242. color: #333;
  1243. margin-bottom: 4rpx;
  1244. }
  1245. .addr-desc {
  1246. font-size: 24rpx;
  1247. color: #999;
  1248. line-height: 1.4;
  1249. }
  1250. .distance-tag {
  1251. display: flex;
  1252. align-items: center;
  1253. justify-content: flex-end;
  1254. flex-shrink: 0;
  1255. min-width: 80rpx;
  1256. }
  1257. .distance-text {
  1258. font-size: 24rpx;
  1259. color: #FF5722;
  1260. margin-right: 15rpx;
  1261. font-weight: 500;
  1262. }
  1263. .nav-icon-circle {
  1264. width: 48rpx;
  1265. height: 48rpx;
  1266. background-color: #FFF3E0;
  1267. border-radius: 50%;
  1268. display: flex;
  1269. align-items: center;
  1270. justify-content: center;
  1271. }
  1272. .nav-arrow {
  1273. width: 24rpx;
  1274. height: 24rpx;
  1275. }
  1276. .service-content {
  1277. margin-top: -10rpx;
  1278. font-size: 24rpx;
  1279. color: #666;
  1280. padding-left: 60rpx;
  1281. }
  1282. .content-label {
  1283. color: #999;
  1284. margin-right: 10rpx;
  1285. }
  1286. .remark-box {
  1287. background-color: #F8F8F8;
  1288. padding: 15rpx 20rpx;
  1289. border-radius: 8rpx;
  1290. font-size: 24rpx;
  1291. color: #666;
  1292. margin-bottom: 20rpx;
  1293. }
  1294. .action-btns {
  1295. display: flex;
  1296. flex-direction: column;
  1297. gap: 16rpx;
  1298. margin-top: 20rpx;
  1299. }
  1300. .action-row {
  1301. display: flex;
  1302. justify-content: space-between;
  1303. align-items: center;
  1304. width: 100%;
  1305. }
  1306. .btn {
  1307. height: 60rpx;
  1308. line-height: 60rpx;
  1309. border-radius: 30rpx;
  1310. font-size: 26rpx;
  1311. padding: 0 30rpx;
  1312. margin: 0;
  1313. }
  1314. .action-right .btn:not(:last-child) {
  1315. margin-right: 20rpx;
  1316. }
  1317. .btn::after {
  1318. border: none;
  1319. }
  1320. .btn.normal {
  1321. background-color: #F8F8F8;
  1322. color: #666;
  1323. border: none;
  1324. }
  1325. .btn.primary {
  1326. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1327. color: #fff;
  1328. box-shadow: 0 4rpx 12rpx rgba(255, 87, 34, 0.2);
  1329. border: none;
  1330. }
  1331. .btn.normal.danger {
  1332. background-color: #FFF2F0;
  1333. color: #F5222D;
  1334. }
  1335. .pet-modal-mask {
  1336. position: fixed;
  1337. top: 0;
  1338. left: 0;
  1339. right: 0;
  1340. bottom: 0;
  1341. background-color: rgba(0, 0, 0, 0.4);
  1342. z-index: 1000;
  1343. display: flex;
  1344. align-items: center;
  1345. justify-content: center;
  1346. }
  1347. .pet-modal-content {
  1348. width: 680rpx;
  1349. height: 85vh;
  1350. background-color: #fff;
  1351. border-radius: 20rpx;
  1352. display: flex;
  1353. flex-direction: column;
  1354. overflow: hidden;
  1355. }
  1356. .pet-modal-header {
  1357. display: flex;
  1358. align-items: center;
  1359. justify-content: space-between;
  1360. padding: 30rpx;
  1361. border-bottom: 1rpx solid #F0F0F0;
  1362. }
  1363. .pet-modal-title {
  1364. font-size: 34rpx;
  1365. font-weight: bold;
  1366. color: #333;
  1367. }
  1368. .pet-modal-scroll {
  1369. flex: 1;
  1370. height: 0;
  1371. padding: 30rpx;
  1372. box-sizing: border-box;
  1373. }
  1374. .pet-base-info {
  1375. display: flex;
  1376. align-items: center;
  1377. margin-bottom: 40rpx;
  1378. }
  1379. .pm-avatar {
  1380. width: 120rpx;
  1381. height: 120rpx;
  1382. border-radius: 50%;
  1383. margin-right: 30rpx;
  1384. border: 2rpx solid #f5f5f5;
  1385. }
  1386. .pm-info-text {
  1387. flex: 1;
  1388. display: flex;
  1389. flex-direction: column;
  1390. }
  1391. .pm-name-row {
  1392. display: flex;
  1393. align-items: center;
  1394. margin-bottom: 15rpx;
  1395. }
  1396. .pm-name {
  1397. font-size: 36rpx;
  1398. font-weight: bold;
  1399. color: #333;
  1400. margin-right: 20rpx;
  1401. }
  1402. .pm-gender {
  1403. display: flex;
  1404. align-items: center;
  1405. background-color: #E3F2FD;
  1406. padding: 4rpx 12rpx;
  1407. border-radius: 20rpx;
  1408. }
  1409. .pm-gender text {
  1410. font-size: 22rpx;
  1411. color: #1E88E5;
  1412. }
  1413. .pm-gender .gender-icon {
  1414. font-weight: bold;
  1415. margin-right: 4rpx;
  1416. }
  1417. .pm-gender.female {
  1418. background-color: #FCE4EC;
  1419. }
  1420. .pm-gender.female text {
  1421. color: #D81B60;
  1422. }
  1423. .pm-breed {
  1424. font-size: 26rpx;
  1425. color: #999;
  1426. }
  1427. .pm-detail-grid {
  1428. display: flex;
  1429. flex-wrap: wrap;
  1430. justify-content: space-between;
  1431. }
  1432. .pm-grid-item {
  1433. background-color: #F8F8F8;
  1434. border-radius: 16rpx;
  1435. padding: 24rpx;
  1436. margin-bottom: 20rpx;
  1437. display: flex;
  1438. flex-direction: column;
  1439. }
  1440. .pm-grid-item.half {
  1441. width: 48%;
  1442. box-sizing: border-box;
  1443. }
  1444. .pm-grid-item.full {
  1445. width: 100%;
  1446. box-sizing: border-box;
  1447. }
  1448. .pm-label {
  1449. font-size: 24rpx;
  1450. color: #999;
  1451. margin-bottom: 10rpx;
  1452. }
  1453. .pm-val {
  1454. font-size: 28rpx;
  1455. color: #333;
  1456. font-weight: 500;
  1457. }
  1458. .pm-tags {
  1459. display: flex;
  1460. flex-wrap: wrap;
  1461. gap: 20rpx;
  1462. margin-bottom: 40rpx;
  1463. }
  1464. .pm-tag {
  1465. background-color: #FFF8EB;
  1466. border: 2rpx solid #FFCC80;
  1467. color: #FF9800;
  1468. font-size: 22rpx;
  1469. padding: 8rpx 24rpx;
  1470. border-radius: 30rpx;
  1471. }
  1472. .pm-section-title {
  1473. display: flex;
  1474. align-items: center;
  1475. margin-bottom: 30rpx;
  1476. padding-top: 30rpx;
  1477. border-top: 2rpx dashed #F0F0F0;
  1478. }
  1479. .pm-section-title .orange-bar {
  1480. width: 8rpx;
  1481. height: 32rpx;
  1482. background-color: #FF9800;
  1483. margin-right: 16rpx;
  1484. border-radius: 4rpx;
  1485. }
  1486. .pm-section-title text {
  1487. font-size: 30rpx;
  1488. font-weight: bold;
  1489. color: #333;
  1490. }
  1491. .pm-log-list {
  1492. display: flex;
  1493. flex-direction: column;
  1494. }
  1495. .pm-log-item {
  1496. display: flex;
  1497. flex-direction: column;
  1498. padding: 24rpx 0;
  1499. border-bottom: 1rpx solid #F0F0F0;
  1500. }
  1501. .pm-log-item:last-child {
  1502. border-bottom: none;
  1503. }
  1504. .pm-log-date {
  1505. font-size: 24rpx;
  1506. color: #999;
  1507. margin-bottom: 16rpx;
  1508. }
  1509. .pm-log-text {
  1510. font-size: 28rpx;
  1511. color: #333;
  1512. line-height: 1.6;
  1513. margin-bottom: 20rpx;
  1514. }
  1515. .pm-log-recorder {
  1516. font-size: 24rpx;
  1517. color: #FF9800;
  1518. align-self: flex-end;
  1519. }
  1520. .pm-bottom-close {
  1521. width: 100%;
  1522. height: 80rpx;
  1523. line-height: 80rpx;
  1524. background-color: #F5F5F5;
  1525. color: #666;
  1526. border-radius: 40rpx;
  1527. font-size: 30rpx;
  1528. font-weight: bold;
  1529. margin: 0;
  1530. }
  1531. .pm-bottom-close::after {
  1532. border: none;
  1533. }
  1534. .close-icon-btn {
  1535. font-size: 48rpx;
  1536. color: #999;
  1537. line-height: 1;
  1538. padding: 0 10rpx;
  1539. }
  1540. .nav-modal-mask {
  1541. position: fixed;
  1542. top: 0;
  1543. left: 0;
  1544. right: 0;
  1545. bottom: 0;
  1546. background-color: rgba(0, 0, 0, 0.5);
  1547. z-index: 1000;
  1548. display: flex;
  1549. flex-direction: column;
  1550. justify-content: flex-end;
  1551. }
  1552. .nav-action-sheet {
  1553. background-color: #fff;
  1554. width: 100%;
  1555. border-top-left-radius: 24rpx;
  1556. border-top-right-radius: 24rpx;
  1557. overflow: hidden;
  1558. padding-bottom: constant(safe-area-inset-bottom);
  1559. padding-bottom: env(safe-area-inset-bottom);
  1560. }
  1561. .nav-sheet-title {
  1562. text-align: center;
  1563. padding: 30rpx 0;
  1564. font-size: 13px;
  1565. color: #999;
  1566. border-bottom: 1rpx solid #efefef;
  1567. }
  1568. .nav-sheet-item {
  1569. text-align: center;
  1570. padding: 30rpx 0;
  1571. font-size: 13px;
  1572. color: #333;
  1573. background-color: #fff;
  1574. border-bottom: 1rpx solid #efefef;
  1575. }
  1576. .nav-sheet-item.cancel {
  1577. border-bottom: none;
  1578. color: #666;
  1579. }
  1580. .nav-sheet-gap {
  1581. height: 16rpx;
  1582. background-color: #F8F8F8;
  1583. }
  1584. .order-list {
  1585. flex: 1;
  1586. overflow-y: auto;
  1587. width: 100%;
  1588. padding: 0 30rpx;
  1589. box-sizing: border-box;
  1590. }
  1591. .loading-text {
  1592. text-align: center;
  1593. font-size: 24rpx;
  1594. color: #999;
  1595. padding: 30rpx 0;
  1596. }
  1597. .pm-header-actions {
  1598. display: flex;
  1599. align-items: center;
  1600. gap: 16rpx;
  1601. }
  1602. .pm-remark-btn {
  1603. font-size: 24rpx;
  1604. color: #fff;
  1605. background-color: #FF9800;
  1606. padding: 6rpx 18rpx;
  1607. border-radius: 20rpx;
  1608. }
  1609. .remark-mask {
  1610. position: fixed;
  1611. top: 0;
  1612. left: 0;
  1613. right: 0;
  1614. bottom: 0;
  1615. background-color: rgba(0, 0, 0, 0.5);
  1616. z-index: 3000;
  1617. display: flex;
  1618. align-items: center;
  1619. justify-content: center;
  1620. }
  1621. .remark-sheet {
  1622. width: 600rpx;
  1623. background-color: #fff;
  1624. border-radius: 24rpx;
  1625. padding: 40rpx;
  1626. box-sizing: border-box;
  1627. display: flex;
  1628. flex-direction: column;
  1629. align-items: center;
  1630. }
  1631. .remark-sheet-header {
  1632. width: 100%;
  1633. text-align: center;
  1634. margin-bottom: 30rpx;
  1635. position: relative;
  1636. }
  1637. .remark-sheet-header .close-icon-btn {
  1638. position: absolute;
  1639. right: 0;
  1640. top: 50%;
  1641. transform: translateY(-50%);
  1642. }
  1643. .remark-sheet-title {
  1644. font-size: 32rpx;
  1645. font-weight: bold;
  1646. color: #333;
  1647. }
  1648. .remark-textarea {
  1649. width: 100%;
  1650. height: 160rpx;
  1651. border: 1rpx solid #eee;
  1652. border-radius: 12rpx;
  1653. padding: 20rpx;
  1654. font-size: 28rpx;
  1655. color: #333;
  1656. box-sizing: border-box;
  1657. margin-bottom: 40rpx;
  1658. }
  1659. .remark-submit-btn {
  1660. width: 100%;
  1661. background-color: #FF5722;
  1662. color: #fff;
  1663. font-size: 32rpx;
  1664. font-weight: bold;
  1665. text-align: center;
  1666. padding: 24rpx 0;
  1667. border-radius: 16rpx;
  1668. }
  1669. .action-left {
  1670. position: relative;
  1671. z-index: 10;
  1672. }
  1673. .action-left .btn.normal {
  1674. font-size: 26rpx;
  1675. }
  1676. .call-popover {
  1677. position: absolute;
  1678. top: calc(100% + 10rpx);
  1679. left: 0;
  1680. background-color: #fff;
  1681. border-radius: 12rpx;
  1682. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  1683. z-index: 999;
  1684. display: flex;
  1685. flex-direction: column;
  1686. width: 200rpx;
  1687. }
  1688. .call-pop-item {
  1689. font-size: 26rpx;
  1690. color: #333;
  1691. text-align: center;
  1692. padding: 24rpx 0;
  1693. border-bottom: 1rpx solid #eee;
  1694. }
  1695. .call-pop-item:last-child {
  1696. border-bottom: none;
  1697. }
  1698. .call-mask {
  1699. position: fixed;
  1700. top: 0;
  1701. left: 0;
  1702. right: 0;
  1703. bottom: 0;
  1704. z-index: 900;
  1705. background: transparent;
  1706. }
  1707. /* 全局通用对话框样式 (复用首页思路) */
  1708. .modal-mask {
  1709. position: fixed;
  1710. top: 0;
  1711. left: 0;
  1712. right: 0;
  1713. bottom: 0;
  1714. background-color: rgba(0, 0, 0, 0.5);
  1715. z-index: 5000;
  1716. display: flex;
  1717. align-items: center;
  1718. justify-content: center;
  1719. }
  1720. .custom-modal {
  1721. width: 600rpx;
  1722. background-color: #fff;
  1723. border-radius: 24rpx;
  1724. padding: 40rpx 50rpx;
  1725. display: flex;
  1726. flex-direction: column;
  1727. align-items: center;
  1728. }
  1729. .modal-title {
  1730. font-size: 36rpx;
  1731. font-weight: bold;
  1732. color: #333;
  1733. margin-bottom: 30rpx;
  1734. }
  1735. .modal-btns {
  1736. width: 100%;
  1737. display: flex;
  1738. justify-content: space-between;
  1739. }
  1740. .modal-btn {
  1741. width: 45%;
  1742. height: 80rpx;
  1743. line-height: 80rpx;
  1744. border-radius: 40rpx;
  1745. font-size: 30rpx;
  1746. font-weight: bold;
  1747. margin: 0;
  1748. }
  1749. .modal-btn::after {
  1750. border: none;
  1751. }
  1752. .modal-btn.cancel {
  1753. background-color: #F5F5F5;
  1754. color: #999;
  1755. }
  1756. .modal-btn.confirm {
  1757. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1758. color: #fff;
  1759. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1760. }
  1761. .textarea-container {
  1762. padding: 0 4rpx;
  1763. width: 100%;
  1764. position: relative;
  1765. margin-bottom: 20rpx;
  1766. }
  1767. .reject-textarea {
  1768. width: 100%;
  1769. height: 240rpx;
  1770. background-color: #F9F9F9;
  1771. border: 1rpx solid #E0E0E0;
  1772. border-radius: 16rpx;
  1773. padding: 24rpx;
  1774. padding-bottom: 60rpx;
  1775. font-size: 28rpx;
  1776. line-height: 1.6;
  1777. box-sizing: border-box;
  1778. transition: all 0.3s;
  1779. }
  1780. .reject-textarea:focus {
  1781. border-color: #FF9800;
  1782. background-color: #fff;
  1783. }
  1784. .char-count {
  1785. position: absolute;
  1786. right: 44rpx;
  1787. bottom: 24rpx;
  1788. font-size: 22rpx;
  1789. color: #999;
  1790. }
  1791. .modal-btn.confirm.disabled {
  1792. background: #FFD180;
  1793. box-shadow: none;
  1794. opacity: 0.8;
  1795. }
  1796. .mt-30 {
  1797. margin-top: 30rpx;
  1798. }
  1799. .disabled-card {
  1800. opacity: 0.5; /* 降低透明度以示禁用 */
  1801. pointer-events: none; /* 禁用该卡片内背景的一切交互 */
  1802. filter: grayscale(80%); /* 增加灰度,使视觉效果更明显 */
  1803. }
  1804. .disabled-card .action-row {
  1805. pointer-events: auto; /* 允许按钮即使在置灰状态下也能点击操作 */
  1806. }
  1807. /* 即使使用了 pointer-events: none,外层的 @click 也会失效,为了保险我们在 JS 中也做了判断 */
  1808. </style>