index.vue 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323
  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">
  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" @click.stop="reportAbnormal(item)">异常上报</button>
  145. <button class="btn normal" @click.stop="addOrUpdateService(item)">服务变更</button>
  146. <button class="btn normal danger" @click.stop="handleCancelOrder(item)">取消订单</button>
  147. </view>
  148. <view class="action-row">
  149. <button class="btn normal" @click.stop="doCall('customer', item)">拨打电话</button>
  150. <button class="btn normal" @click.stop="viewAppealProgress(item)">变更进度</button>
  151. <button class="btn primary" @click.stop="mainAction(item)">{{ item.statusText }}</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. <!-- 申诉进度弹窗 -->
  274. <view class="modal-mask" v-if="showAppealModal" @click="closeAppealModal">
  275. <view class="custom-modal appeal-modal" @click.stop>
  276. <view class="appeal-title-bar">
  277. <text class="modal-title">变更进度</text>
  278. <view class="appeal-close-btn" @click="closeAppealModal">
  279. <text class="close-icon">✕</text>
  280. </view>
  281. </view>
  282. <scroll-view scroll-y class="appeal-scroll">
  283. <view class="appeal-empty" v-if="appealList.length === 0 && !appealLoading">
  284. <text>暂无变更记录</text>
  285. </view>
  286. <view class="appeal-timeline" v-else>
  287. <view class="appeal-item" v-for="(record, idx) in appealList" :key="idx">
  288. <view class="timeline-dot" :class="getAppealStatusClass(record)"></view>
  289. <view class="timeline-line" v-if="idx < appealList.length - 1"></view>
  290. <view class="appeal-card">
  291. <view class="appeal-header">
  292. <text class="appeal-service">{{ record.service || '服务变更' }}</text>
  293. <text class="appeal-status" :class="getAppealStatusClass(record)">{{
  294. getAppealStatusText(record) }}</text>
  295. </view>
  296. <view class="appeal-row" v-if="record.fulfillerName">
  297. <text class="appeal-label">申请人:</text>
  298. <text class="appeal-value">{{ record.fulfillerName }}</text>
  299. </view>
  300. <view class="appeal-row" v-if="record.serviceSpecification">
  301. <text class="appeal-label">变更说明:</text>
  302. <text class="appeal-value">{{ record.serviceSpecification }}</text>
  303. </view>
  304. <view class="appeal-row" v-if="record.reason">
  305. <text class="appeal-label">申诉理由:</text>
  306. <text class="appeal-value">{{ record.reason }}</text>
  307. </view>
  308. <view class="appeal-row" v-if="record.rejectReason">
  309. <text class="appeal-label">驳回理由:</text>
  310. <text class="appeal-value reject-reason">{{ record.rejectReason }}</text>
  311. </view>
  312. <view class="appeal-row" v-if="record.auditorName">
  313. <text class="appeal-label">审核人:</text>
  314. <text class="appeal-value">{{ record.auditorName }}</text>
  315. </view>
  316. <view class="appeal-time-row">
  317. <text class="appeal-time">提交:{{ formatTime(record.createTime) }}</text>
  318. <text class="appeal-time" v-if="record.auditTime">审核:{{ formatTime(record.auditTime)
  319. }}</text>
  320. </view>
  321. </view>
  322. </view>
  323. </view>
  324. </scroll-view>
  325. </view>
  326. </view>
  327. <custom-tabbar currentPath="pages/orders/index"></custom-tabbar>
  328. </template>
  329. <script>
  330. import { getMyOrders, cancelOrderApi } from '@/api/order/subOrder'
  331. import { getAppealListByOrderId } from '@/api/order/subOrderAppeal'
  332. import { listAllService } from '@/api/service/list'
  333. import { reportGps } from '@/utils/gps'
  334. import customTabbar from '@/components/custom-tabbar/index.vue'
  335. export default {
  336. components: {
  337. customTabbar
  338. },
  339. data() {
  340. return {
  341. currentTab: 0,
  342. tabs: ['待接送/服务', '配送/服务中', '已完成', '已取消'],
  343. typeFilterOptions: ['全部类型'],
  344. currentTypeFilterIdx: 0,
  345. activeDropdown: 0,
  346. hasTimeFilter: false,
  347. currentMonth: '',
  348. viewDate: new Date(),
  349. weekDays: ['日', '一', '二', '三', '四', '五', '六'],
  350. calendarDays: [],
  351. selectedDateRange: [],
  352. allOrderList: [],
  353. serviceList: [],
  354. searchContent: '',
  355. startServiceTime: '',
  356. endServiceTime: '',
  357. showPetModal: false,
  358. currentPetInfo: {},
  359. showNavModal: false,
  360. navTargetItem: null,
  361. navTargetPointType: '',
  362. activeCallItem: null,
  363. showRemarkInput: false,
  364. remarkText: '',
  365. showCancelModal: false,
  366. cancelReason: '',
  367. currentOrder: null,
  368. showAppealModal: false,
  369. appealList: [],
  370. appealLoading: false
  371. }
  372. },
  373. created() {
  374. this.initCalendar();
  375. },
  376. async onLoad() {
  377. await this.loadServiceList()
  378. await this.loadOrders()
  379. // 显式请求一次定位授权
  380. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  381. },
  382. onShow() {
  383. uni.hideTabBar()
  384. // 此处不需要重复调用,因为逻辑可能在onLoad已处理,
  385. // 或者如果需要每次进入都刷新,可以保留,但需确保顺序
  386. this.loadOrders()
  387. },
  388. async onPullDownRefresh() {
  389. try {
  390. await this.loadServiceList()
  391. await this.loadOrders()
  392. } finally {
  393. uni.stopPullDownRefresh()
  394. }
  395. },
  396. watch: {
  397. currentTab() {
  398. this.loadOrders()
  399. },
  400. currentTypeFilterIdx() {
  401. this.loadOrders()
  402. },
  403. searchContent() {
  404. // 搜索内容变化时,自动重新加载订单
  405. this.loadOrders()
  406. }
  407. },
  408. computed: {
  409. orderList() {
  410. return this.allOrderList;
  411. }
  412. },
  413. methods: {
  414. async loadServiceList() {
  415. try {
  416. const res = await listAllService()
  417. this.serviceList = res.data || []
  418. this.typeFilterOptions = ['全部类型', ...this.serviceList.map(s => s.name)]
  419. } catch (err) {
  420. console.error('获取服务类型失败:', err)
  421. uni.showToast({ title: err.message || err.msg || '获取服务失败', icon: 'none' })
  422. }
  423. },
  424. async loadOrders() {
  425. try {
  426. const statusMap = { 0: 2, 1: 3, 2: 4, 3: 5 }
  427. const serviceId = this.currentTypeFilterIdx > 0 ? this.serviceList[this.currentTypeFilterIdx - 1]?.id : undefined
  428. const params = {
  429. status: statusMap[this.currentTab],
  430. content: this.searchContent || undefined,
  431. service: serviceId,
  432. startServiceTime: this.startServiceTime || undefined,
  433. endServiceTime: this.endServiceTime || undefined
  434. }
  435. console.log('订单列表请求参数:', params)
  436. const res = await getMyOrders(params)
  437. console.log('订单列表响应:', res)
  438. const orders = res.rows || []
  439. console.log('订单数量:', orders.length)
  440. this.allOrderList = orders.map(order => this.transformOrder(order, this.currentTab))
  441. } catch (err) {
  442. console.error('获取订单列表失败:', err)
  443. this.allOrderList = []
  444. uni.showToast({ title: err.message || err.msg || '加载订单失败', icon: 'none' })
  445. }
  446. },
  447. transformOrder(order, tabIndex) {
  448. const service = this.serviceList.find(s => s.id === order.service)
  449. const serviceText = service?.name || '未知'
  450. const serviceIcon = service?.iconUrl || ''
  451. const mode = service?.mode || 0
  452. const isRoundTrip = mode === 1
  453. // 待服务或服务中统一展示为“到达打卡”
  454. let statusText = ''
  455. if (tabIndex === 0 || tabIndex === 1) {
  456. statusText = '到达打卡'
  457. } else if (tabIndex === 2) {
  458. statusText = '已完成'
  459. } else if (tabIndex === 3) {
  460. statusText = '已取消'
  461. }
  462. return {
  463. id: order.id,
  464. status: order.status, // 保存原始 status 用于判断权限
  465. type: isRoundTrip ? 1 : 2,
  466. typeText: serviceText,
  467. typeIcon: serviceIcon,
  468. statusText: statusText,
  469. fulfillmentCommission: (order.fulfillmentCommission / 100).toFixed(2),
  470. timeLabel: '服务时间',
  471. time: order.serviceTime || '',
  472. petAvatar: order.petAvatar || '/static/dog.png',
  473. petAvatarUrl: order.petAvatarUrl || '',
  474. petName: order.petName || '',
  475. petBreed: order.breed || '',
  476. startLocation: order.fromAddress || '暂无起点',
  477. startAddress: order.fromAddress || '',
  478. fromAddress: order.fromAddress || '',
  479. fromLat: order.fromLat,
  480. fromLng: order.fromLng,
  481. startDistance: '0km',
  482. endLocation: (order.customerName || '') + ' ' + (order.customerPhone || ''),
  483. endAddress: order.toAddress || '',
  484. toAddress: order.toAddress || '',
  485. toLat: order.toLat,
  486. toLng: order.toLng,
  487. customerPhone: order.customerPhone || '',
  488. endDistance: '0km',
  489. serviceContent: order.remark || '',
  490. remark: order.remark || '',
  491. serviceFlag: !!order.serviceFlag // 是否允许服务(点击跳转)
  492. }
  493. },
  494. getDisplayStatus(item) {
  495. if (item.status === 4) return '已完成';
  496. if (item.status === 5 || item.status === 6) return '已取消';
  497. if (item.status === 2) return item.type === 1 ? '待接送' : '待服务';
  498. if (item.status === 3) return item.type === 1 ? '配送中' : '服务中';
  499. return item.statusText;
  500. },
  501. getStatusClass(item) {
  502. let display = this.getDisplayStatus(item);
  503. if (display === '已完成') return 'finish';
  504. if (display === '已取消') return 'reject';
  505. if (display === '配送中' || display === '服务中') return 'processing';
  506. return 'highlight';
  507. },
  508. goToDetail(item) {
  509. uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
  510. },
  511. showPetProfile(item) {
  512. this.currentPetInfo = {
  513. ...item,
  514. petGender: 'M',
  515. petAge: '2岁',
  516. petWeight: '15kg',
  517. petPersonality: '活泼亲人,精力旺盛',
  518. petHobby: '喜欢追飞盘,爱吃肉干',
  519. petRemark: '肠胃较弱,不能乱喂零食;出门易爆冲,请拉紧牵引绳。',
  520. petTags: ['拉响警报', '不能吃鸡肉', '精力旺盛'],
  521. petLogs: [
  522. { date: '2026-02-09 14:00', content: '今天遛弯拉了两次粑粑,精神状态很好。', recorder: '王阿姨' },
  523. { date: '2026-02-08 10:30', content: '有些挑食,剩了小半碗狗粮。', recorder: '李师傅' },
  524. { date: '2026-02-05 09:00', content: '建档。', recorder: '系统记录' }
  525. ]
  526. };
  527. this.showPetModal = true;
  528. },
  529. closePetProfile() {
  530. this.showPetModal = false;
  531. },
  532. openNavigation(item, pointType) {
  533. if (!item.serviceFlag) {
  534. uni.showToast({ title: '该订单暂未开通服务,无法操作', icon: 'none' });
  535. return;
  536. }
  537. this.navTargetItem = item;
  538. this.navTargetPointType = pointType;
  539. this.showNavModal = true;
  540. },
  541. closeNavModal() {
  542. this.showNavModal = false;
  543. },
  544. chooseMap(mapType) {
  545. let item = this.navTargetItem;
  546. let pointType = this.navTargetPointType;
  547. let name = pointType === 'start' ? (item.fromAddress || '起点') : (item.toAddress || '终点');
  548. let address = pointType === 'start' ? (item.fromAddress || '起点地址') : (item.toAddress || '终点地址');
  549. let latitude = pointType === 'start' ? Number(item.fromLat) : Number(item.toLat);
  550. let longitude = pointType === 'start' ? Number(item.fromLng) : Number(item.toLng);
  551. this.showNavModal = false;
  552. const openApp = (lat, lng, addrName, addrDesc) => {
  553. let url = '';
  554. // #ifdef APP-PLUS
  555. if (mapType === '高德') {
  556. url = `androidamap://viewMap?sourceApplication=yingpaipay&poiname=${encodeURIComponent(addrName)}&lat=${lat}&lon=${lng}&dev=0`;
  557. } else if (mapType === '腾讯') {
  558. url = `qqmap://map/marker?marker=coord:${lat},${lng};title:${encodeURIComponent(addrName)};addr:${encodeURIComponent(addrDesc)}&referer=yingpaipay`;
  559. } else if (mapType === '百度') {
  560. url = `baidumap://map/marker?location=${lat},${lng}&title=${encodeURIComponent(addrName)}&content=${encodeURIComponent(addrDesc)}&src=yingpaipay`;
  561. }
  562. plus.runtime.openURL(url, (e) => {
  563. console.error('打开' + mapType + '地图失败:', e);
  564. });
  565. // #endif
  566. // #ifdef H5
  567. if (mapType === '高德') {
  568. url = `https://uri.amap.com/marker?position=${lng},${lat}&name=${encodeURIComponent(addrName)}`;
  569. } else if (mapType === '腾讯') {
  570. url = `https://apis.map.qq.com/uri/v1/marker?marker=coord:${lat},${lng};title:${encodeURIComponent(addrName)}&referer=yingpaipay`;
  571. } else if (mapType === '百度') {
  572. url = `https://api.map.baidu.com/marker?location=${lat},${lng}&title=${encodeURIComponent(addrName)}&content=${encodeURIComponent(addrDesc)}&output=html&src=yingpaipay`;
  573. }
  574. window.open(url, '_blank');
  575. // #endif
  576. };
  577. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  578. openApp(latitude, longitude, name, address);
  579. } else {
  580. uni.showLoading({ title: '获取当前位置...', mask: true });
  581. reportGps(true).then(res => {
  582. uni.hideLoading();
  583. openApp(res.latitude, res.longitude, name, address);
  584. }).catch(err => {
  585. uni.hideLoading();
  586. console.error('获取地理位置失败:', err);
  587. });
  588. }
  589. },
  590. toggleCallMenu(item) {
  591. if (this.activeCallItem === item) {
  592. this.activeCallItem = null;
  593. } else {
  594. this.activeCallItem = item;
  595. }
  596. },
  597. closeCallMenu() {
  598. this.activeCallItem = null;
  599. },
  600. doCall(type, item) {
  601. if (!item.serviceFlag) {
  602. uni.showToast({ title: '该订单暂未开通服务,无法操作', icon: 'none' });
  603. return;
  604. }
  605. let phoneNum = '';
  606. const targetItem = item || this.activeCallItem;
  607. // 1. 获取电话号码
  608. if (type === 'merchant') {
  609. phoneNum = '18900008451';
  610. } else if (type === 'customer') {
  611. phoneNum = targetItem?.customerPhone;
  612. }
  613. // 2. 基础校验
  614. if (!phoneNum) {
  615. uni.showToast({ title: '未找到电话号码', icon: 'none' });
  616. this.activeCallItem = null;
  617. return;
  618. }
  619. // 3. 清洗号码 (去除空格、横杠等非数字字符)
  620. phoneNum = phoneNum.replace(/[^\d]/g, '');
  621. // 二次校验:确保清洗后仍有数字
  622. if (phoneNum.length < 3) {
  623. uni.showToast({ title: '电话号码格式错误', icon: 'none' });
  624. this.activeCallItem = null;
  625. return;
  626. }
  627. console.log('正在发起直接呼叫:', phoneNum);
  628. // 4. 核心逻辑:区分环境处理
  629. // #ifdef APP-PLUS
  630. // App 端:使用 uni.makePhoneCall 直接发起呼叫
  631. uni.makePhoneCall({
  632. phoneNumber: phoneNum,
  633. success: () => {
  634. console.log('成功唤起系统拨号盘');
  635. },
  636. fail: (err) => {
  637. console.error('拨号失败:', err);
  638. // 常见错误:Permission denied (权限被拒) 或 Activity not found
  639. let msg = '拨号失败';
  640. if (err.message && err.message.includes('permission')) {
  641. msg = '请在手机设置中允许"电话"权限';
  642. }
  643. uni.showToast({ title: msg, icon: 'none', duration: 3000 });
  644. // 如果失败,尝试引导用户去设置页 (仅限 Android)
  645. // #ifdef APP-ANDROID
  646. if (err.message && err.message.includes('permission')) {
  647. uni.showModal({
  648. title: '权限提示',
  649. content: '拨打电话需要电话权限,是否前往设置开启?',
  650. success: (res) => {
  651. if (res.confirm) {
  652. plus.runtime.openURL("app-settings:");
  653. }
  654. }
  655. });
  656. }
  657. // #endif
  658. },
  659. complete: () => {
  660. this.activeCallItem = null; // 关闭弹窗
  661. }
  662. });
  663. // #endif
  664. // #ifdef H5
  665. // H5 端:使用 tel: 协议
  666. window.location.href = `tel:${phoneNum}`;
  667. this.activeCallItem = null;
  668. // #endif
  669. // #ifdef MP-WEIXIN
  670. // 小程序端:直接调用 makePhoneCall (微信小程序支持直接弹框确认拨打)
  671. uni.makePhoneCall({
  672. phoneNumber: phoneNum,
  673. fail: () => {
  674. uni.showToast({ title: '拨号失败', icon: 'none' });
  675. },
  676. complete: () => {
  677. this.activeCallItem = null;
  678. }
  679. });
  680. // #endif
  681. },
  682. reportAbnormal(item) {
  683. if (!item.serviceFlag) {
  684. uni.showToast({ title: '该订单暂未开通服务,无法操作', icon: 'none' });
  685. return;
  686. }
  687. uni.navigateTo({ url: '/pages/orders/anomaly/index?orderId=' + (item.id || '') });
  688. },
  689. mainAction(item) {
  690. if (!item.serviceFlag) {
  691. uni.showToast({ title: '该订单暂未开通服务,无法操作', icon: 'none' });
  692. return;
  693. }
  694. uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
  695. },
  696. addOrUpdateService(item) {
  697. // 跳转到申诉(增改服务项)页面
  698. uni.navigateTo({ url: `/pages/orders/appeal/index?id=${item.id}` });
  699. },
  700. toggleDropdown(idx) {
  701. if (this.activeDropdown === idx) {
  702. this.activeDropdown = 0;
  703. } else {
  704. this.activeDropdown = idx;
  705. }
  706. },
  707. closeDropdown() {
  708. this.activeDropdown = 0;
  709. },
  710. selectType(index) {
  711. this.currentTypeFilterIdx = index;
  712. this.closeDropdown();
  713. },
  714. initCalendar() {
  715. const year = this.viewDate.getFullYear();
  716. const month = this.viewDate.getMonth();
  717. this.currentMonth = `${year}年${month + 1}月`;
  718. // 获取该月第一天是周几 (0-6)
  719. const firstDay = new Date(year, month, 1).getDay();
  720. // 获取该月有多少天
  721. const daysInMonth = new Date(year, month + 1, 0).getDate();
  722. let days = [];
  723. // 填充开头的空白
  724. for (let i = 0; i < firstDay; i++) {
  725. days.push(0);
  726. }
  727. // 填充真实日期
  728. for (let i = 1; i <= daysInMonth; i++) {
  729. days.push(i);
  730. }
  731. this.calendarDays = days;
  732. },
  733. prevMonth() {
  734. this.viewDate.setMonth(this.viewDate.getMonth() - 1);
  735. // 切换月份时强制重新创建 Date 对象以触发 Vue 响应式(如果需要)或者简单调用 init
  736. this.viewDate = new Date(this.viewDate);
  737. this.initCalendar();
  738. },
  739. nextMonth() {
  740. this.viewDate.setMonth(this.viewDate.getMonth() + 1);
  741. this.viewDate = new Date(this.viewDate);
  742. this.initCalendar();
  743. },
  744. selectDateItem(day) {
  745. if (this.selectedDateRange.length === 2) {
  746. this.selectedDateRange = [day];
  747. } else if (this.selectedDateRange.length === 1) {
  748. let start = this.selectedDateRange[0];
  749. if (day > start) {
  750. this.selectedDateRange = [start, day];
  751. } else if (day < start) {
  752. this.selectedDateRange = [day, start];
  753. } else {
  754. this.selectedDateRange = [];
  755. }
  756. } else {
  757. this.selectedDateRange = [day];
  758. }
  759. },
  760. getDateClass(day) {
  761. if (!day || this.selectedDateRange.length === 0) return '';
  762. if (this.selectedDateRange.length === 1) {
  763. return day === this.selectedDateRange[0] ? 'is-start' : '';
  764. }
  765. let start = this.selectedDateRange[0];
  766. let end = this.selectedDateRange[1];
  767. if (day === start) return 'is-start';
  768. if (day === end) return 'is-end';
  769. if (day > start && day < end) return 'is-between';
  770. return '';
  771. },
  772. resetTimeFilter() {
  773. this.hasTimeFilter = false;
  774. this.selectedDateRange = [];
  775. this.startServiceTime = '';
  776. this.endServiceTime = '';
  777. this.closeDropdown();
  778. this.loadOrders();
  779. },
  780. confirmTimeFilter() {
  781. if (this.selectedDateRange.length === 0) {
  782. uni.showToast({ title: '请先选择日期', icon: 'none' });
  783. return;
  784. }
  785. // 构建时间范围参数
  786. const year = this.currentMonth.replace(/[^0-9]/g, '').substring(0, 4);
  787. const month = this.currentMonth.replace(/[^0-9]/g, '').substring(4);
  788. const pad = (n) => String(n).padStart(2, '0');
  789. if (this.selectedDateRange.length === 2) {
  790. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  791. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[1])} 23:59:59`;
  792. } else {
  793. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  794. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 23:59:59`;
  795. }
  796. this.hasTimeFilter = true;
  797. this.closeDropdown();
  798. this.loadOrders();
  799. },
  800. getMainActionText(item) {
  801. return '查看详情';
  802. },
  803. mainAction(item) {
  804. uni.navigateTo({ url: `/pages/orders/detail/index?id=${item.id}` });
  805. },
  806. addOrUpdateService(item) {
  807. // 跳转到申诉(增改服务项)页面
  808. uni.navigateTo({ url: `/pages/orders/appeal/index?id=${item.id}` });
  809. },
  810. openRemarkInput() {
  811. this.remarkText = '';
  812. this.showRemarkInput = true;
  813. },
  814. closeRemarkInput() {
  815. this.showRemarkInput = false;
  816. this.remarkText = '';
  817. },
  818. submitRemark() {
  819. const text = this.remarkText.trim();
  820. if (!text) {
  821. uni.showToast({ title: '备注内容不能为空', icon: 'none' });
  822. return;
  823. }
  824. const now = new Date();
  825. 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')}`;
  826. if (!this.currentPetInfo.petLogs) {
  827. this.$set(this.currentPetInfo, 'petLogs', []);
  828. }
  829. this.currentPetInfo.petLogs.unshift({
  830. date: dateStr,
  831. content: text,
  832. recorder: '我'
  833. });
  834. uni.showToast({ title: '备注已添加', icon: 'success' });
  835. this.closeRemarkInput();
  836. },
  837. /**
  838. * 取消订单处理逻辑 - 打开弹窗
  839. * @param {Object} item - 订单项
  840. */
  841. handleCancelOrder(item) {
  842. if (!item.serviceFlag) {
  843. uni.showToast({ title: '该订单暂未开通服务,无法操作', icon: 'none' });
  844. return;
  845. }
  846. this.currentOrder = item;
  847. this.cancelReason = '';
  848. this.showCancelModal = true;
  849. },
  850. closeCancelModal() {
  851. this.showCancelModal = false;
  852. this.currentOrder = null;
  853. },
  854. async confirmCancel() {
  855. if (!this.cancelReason.trim()) {
  856. uni.showToast({ title: '请输入取消原因', icon: 'none' });
  857. return;
  858. }
  859. try {
  860. uni.showLoading({ title: '取消中...', mask: true });
  861. await cancelOrderApi({
  862. orderId: this.currentOrder.id,
  863. reason: this.cancelReason
  864. });
  865. uni.showToast({ title: '已取消接单', icon: 'success' });
  866. this.showCancelModal = false;
  867. this.currentOrder = null;
  868. // 延时刷新列表,防止提示框闪现
  869. setTimeout(() => {
  870. this.loadOrders();
  871. }, 1000);
  872. } catch (err) {
  873. console.error('取消订单失败:', err);
  874. uni.showToast({ title: err.message || err.msg || '取消失败', icon: 'none' });
  875. } finally {
  876. uni.hideLoading();
  877. }
  878. },
  879. async viewAppealProgress(item) {
  880. this.appealList = [];
  881. this.appealLoading = true;
  882. this.showAppealModal = true;
  883. try {
  884. const res = await getAppealListByOrderId(item.id);
  885. this.appealList = res.data || [];
  886. } catch (err) {
  887. console.error('获取申诉记录失败:', err);
  888. uni.showToast({ title: '获取申诉进度失败', icon: 'none' });
  889. } finally {
  890. this.appealLoading = false;
  891. }
  892. },
  893. closeAppealModal() {
  894. this.showAppealModal = false;
  895. this.appealList = [];
  896. },
  897. getAppealStatusText(record) {
  898. // auditStatus: 0=待审核, 1=通过, 2=驳回
  899. if (record.auditStatus === 0 || record.auditStatus === null || record.auditStatus === undefined) return '待审核';
  900. if (record.auditStatus === 1) return '已通过';
  901. if (record.auditStatus === 2) return '已驳回';
  902. return '未知';
  903. },
  904. getAppealStatusClass(record) {
  905. if (record.auditStatus === 0 || record.auditStatus === null || record.auditStatus === undefined) return 'pending';
  906. if (record.auditStatus === 1) return 'approved';
  907. if (record.auditStatus === 2) return 'rejected';
  908. return '';
  909. },
  910. formatTime(timeStr) {
  911. if (!timeStr) return '';
  912. // 后端返回格式: 2026-04-23T15:40:00 或 2026-04-23 15:40:00
  913. return timeStr.replace('T', ' ').substring(0, 16);
  914. }
  915. }
  916. }
  917. </script>
  918. <style>
  919. page {
  920. background-color: #F8F8F8;
  921. }
  922. .custom-nav-bar {
  923. padding: 80rpx 30rpx 20rpx;
  924. background-color: #fff;
  925. display: flex;
  926. align-items: center;
  927. justify-content: center;
  928. }
  929. .nav-title {
  930. font-size: 34rpx;
  931. font-weight: bold;
  932. color: #333;
  933. }
  934. .sticky-header {
  935. position: sticky;
  936. top: 0;
  937. z-index: 999;
  938. background-color: #F8F8F8;
  939. }
  940. .container {
  941. background-color: #F8F8F8;
  942. display: flex;
  943. flex-direction: column;
  944. min-height: 100vh;
  945. }
  946. .status-tabs {
  947. display: flex;
  948. background-color: #fff;
  949. padding: 0 30rpx;
  950. justify-content: space-between;
  951. }
  952. .tab-item {
  953. position: relative;
  954. padding: 20rpx 0;
  955. font-size: 26rpx;
  956. color: #666;
  957. font-weight: 500;
  958. }
  959. .tab-item.active {
  960. color: #FF5722;
  961. font-weight: bold;
  962. }
  963. .indicator {
  964. position: absolute;
  965. bottom: 0;
  966. left: 50%;
  967. transform: translateX(-50%);
  968. width: 40rpx;
  969. height: 6rpx;
  970. background-color: #FF5722;
  971. border-radius: 3rpx;
  972. }
  973. .search-bar {
  974. padding: 10rpx 30rpx;
  975. background-color: #fff;
  976. }
  977. .search-input-box {
  978. display: flex;
  979. align-items: center;
  980. background-color: #F8F8F8;
  981. height: 64rpx;
  982. border-radius: 32rpx;
  983. padding: 0 30rpx;
  984. }
  985. .search-input {
  986. flex: 1;
  987. font-size: 26rpx;
  988. color: #333;
  989. padding-left: 20rpx;
  990. }
  991. .ph-style {
  992. font-size: 26rpx;
  993. color: #999;
  994. }
  995. .filter-wrapper {
  996. position: relative;
  997. z-index: 998;
  998. }
  999. .filter-bar {
  1000. display: flex;
  1001. background-color: #fff;
  1002. padding: 5rpx 30rpx 10rpx 30rpx;
  1003. justify-content: space-between;
  1004. position: relative;
  1005. z-index: 998;
  1006. }
  1007. .filter-item {
  1008. width: 48%;
  1009. display: flex;
  1010. align-items: center;
  1011. justify-content: center;
  1012. font-size: 26rpx;
  1013. color: #666;
  1014. background-color: #F8F8F8;
  1015. height: 56rpx;
  1016. border-radius: 12rpx;
  1017. transition: all 0.2s;
  1018. }
  1019. .filter-item.active {
  1020. background-color: #FFF3E0;
  1021. }
  1022. .active-text {
  1023. color: #FF5722;
  1024. font-weight: 500;
  1025. }
  1026. .triangle {
  1027. width: 0;
  1028. height: 0;
  1029. border-left: 8rpx solid transparent;
  1030. border-right: 8rpx solid transparent;
  1031. margin-left: 10rpx;
  1032. transition: all 0.2s;
  1033. }
  1034. .triangle.down {
  1035. border-top: 10rpx solid #dcdcdc;
  1036. }
  1037. .filter-item.active .triangle.down,
  1038. .active-text+.triangle.down {
  1039. border-top-color: #FF5722;
  1040. }
  1041. .triangle.up {
  1042. border-bottom: 10rpx solid #FF5722;
  1043. }
  1044. .dropdown-mask {
  1045. position: absolute;
  1046. top: 100%;
  1047. left: 0;
  1048. right: 0;
  1049. height: 100vh;
  1050. background-color: rgba(0, 0, 0, 0.4);
  1051. z-index: 80;
  1052. }
  1053. .dropdown-panel {
  1054. position: absolute;
  1055. top: 100%;
  1056. left: 0;
  1057. right: 0;
  1058. background-color: #fff;
  1059. z-index: 90;
  1060. border-radius: 0 0 20rpx 20rpx;
  1061. box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.05);
  1062. overflow: hidden;
  1063. }
  1064. .type-option {
  1065. padding: 30rpx 40rpx;
  1066. font-size: 28rpx;
  1067. color: #333;
  1068. border-bottom: 1px solid #f5f5f5;
  1069. }
  1070. .type-option:last-child {
  1071. border-bottom: none;
  1072. }
  1073. .type-option.selected text {
  1074. color: #FF5722;
  1075. font-weight: bold;
  1076. }
  1077. .calendar-panel {
  1078. padding-bottom: 30rpx;
  1079. }
  1080. .custom-calendar-container {
  1081. padding: 20rpx 30rpx 0;
  1082. }
  1083. .cal-header {
  1084. display: flex;
  1085. justify-content: space-between;
  1086. align-items: center;
  1087. padding: 20rpx 0;
  1088. }
  1089. .cal-title {
  1090. font-size: 32rpx;
  1091. font-weight: bold;
  1092. color: #333;
  1093. }
  1094. .cal-weekdays {
  1095. display: flex;
  1096. justify-content: space-around;
  1097. padding: 20rpx 0;
  1098. border-bottom: 1px solid #f5f5f5;
  1099. }
  1100. .wk-item {
  1101. font-size: 24rpx;
  1102. color: #999;
  1103. width: 14.28%;
  1104. text-align: center;
  1105. }
  1106. .cal-body {
  1107. display: flex;
  1108. flex-wrap: wrap;
  1109. padding-top: 20rpx;
  1110. }
  1111. .cal-day-box {
  1112. width: 14.28%;
  1113. height: 80rpx;
  1114. display: flex;
  1115. align-items: center;
  1116. justify-content: center;
  1117. margin-bottom: 10rpx;
  1118. position: relative;
  1119. }
  1120. .cal-day-text {
  1121. width: 64rpx;
  1122. height: 64rpx;
  1123. line-height: 64rpx;
  1124. text-align: center;
  1125. font-size: 28rpx;
  1126. color: #333;
  1127. border-radius: 8rpx;
  1128. position: relative;
  1129. z-index: 2;
  1130. }
  1131. .cal-day-box.is-start .cal-day-text,
  1132. .cal-day-box.is-end .cal-day-text {
  1133. background-color: #FF5722;
  1134. color: #fff;
  1135. font-weight: bold;
  1136. }
  1137. .cal-day-box.is-start::after {
  1138. content: '';
  1139. position: absolute;
  1140. right: 0;
  1141. top: 8rpx;
  1142. bottom: 8rpx;
  1143. width: 50%;
  1144. background-color: #FFF3E0;
  1145. z-index: 1;
  1146. }
  1147. .cal-day-box.is-end::after {
  1148. content: '';
  1149. position: absolute;
  1150. left: 0;
  1151. top: 8rpx;
  1152. bottom: 8rpx;
  1153. width: 50%;
  1154. background-color: #FFF3E0;
  1155. z-index: 1;
  1156. }
  1157. .cal-day-box.is-start.is-end::after {
  1158. display: none;
  1159. }
  1160. .cal-day-box.is-between {
  1161. background-color: #FFF3E0;
  1162. margin-top: 8rpx;
  1163. height: 64rpx;
  1164. margin-bottom: 18rpx;
  1165. }
  1166. .cal-day-box.is-between .cal-day-text {
  1167. color: #FF5722;
  1168. }
  1169. .calendar-actions {
  1170. display: flex;
  1171. justify-content: space-between;
  1172. padding: 0 30rpx;
  1173. margin-top: 20rpx;
  1174. }
  1175. .cal-btn {
  1176. width: 48%;
  1177. height: 70rpx;
  1178. line-height: 70rpx;
  1179. text-align: center;
  1180. border-radius: 10rpx;
  1181. font-size: 28rpx;
  1182. margin: 0;
  1183. }
  1184. .cal-btn.reset {
  1185. background-color: #f5f5f5;
  1186. color: #666;
  1187. }
  1188. .cal-btn.confirm {
  1189. background-color: #FF5722;
  1190. color: #fff;
  1191. }
  1192. .order-list {
  1193. padding: 0 30rpx;
  1194. width: 100%;
  1195. box-sizing: border-box;
  1196. }
  1197. .order-card {
  1198. background-color: #fff;
  1199. border-radius: 24rpx;
  1200. padding: 20rpx 20rpx;
  1201. margin-bottom: 20rpx;
  1202. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
  1203. }
  1204. .order-card:first-child {
  1205. margin-top: 20rpx;
  1206. }
  1207. .card-header {
  1208. display: flex;
  1209. justify-content: space-between;
  1210. align-items: center;
  1211. margin-bottom: 15rpx;
  1212. }
  1213. .type-badge {
  1214. display: flex;
  1215. align-items: center;
  1216. }
  1217. .type-icon {
  1218. width: 44rpx;
  1219. height: 44rpx;
  1220. margin-right: 15rpx;
  1221. background-color: #FFF3E0;
  1222. border-radius: 50%;
  1223. padding: 6rpx;
  1224. box-sizing: border-box;
  1225. }
  1226. .type-text {
  1227. font-size: 30rpx;
  1228. font-weight: bold;
  1229. color: #333;
  1230. }
  1231. .status-badge {
  1232. font-size: 28rpx;
  1233. }
  1234. .status-badge.highlight {
  1235. color: #FF5722;
  1236. }
  1237. .status-badge.processing {
  1238. color: #2196F3;
  1239. }
  1240. .status-badge.finish {
  1241. color: #4CAF50;
  1242. }
  1243. .status-badge.reject {
  1244. color: #9E9E9E;
  1245. }
  1246. .time-row {
  1247. display: flex;
  1248. justify-content: space-between;
  1249. align-items: center;
  1250. margin-bottom: 25rpx;
  1251. }
  1252. .time-row .time-col {
  1253. display: flex;
  1254. align-items: center;
  1255. font-size: 26rpx;
  1256. color: #333;
  1257. }
  1258. .time-row .label {
  1259. color: #666;
  1260. margin-right: 10rpx;
  1261. }
  1262. .fulfillmentCommission {
  1263. font-size: 36rpx;
  1264. font-weight: bold;
  1265. color: #FF5722;
  1266. }
  1267. .pet-card {
  1268. background-color: #FFF8F0;
  1269. border-radius: 16rpx;
  1270. padding: 15rpx 20rpx;
  1271. display: flex;
  1272. align-items: center;
  1273. margin-bottom: 20rpx;
  1274. }
  1275. .pet-avatar {
  1276. width: 80rpx;
  1277. height: 80rpx;
  1278. border-radius: 50%;
  1279. margin-right: 20rpx;
  1280. }
  1281. .pet-info {
  1282. flex: 1;
  1283. display: flex;
  1284. flex-direction: column;
  1285. }
  1286. .pet-name {
  1287. font-size: 28rpx;
  1288. font-weight: bold;
  1289. color: #333;
  1290. margin-bottom: 5rpx;
  1291. }
  1292. .pet-breed {
  1293. font-size: 24rpx;
  1294. color: #999;
  1295. }
  1296. .pet-profile-btn {
  1297. font-size: 24rpx;
  1298. color: #FF9800;
  1299. border: 1px solid #FF9800;
  1300. padding: 6rpx 20rpx;
  1301. border-radius: 50rpx;
  1302. background-color: #fff;
  1303. }
  1304. .route-info {
  1305. margin-bottom: 25rpx;
  1306. }
  1307. .route-item {
  1308. display: flex;
  1309. align-items: flex-start;
  1310. padding-bottom: 12rpx;
  1311. position: relative;
  1312. width: 100%;
  1313. }
  1314. .route-item:not(:last-child) {
  1315. margin-bottom: 5rpx;
  1316. }
  1317. .route-item:last-child {
  1318. padding-bottom: 0;
  1319. margin-bottom: 0;
  1320. }
  1321. .route-line-vertical {
  1322. position: absolute;
  1323. left: 19rpx;
  1324. top: 46rpx;
  1325. bottom: -15rpx;
  1326. border-left: 2rpx dashed #E0E0E0;
  1327. width: 0;
  1328. z-index: 0;
  1329. }
  1330. .icon-circle {
  1331. width: 40rpx;
  1332. height: 40rpx;
  1333. border-radius: 50%;
  1334. color: #fff;
  1335. font-size: 22rpx;
  1336. display: flex;
  1337. align-items: center;
  1338. justify-content: center;
  1339. margin-right: 20rpx;
  1340. flex-shrink: 0;
  1341. font-weight: bold;
  1342. margin-top: 6rpx;
  1343. position: relative;
  1344. z-index: 1;
  1345. }
  1346. .icon-circle.service {
  1347. background-color: #81C784;
  1348. }
  1349. .icon-circle.start {
  1350. background-color: #FFB74D;
  1351. }
  1352. .icon-circle.end {
  1353. background-color: #81C784;
  1354. }
  1355. .address-box {
  1356. flex: 1;
  1357. display: flex;
  1358. flex-direction: column;
  1359. margin-right: 20rpx;
  1360. }
  1361. .addr-title {
  1362. font-size: 28rpx;
  1363. font-weight: bold;
  1364. color: #333;
  1365. margin-bottom: 4rpx;
  1366. }
  1367. .addr-desc {
  1368. font-size: 24rpx;
  1369. color: #999;
  1370. line-height: 1.4;
  1371. }
  1372. .distance-tag {
  1373. display: flex;
  1374. align-items: center;
  1375. justify-content: flex-end;
  1376. flex-shrink: 0;
  1377. min-width: 80rpx;
  1378. }
  1379. .distance-text {
  1380. font-size: 24rpx;
  1381. color: #FF5722;
  1382. margin-right: 15rpx;
  1383. font-weight: 500;
  1384. }
  1385. .nav-icon-circle {
  1386. width: 48rpx;
  1387. height: 48rpx;
  1388. background-color: #FFF3E0;
  1389. border-radius: 50%;
  1390. display: flex;
  1391. align-items: center;
  1392. justify-content: center;
  1393. }
  1394. .nav-arrow {
  1395. width: 24rpx;
  1396. height: 24rpx;
  1397. }
  1398. .service-content {
  1399. margin-top: -10rpx;
  1400. font-size: 24rpx;
  1401. color: #666;
  1402. padding-left: 60rpx;
  1403. }
  1404. .content-label {
  1405. color: #999;
  1406. margin-right: 10rpx;
  1407. }
  1408. .remark-box {
  1409. background-color: #F8F8F8;
  1410. padding: 15rpx 20rpx;
  1411. border-radius: 8rpx;
  1412. font-size: 24rpx;
  1413. color: #666;
  1414. margin-bottom: 20rpx;
  1415. }
  1416. .action-btns {
  1417. display: flex;
  1418. flex-direction: column;
  1419. gap: 16rpx;
  1420. margin-top: 20rpx;
  1421. }
  1422. .action-row {
  1423. display: flex;
  1424. gap: 16rpx;
  1425. align-items: center;
  1426. width: 100%;
  1427. }
  1428. .btn {
  1429. flex: 1;
  1430. height: 64rpx;
  1431. line-height: 64rpx;
  1432. border-radius: 32rpx;
  1433. font-size: 24rpx;
  1434. padding: 0;
  1435. margin: 0;
  1436. text-align: center;
  1437. min-width: 0;
  1438. }
  1439. .action-right .btn:not(:last-child) {
  1440. margin-right: 20rpx;
  1441. }
  1442. .btn::after {
  1443. border: none;
  1444. }
  1445. .btn.normal {
  1446. background-color: #F8F8F8;
  1447. color: #666;
  1448. border: none;
  1449. }
  1450. .btn.primary {
  1451. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1452. color: #fff;
  1453. box-shadow: 0 4rpx 12rpx rgba(255, 87, 34, 0.2);
  1454. border: none;
  1455. }
  1456. .btn.normal.danger {
  1457. background-color: #FFF2F0;
  1458. color: #F5222D;
  1459. }
  1460. .pet-modal-mask {
  1461. position: fixed;
  1462. top: 0;
  1463. left: 0;
  1464. right: 0;
  1465. bottom: 0;
  1466. background-color: rgba(0, 0, 0, 0.4);
  1467. z-index: 1000;
  1468. display: flex;
  1469. align-items: center;
  1470. justify-content: center;
  1471. }
  1472. .pet-modal-content {
  1473. width: 680rpx;
  1474. height: 85vh;
  1475. background-color: #fff;
  1476. border-radius: 20rpx;
  1477. display: flex;
  1478. flex-direction: column;
  1479. overflow: hidden;
  1480. }
  1481. .pet-modal-header {
  1482. display: flex;
  1483. align-items: center;
  1484. justify-content: space-between;
  1485. padding: 30rpx;
  1486. border-bottom: 1rpx solid #F0F0F0;
  1487. }
  1488. .pet-modal-title {
  1489. font-size: 34rpx;
  1490. font-weight: bold;
  1491. color: #333;
  1492. }
  1493. .pet-modal-scroll {
  1494. flex: 1;
  1495. height: 0;
  1496. padding: 30rpx;
  1497. box-sizing: border-box;
  1498. }
  1499. .pet-base-info {
  1500. display: flex;
  1501. align-items: center;
  1502. margin-bottom: 40rpx;
  1503. }
  1504. .pm-avatar {
  1505. width: 120rpx;
  1506. height: 120rpx;
  1507. border-radius: 50%;
  1508. margin-right: 30rpx;
  1509. border: 2rpx solid #f5f5f5;
  1510. }
  1511. .pm-info-text {
  1512. flex: 1;
  1513. display: flex;
  1514. flex-direction: column;
  1515. }
  1516. .pm-name-row {
  1517. display: flex;
  1518. align-items: center;
  1519. margin-bottom: 15rpx;
  1520. }
  1521. .pm-name {
  1522. font-size: 36rpx;
  1523. font-weight: bold;
  1524. color: #333;
  1525. margin-right: 20rpx;
  1526. }
  1527. .pm-gender {
  1528. display: flex;
  1529. align-items: center;
  1530. background-color: #E3F2FD;
  1531. padding: 4rpx 12rpx;
  1532. border-radius: 20rpx;
  1533. }
  1534. .pm-gender text {
  1535. font-size: 22rpx;
  1536. color: #1E88E5;
  1537. }
  1538. .pm-gender .gender-icon {
  1539. font-weight: bold;
  1540. margin-right: 4rpx;
  1541. }
  1542. .pm-gender.female {
  1543. background-color: #FCE4EC;
  1544. }
  1545. .pm-gender.female text {
  1546. color: #D81B60;
  1547. }
  1548. .pm-breed {
  1549. font-size: 26rpx;
  1550. color: #999;
  1551. }
  1552. .pm-detail-grid {
  1553. display: flex;
  1554. flex-wrap: wrap;
  1555. justify-content: space-between;
  1556. }
  1557. .pm-grid-item {
  1558. background-color: #F8F8F8;
  1559. border-radius: 16rpx;
  1560. padding: 24rpx;
  1561. margin-bottom: 20rpx;
  1562. display: flex;
  1563. flex-direction: column;
  1564. }
  1565. .pm-grid-item.half {
  1566. width: 48%;
  1567. box-sizing: border-box;
  1568. }
  1569. .pm-grid-item.full {
  1570. width: 100%;
  1571. box-sizing: border-box;
  1572. }
  1573. .pm-label {
  1574. font-size: 24rpx;
  1575. color: #999;
  1576. margin-bottom: 10rpx;
  1577. }
  1578. .pm-val {
  1579. font-size: 28rpx;
  1580. color: #333;
  1581. font-weight: 500;
  1582. }
  1583. .pm-tags {
  1584. display: flex;
  1585. flex-wrap: wrap;
  1586. gap: 20rpx;
  1587. margin-bottom: 40rpx;
  1588. }
  1589. .pm-tag {
  1590. background-color: #FFF8EB;
  1591. border: 2rpx solid #FFCC80;
  1592. color: #FF9800;
  1593. font-size: 22rpx;
  1594. padding: 8rpx 24rpx;
  1595. border-radius: 30rpx;
  1596. }
  1597. .pm-section-title {
  1598. display: flex;
  1599. align-items: center;
  1600. margin-bottom: 30rpx;
  1601. padding-top: 30rpx;
  1602. border-top: 2rpx dashed #F0F0F0;
  1603. }
  1604. .pm-section-title .orange-bar {
  1605. width: 8rpx;
  1606. height: 32rpx;
  1607. background-color: #FF9800;
  1608. margin-right: 16rpx;
  1609. border-radius: 4rpx;
  1610. }
  1611. .pm-section-title text {
  1612. font-size: 30rpx;
  1613. font-weight: bold;
  1614. color: #333;
  1615. }
  1616. .pm-log-list {
  1617. display: flex;
  1618. flex-direction: column;
  1619. }
  1620. .pm-log-item {
  1621. display: flex;
  1622. flex-direction: column;
  1623. padding: 24rpx 0;
  1624. border-bottom: 1rpx solid #F0F0F0;
  1625. }
  1626. .pm-log-item:last-child {
  1627. border-bottom: none;
  1628. }
  1629. .pm-log-date {
  1630. font-size: 24rpx;
  1631. color: #999;
  1632. margin-bottom: 16rpx;
  1633. }
  1634. .pm-log-text {
  1635. font-size: 28rpx;
  1636. color: #333;
  1637. line-height: 1.6;
  1638. margin-bottom: 20rpx;
  1639. }
  1640. .pm-log-recorder {
  1641. font-size: 24rpx;
  1642. color: #FF9800;
  1643. align-self: flex-end;
  1644. }
  1645. .pm-bottom-close {
  1646. width: 100%;
  1647. height: 80rpx;
  1648. line-height: 80rpx;
  1649. background-color: #F5F5F5;
  1650. color: #666;
  1651. border-radius: 40rpx;
  1652. font-size: 30rpx;
  1653. font-weight: bold;
  1654. margin: 0;
  1655. }
  1656. .pm-bottom-close::after {
  1657. border: none;
  1658. }
  1659. .close-icon-btn {
  1660. font-size: 48rpx;
  1661. color: #999;
  1662. line-height: 1;
  1663. padding: 0 10rpx;
  1664. }
  1665. .nav-modal-mask {
  1666. position: fixed;
  1667. top: 0;
  1668. left: 0;
  1669. right: 0;
  1670. bottom: 0;
  1671. background-color: rgba(0, 0, 0, 0.5);
  1672. z-index: 1000;
  1673. display: flex;
  1674. flex-direction: column;
  1675. justify-content: flex-end;
  1676. }
  1677. .nav-action-sheet {
  1678. background-color: #fff;
  1679. width: 100%;
  1680. border-top-left-radius: 24rpx;
  1681. border-top-right-radius: 24rpx;
  1682. overflow: hidden;
  1683. padding-bottom: constant(safe-area-inset-bottom);
  1684. padding-bottom: env(safe-area-inset-bottom);
  1685. }
  1686. .nav-sheet-title {
  1687. text-align: center;
  1688. padding: 30rpx 0;
  1689. font-size: 13px;
  1690. color: #999;
  1691. border-bottom: 1rpx solid #efefef;
  1692. }
  1693. .nav-sheet-item {
  1694. text-align: center;
  1695. padding: 30rpx 0;
  1696. font-size: 13px;
  1697. color: #333;
  1698. background-color: #fff;
  1699. border-bottom: 1rpx solid #efefef;
  1700. }
  1701. .nav-sheet-item.cancel {
  1702. border-bottom: none;
  1703. color: #666;
  1704. }
  1705. .nav-sheet-gap {
  1706. height: 16rpx;
  1707. background-color: #F8F8F8;
  1708. }
  1709. .order-list {
  1710. flex: 1;
  1711. overflow-y: auto;
  1712. width: 100%;
  1713. padding: 0 30rpx;
  1714. box-sizing: border-box;
  1715. }
  1716. .loading-text {
  1717. text-align: center;
  1718. font-size: 24rpx;
  1719. color: #999;
  1720. padding: 30rpx 0;
  1721. }
  1722. .pm-header-actions {
  1723. display: flex;
  1724. align-items: center;
  1725. gap: 16rpx;
  1726. }
  1727. .pm-remark-btn {
  1728. font-size: 24rpx;
  1729. color: #fff;
  1730. background-color: #FF9800;
  1731. padding: 6rpx 18rpx;
  1732. border-radius: 20rpx;
  1733. }
  1734. .remark-mask {
  1735. position: fixed;
  1736. top: 0;
  1737. left: 0;
  1738. right: 0;
  1739. bottom: 0;
  1740. background-color: rgba(0, 0, 0, 0.5);
  1741. z-index: 3000;
  1742. display: flex;
  1743. align-items: center;
  1744. justify-content: center;
  1745. }
  1746. .remark-sheet {
  1747. width: 600rpx;
  1748. background-color: #fff;
  1749. border-radius: 24rpx;
  1750. padding: 40rpx;
  1751. box-sizing: border-box;
  1752. display: flex;
  1753. flex-direction: column;
  1754. align-items: center;
  1755. }
  1756. .remark-sheet-header {
  1757. width: 100%;
  1758. text-align: center;
  1759. margin-bottom: 30rpx;
  1760. position: relative;
  1761. }
  1762. .remark-sheet-header .close-icon-btn {
  1763. position: absolute;
  1764. right: 0;
  1765. top: 50%;
  1766. transform: translateY(-50%);
  1767. }
  1768. .remark-sheet-title {
  1769. font-size: 32rpx;
  1770. font-weight: bold;
  1771. color: #333;
  1772. }
  1773. .remark-textarea {
  1774. width: 100%;
  1775. height: 160rpx;
  1776. border: 1rpx solid #eee;
  1777. border-radius: 12rpx;
  1778. padding: 20rpx;
  1779. font-size: 28rpx;
  1780. color: #333;
  1781. box-sizing: border-box;
  1782. margin-bottom: 40rpx;
  1783. }
  1784. .remark-submit-btn {
  1785. width: 100%;
  1786. background-color: #FF5722;
  1787. color: #fff;
  1788. font-size: 32rpx;
  1789. font-weight: bold;
  1790. text-align: center;
  1791. padding: 24rpx 0;
  1792. border-radius: 16rpx;
  1793. }
  1794. .action-left {
  1795. position: relative;
  1796. z-index: 10;
  1797. }
  1798. .action-left .btn.normal {
  1799. font-size: 26rpx;
  1800. }
  1801. .call-popover {
  1802. position: absolute;
  1803. top: calc(100% + 10rpx);
  1804. left: 0;
  1805. background-color: #fff;
  1806. border-radius: 12rpx;
  1807. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  1808. z-index: 999;
  1809. display: flex;
  1810. flex-direction: column;
  1811. width: 200rpx;
  1812. }
  1813. .call-pop-item {
  1814. font-size: 26rpx;
  1815. color: #333;
  1816. text-align: center;
  1817. padding: 24rpx 0;
  1818. border-bottom: 1rpx solid #eee;
  1819. }
  1820. .call-pop-item:last-child {
  1821. border-bottom: none;
  1822. }
  1823. .call-mask {
  1824. position: fixed;
  1825. top: 0;
  1826. left: 0;
  1827. right: 0;
  1828. bottom: 0;
  1829. z-index: 900;
  1830. background: transparent;
  1831. }
  1832. /* 全局通用对话框样式 (复用首页思路) */
  1833. .modal-mask {
  1834. position: fixed;
  1835. top: 0;
  1836. left: 0;
  1837. right: 0;
  1838. bottom: 0;
  1839. background-color: rgba(0, 0, 0, 0.5);
  1840. z-index: 5000;
  1841. display: flex;
  1842. align-items: center;
  1843. justify-content: center;
  1844. }
  1845. .custom-modal {
  1846. width: 600rpx;
  1847. background-color: #fff;
  1848. border-radius: 24rpx;
  1849. padding: 40rpx 50rpx;
  1850. display: flex;
  1851. flex-direction: column;
  1852. align-items: center;
  1853. }
  1854. .modal-title {
  1855. font-size: 36rpx;
  1856. font-weight: bold;
  1857. color: #333;
  1858. margin-bottom: 30rpx;
  1859. }
  1860. .modal-btns {
  1861. width: 100%;
  1862. display: flex;
  1863. justify-content: space-between;
  1864. }
  1865. .modal-btn {
  1866. width: 45%;
  1867. height: 80rpx;
  1868. line-height: 80rpx;
  1869. border-radius: 40rpx;
  1870. font-size: 30rpx;
  1871. font-weight: bold;
  1872. margin: 0;
  1873. }
  1874. .modal-btn::after {
  1875. border: none;
  1876. }
  1877. .modal-btn.cancel {
  1878. background-color: #F5F5F5;
  1879. color: #999;
  1880. }
  1881. .modal-btn.confirm {
  1882. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1883. color: #fff;
  1884. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.3);
  1885. }
  1886. .textarea-container {
  1887. padding: 0 4rpx;
  1888. width: 100%;
  1889. position: relative;
  1890. margin-bottom: 20rpx;
  1891. }
  1892. .reject-textarea {
  1893. width: 100%;
  1894. height: 240rpx;
  1895. background-color: #F9F9F9;
  1896. border: 1rpx solid #E0E0E0;
  1897. border-radius: 16rpx;
  1898. padding: 24rpx;
  1899. padding-bottom: 60rpx;
  1900. font-size: 28rpx;
  1901. line-height: 1.6;
  1902. box-sizing: border-box;
  1903. transition: all 0.3s;
  1904. }
  1905. .reject-textarea:focus {
  1906. border-color: #FF9800;
  1907. background-color: #fff;
  1908. }
  1909. .char-count {
  1910. position: absolute;
  1911. right: 44rpx;
  1912. bottom: 24rpx;
  1913. font-size: 22rpx;
  1914. color: #999;
  1915. }
  1916. .modal-btn.confirm.disabled {
  1917. background: #FFD180;
  1918. box-shadow: none;
  1919. opacity: 0.8;
  1920. }
  1921. .mt-30 {
  1922. margin-top: 30rpx;
  1923. }
  1924. .disabled-card .card-header,
  1925. .disabled-card .card-body {
  1926. opacity: 0.6;
  1927. filter: grayscale(80%);
  1928. }
  1929. .service-disabled-tip {
  1930. display: flex;
  1931. align-items: center;
  1932. justify-content: center;
  1933. padding: 20rpx 0;
  1934. background-color: #FFF8E1;
  1935. border-radius: 12rpx;
  1936. border: 2rpx dashed #FFCC00;
  1937. }
  1938. .service-disabled-tip text {
  1939. font-size: 24rpx;
  1940. color: #F57C00;
  1941. }
  1942. .appeal-progress-btn {
  1943. width: 100% !important;
  1944. }
  1945. /* 申诉进度弹窗 */
  1946. .appeal-modal {
  1947. max-height: 80vh;
  1948. display: flex;
  1949. flex-direction: column;
  1950. position: relative;
  1951. }
  1952. .appeal-title-bar {
  1953. width: 100%;
  1954. display: flex;
  1955. align-items: center;
  1956. justify-content: center;
  1957. position: relative;
  1958. margin-bottom: 20rpx;
  1959. }
  1960. .appeal-title-bar .modal-title {
  1961. margin-bottom: 0;
  1962. }
  1963. .appeal-close-btn {
  1964. position: absolute;
  1965. right: -10rpx;
  1966. top: 50%;
  1967. transform: translateY(-50%);
  1968. width: 56rpx;
  1969. height: 56rpx;
  1970. display: flex;
  1971. align-items: center;
  1972. justify-content: center;
  1973. border-radius: 50%;
  1974. background-color: #f5f5f5;
  1975. }
  1976. .appeal-close-btn:active {
  1977. background-color: #e8e8e8;
  1978. }
  1979. .close-icon {
  1980. font-size: 28rpx;
  1981. color: #999;
  1982. line-height: 1;
  1983. }
  1984. .appeal-scroll {
  1985. max-height: 60vh;
  1986. padding: 10rpx 0;
  1987. }
  1988. .appeal-empty {
  1989. padding: 80rpx 0;
  1990. text-align: center;
  1991. color: #999;
  1992. font-size: 26rpx;
  1993. }
  1994. .appeal-timeline {
  1995. padding: 0 10rpx;
  1996. }
  1997. .appeal-item {
  1998. position: relative;
  1999. padding-left: 40rpx;
  2000. padding-bottom: 30rpx;
  2001. }
  2002. .timeline-dot {
  2003. position: absolute;
  2004. left: 4rpx;
  2005. top: 12rpx;
  2006. width: 20rpx;
  2007. height: 20rpx;
  2008. border-radius: 50%;
  2009. background-color: #e6a23c;
  2010. border: 4rpx solid #faecd8;
  2011. }
  2012. .timeline-dot.approved {
  2013. background-color: #67c23a;
  2014. border-color: #e1f3d8;
  2015. }
  2016. .timeline-dot.rejected {
  2017. background-color: #f56c6c;
  2018. border-color: #fde2e2;
  2019. }
  2020. .timeline-line {
  2021. position: absolute;
  2022. left: 13rpx;
  2023. top: 36rpx;
  2024. bottom: 0;
  2025. width: 2rpx;
  2026. background-color: #e4e7ed;
  2027. }
  2028. .appeal-card {
  2029. background: #f8f8f8;
  2030. border-radius: 16rpx;
  2031. padding: 20rpx 24rpx;
  2032. }
  2033. .appeal-header {
  2034. display: flex;
  2035. justify-content: space-between;
  2036. align-items: center;
  2037. margin-bottom: 12rpx;
  2038. }
  2039. .appeal-service {
  2040. font-size: 28rpx;
  2041. font-weight: bold;
  2042. color: #333;
  2043. }
  2044. .appeal-status {
  2045. font-size: 22rpx;
  2046. padding: 4rpx 16rpx;
  2047. border-radius: 20rpx;
  2048. font-weight: bold;
  2049. }
  2050. .appeal-status.pending {
  2051. color: #e6a23c;
  2052. background-color: #faecd8;
  2053. }
  2054. .appeal-status.approved {
  2055. color: #67c23a;
  2056. background-color: #e1f3d8;
  2057. }
  2058. .appeal-status.rejected {
  2059. color: #f56c6c;
  2060. background-color: #fde2e2;
  2061. }
  2062. .appeal-row {
  2063. display: flex;
  2064. margin-bottom: 8rpx;
  2065. font-size: 24rpx;
  2066. }
  2067. .appeal-label {
  2068. color: #999;
  2069. flex-shrink: 0;
  2070. }
  2071. .appeal-value {
  2072. color: #333;
  2073. flex: 1;
  2074. word-break: break-all;
  2075. }
  2076. .appeal-value.reject-reason {
  2077. color: #f56c6c;
  2078. }
  2079. .appeal-time-row {
  2080. display: flex;
  2081. gap: 24rpx;
  2082. margin-top: 12rpx;
  2083. padding-top: 12rpx;
  2084. border-top: 2rpx solid #eee;
  2085. }
  2086. .appeal-time {
  2087. font-size: 22rpx;
  2088. color: #bbb;
  2089. }
  2090. </style>