index.vue 65 KB

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