index.vue 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637
  1. <template>
  2. <view class="detail-container">
  3. <!-- 加载动画 -->
  4. <view class="loading-container" v-if="pageLoading">
  5. <!-- 顶部骨架 -->
  6. <view class="skeleton-header">
  7. <view class="skeleton-line skeleton-ani" style="width: 30%; height: 36rpx;"></view>
  8. <view class="skeleton-line skeleton-ani" style="width: 20%; height: 36rpx;"></view>
  9. </view>
  10. <view class="skeleton-progress">
  11. <view class="skeleton-circle skeleton-ani" v-for="i in 4" :key="i"></view>
  12. </view>
  13. <!-- 卡片骨架 -->
  14. <view class="skeleton-card" v-for="j in 3" :key="'c' + j">
  15. <view class="skeleton-line skeleton-ani" style="width: 60%; height: 28rpx; margin-bottom: 20rpx;">
  16. </view>
  17. <view class="skeleton-line skeleton-ani" style="width: 90%; height: 24rpx; margin-bottom: 14rpx;">
  18. </view>
  19. <view class="skeleton-line skeleton-ani" style="width: 75%; height: 24rpx;"></view>
  20. </view>
  21. </view>
  22. <template v-else>
  23. <!-- 顶部动态状态区 -->
  24. <view class="detail-header">
  25. <view class="status-row">
  26. <text class="status-title">{{ displayStatusText }}</text>
  27. <text class="status-price">¥{{ orderDetail.fulfillmentCommission }}</text>
  28. </view>
  29. <!-- 进度条区域 -->
  30. <view class="progress-bar">
  31. <view class="step-item" v-for="(step, index) in progressSteps" :key="index"
  32. :class="{ 'active': index === progressIndex, 'done': index < progressIndex }">
  33. <view class="step-circle-wrapper">
  34. <!-- Connecting Line before circle, except first item -->
  35. <view class="step-line" v-if="index !== 0"
  36. :class="{ 'active-line': index <= progressIndex }">
  37. </view>
  38. <view class="step-circle">{{ index + 1 }}</view>
  39. </view>
  40. <text class="step-text">{{ step }}</text>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 内容区域 -->
  45. <scroll-view scroll-y class="detail-content">
  46. <!-- 宠物信息 -->
  47. <view class="white-card pet-bar">
  48. <image class="pb-avatar" :src="orderDetail.petAvatar" mode="aspectFill"></image>
  49. <view class="pb-info">
  50. <view class="pb-name-row">
  51. <text class="pb-name">{{ orderDetail.petName }}</text>
  52. </view>
  53. <view class="pb-tags">
  54. <text class="pb-tag">{{ orderDetail.serviceName }}</text>
  55. </view>
  56. </view>
  57. <view class="pb-actions">
  58. <view class="pb-btn profile-btn" @click="showPetProfile">宠物档案</view>
  59. <view class="pb-btn phone-btn" @click="callPhone">
  60. <image class="phone-icon" src="/static/icons/phone_orange.svg"></image>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- 路线及服务信息 -->
  65. <view class="white-card service-info-card">
  66. <view class="si-row time-row">
  67. <image class="si-icon outline" src="/static/icons/clock.svg"></image>
  68. <view class="si-content">
  69. <text class="si-label">服务时间</text>
  70. <text class="si-val">{{ orderDetail.time }}</text>
  71. </view>
  72. <view class="si-action record-btn" @tap.stop="openAnomalyModal">
  73. <text>异常记录</text>
  74. <image class="record-arrow" src="/static/icons/right_arrow_orange.svg"></image>
  75. </view>
  76. </view>
  77. <!-- 接送类型的地址展现 -->
  78. <template v-if="orderDetail.type === 1">
  79. <view class="si-row addr-row start-addr">
  80. <view class="icon-circle start">起</view>
  81. <view class="route-line-vertical"></view>
  82. <view class="si-content">
  83. <text class="si-addr-title">{{ orderDetail.startLocation }}</text>
  84. <text class="si-addr-desc">{{ orderDetail.startAddress }}</text>
  85. </view>
  86. <view class="nav-btn-circle" @click="openNavigation('start')">
  87. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"></image>
  88. </view>
  89. </view>
  90. <view class="si-row addr-row end-addr">
  91. <view class="icon-circle end">终</view>
  92. <view class="si-content">
  93. <text class="si-addr-title">{{ orderDetail.endLocation }}</text>
  94. <text class="si-addr-desc">{{ orderDetail.endAddress }}</text>
  95. </view>
  96. <view class="nav-btn-circle" @click="openNavigation('end')">
  97. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"></image>
  98. </view>
  99. </view>
  100. </template>
  101. <!-- 喂遛/洗护类型的地址展现 -->
  102. <template v-else>
  103. <view class="si-row addr-row end-addr">
  104. <view class="icon-circle service">服</view>
  105. <view class="si-content">
  106. <text class="si-addr-title">{{ orderDetail.endLocation }}</text>
  107. <text class="si-addr-desc">{{ orderDetail.endAddress }}</text>
  108. </view>
  109. <view class="nav-btn-circle" @click="openNavigation('end')">
  110. <image class="nav-arrow" src="/static/icons/nav_arrow.svg"></image>
  111. </view>
  112. </view>
  113. </template>
  114. <view class="si-row">
  115. <image class="si-icon outline custom-icon-file" src="/static/icons/file.svg"></image>
  116. <view class="si-content">
  117. <text class="si-label">备注</text>
  118. <text class="si-val">{{ orderDetail.remark || '-' }}</text>
  119. </view>
  120. </view>
  121. <!-- <view class="si-row record-history-row" @tap.stop="openAnomalyModal">
  122. <view class="si-content">
  123. <text class="si-label">异常记录</text>
  124. <text class="si-val">查看历史异常记录</text>
  125. </view>
  126. <image class="record-arrow" src="/static/icons/right_arrow_orange.svg"></image>
  127. </view> -->
  128. </view>
  129. <!-- 打卡任务 -->
  130. <view class="white-card task-card" v-if="currentStep < steps.length">
  131. <text class="tc-title">当前任务:{{ currentTaskTitle }}</text>
  132. <text class="tc-desc">{{ currentTaskDesc }}</text>
  133. <!-- 单个巨大点击区触发弹窗 -->
  134. <view class="full-media-add" @click="openUploadModal">
  135. <image class="upload-icon-large" src="/static/icons/camera_grey.svg"></image>
  136. <text class="upload-text-large">上传图或视频</text>
  137. </view>
  138. </view>
  139. <!-- 订单属性 -->
  140. <view class="white-card base-info-card">
  141. <view class="bi-row">
  142. <image class="si-icon outline" src="/static/icons/order_no.svg"></image>
  143. <view class="bi-content">
  144. <text class="bi-label">订单编号</text>
  145. <view class="bi-val-row">
  146. <text class="bi-val">{{ orderDetail.orderNo }}</text>
  147. <text class="bi-copy" @click="copyOrderNo">复制</text>
  148. </view>
  149. </view>
  150. </view>
  151. <view class="bi-row">
  152. <image class="si-icon outline" src="/static/icons/clock.svg"></image>
  153. <view class="bi-content">
  154. <text class="bi-label">下单时间</text>
  155. <text class="bi-val">{{ orderDetail.createTime }}</text>
  156. </view>
  157. </view>
  158. </view>
  159. <!-- 订单进度 -->
  160. <view class="white-card timeline-card">
  161. <view class="tl-title-row">
  162. <view class="orange-bar"></view>
  163. <text class="tl-title">订单进度</text>
  164. </view>
  165. <view class="tl-list">
  166. <view class="tl-item" v-for="(log, idx) in orderDetail.progressLogs" :key="idx">
  167. <view class="tl-marker active">
  168. <view class="tl-dot-inner"></view>
  169. </view>
  170. <view class="tl-content-row">
  171. <view class="tl-header">
  172. <text class="tl-status">{{ log.status }}</text>
  173. <text class="tl-time">{{ log.time }}</text>
  174. </view>
  175. <!-- 媒体(图片/视频)展示 -->
  176. <view class="tl-medias" v-if="log.medias && log.medias.length > 0">
  177. <view class="tl-media-item" v-for="(media, midx) in log.medias" :key="midx"
  178. @click="previewMedia(log.medias, midx)">
  179. <image v-if="!isVideo(media)" class="tl-img" :src="media" mode="aspectFill">
  180. </image>
  181. <view v-else class="tl-video-placeholder">
  182. <view class="tl-play-icon">
  183. </view>
  184. <text class="tl-video-label">视频</text>
  185. </view>
  186. </view>
  187. </view>
  188. <!-- 备注展示 -->
  189. <view class="tl-remark" v-if="log.remark">
  190. <text>{{ log.remark }}</text>
  191. </view>
  192. </view>
  193. </view>
  194. </view>
  195. </view>
  196. <view style="height: 140rpx;"></view>
  197. </scroll-view>
  198. <!-- 底部操作栏 -->
  199. <view class="bottom-action-bar">
  200. <view class="action-left">
  201. <button class="action-btn outline grey-outline" @click="goToAnomaly">异常上报</button>
  202. <button class="action-btn outline orange-outline" @click="openSumModal"
  203. v-if="serviceMode === 0">宠护小结</button>
  204. </view>
  205. <view class="action-right">
  206. <button class="action-btn primary" @click="openUploadModal" v-if="currentStep < steps.length">{{
  207. currentTaskTitle }}</button>
  208. <button class="action-btn primary grey-bg" v-else>已完成</button>
  209. </view>
  210. </view>
  211. <!-- 宠物档案弹窗 -->
  212. <view class="pet-modal-mask" v-if="showPetModal" @click="closePetProfile">
  213. <view class="pet-modal-content" @click.stop>
  214. <!-- 头部 -->
  215. <view class="pet-modal-header">
  216. <text class="pet-modal-title">宠物档案</text>
  217. <view style="flex:1"></view>
  218. <view class="pm-remark-btn" @click="openPetRemarkInput">备注</view>
  219. <view class="close-icon-btn" @click="closePetProfile">×</view>
  220. </view>
  221. <scroll-view scroll-y class="pet-modal-scroll">
  222. <!-- 宠物基础信息 -->
  223. <view class="pet-base-info">
  224. <image class="pm-avatar" :src="currentPetInfo.petAvatar" mode="aspectFill"></image>
  225. <view class="pm-info-text">
  226. <view class="pm-name-row">
  227. <text class="pm-name">{{ currentPetInfo.petName }}</text>
  228. <view class="pm-gender" v-if="currentPetInfo.petGender === 'M'">
  229. <text class="gender-icon">♂</text>
  230. <text>公</text>
  231. </view>
  232. <view class="pm-gender female" v-else-if="currentPetInfo.petGender === 'F'">
  233. <text class="gender-icon">♀</text>
  234. <text>母</text>
  235. </view>
  236. </view>
  237. <text class="pm-breed">品种:{{ currentPetInfo.petBreed }}</text>
  238. </view>
  239. </view>
  240. <!-- 属性网格 -->
  241. <view class="pm-detail-grid">
  242. <view class="pm-grid-item half">
  243. <text class="pm-label">年龄</text>
  244. <text class="pm-val">{{ currentPetInfo.petAge || '未知' }}</text>
  245. </view>
  246. <view class="pm-grid-item half">
  247. <text class="pm-label">体重</text>
  248. <text class="pm-val">{{ currentPetInfo.petWeight || '未知' }}</text>
  249. </view>
  250. <view class="pm-grid-item full">
  251. <text class="pm-label">性格</text>
  252. <text class="pm-val">{{ currentPetInfo.petPersonality || '无' }}</text>
  253. </view>
  254. <view class="pm-grid-item full">
  255. <text class="pm-label">备注</text>
  256. <text class="pm-val">{{ currentPetInfo.petRemark || '无特殊过敏史' }}</text>
  257. </view>
  258. </view>
  259. <!-- 标签 -->
  260. <view class="pm-tags-row" v-if="currentPetInfo.petTags && currentPetInfo.petTags.length > 0">
  261. <view class="pm-tag-chip" v-for="(tag, ti) in currentPetInfo.petTags" :key="ti">
  262. <text class="pm-tag-chip-text">{{ tag }}</text>
  263. </view>
  264. </view>
  265. <!-- 备注日志 -->
  266. <view class="pm-log-section">
  267. <view class="pm-log-header">
  268. <view
  269. style="width:6rpx; height:28rpx; background:#FF9800; border-radius:3rpx; margin-right:12rpx;">
  270. </view>
  271. <text class="pm-log-section-title">备注日志</text>
  272. </view>
  273. <view class="pm-log-item" v-for="(log, lIndex) in currentPetInfo.petLogs" :key="lIndex">
  274. <text class="pm-log-date">{{ log.date }}</text>
  275. <text class="pm-log-text">{{ log.content }}</text>
  276. <text class="pm-log-recorder" v-if="log.recorder !== '系统记录'">记录人:{{ log.recorder
  277. }}</text>
  278. <text class="pm-log-recorder system" v-else>系统记录</text>
  279. </view>
  280. </view>
  281. <view style="height: 30rpx;"></view>
  282. </scroll-view>
  283. </view>
  284. </view>
  285. <!-- 宠物备注输入弹窗 -->
  286. <view class="upload-modal-mask" v-if="showPetRemarkInput" @click="closePetRemarkInput">
  287. <view class="upload-modal-content" @click.stop>
  288. <view class="um-header">
  289. <text class="um-title">添加备注</text>
  290. </view>
  291. <view class="um-body">
  292. <textarea class="um-textarea" v-model="petRemarkText" auto-height placeholder="请输入宠物备注内容..."
  293. placeholder-style="color:#ccc; font-size:26rpx;"></textarea>
  294. </view>
  295. <view class="um-footer">
  296. <button class="um-submit-btn active" @click="submitPetRemark">确认提交</button>
  297. </view>
  298. </view>
  299. </view>
  300. <!-- 选择地图导航弹窗 (复用) -->
  301. <view class="nav-modal-mask" v-if="showNavModal" @click="closeNavModal">
  302. <view class="nav-action-sheet" @click.stop>
  303. <view class="nav-sheet-title">选择地图进行导航</view>
  304. <view class="nav-sheet-item" @click="chooseMap('高德')">高德地图</view>
  305. <view class="nav-sheet-item" @click="chooseMap('腾讯')">腾讯地图</view>
  306. <view class="nav-sheet-item" @click="chooseMap('百度')">百度地图</view>
  307. <view class="nav-sheet-gap"></view>
  308. <view class="nav-sheet-item cancel" @click="closeNavModal">取消</view>
  309. </view>
  310. </view>
  311. <!-- 上传图视频弹窗 (新增) -->
  312. <view class="upload-modal-mask" v-if="showUploadModal" @click="closeUploadModal">
  313. <view class="upload-modal-content" @click.stop>
  314. <view class="um-header">
  315. <text class="um-title">上传图或视频 ({{ modalMediaList.length }}/6)</text>
  316. <text class="um-remark-hint">{{ currentTaskDesc }}</text>
  317. </view>
  318. <view class="um-body">
  319. <view class="um-grid">
  320. <view class="um-item" v-for="(img, idx) in modalMediaList" :key="idx">
  321. <image class="um-preview" :src="img.thumb || img.url || img.localPath || img"
  322. mode="aspectFill">
  323. </image>
  324. <view class="um-video-badge" v-if="img.mediaType === 'video'">
  325. <image class="play-icon-small" src="/static/icons/play_circle.svg"></image>
  326. </view>
  327. <view class="um-del" @click="removeModalMedia(idx)">×</view>
  328. </view>
  329. <view class="um-add" @click="chooseModalMedia" v-if="modalMediaList.length < 6">
  330. <image class="um-add-icon" src="/static/icons/camera_grey.svg"></image>
  331. <text class="um-add-text">拍摄/上传</text>
  332. </view>
  333. </view>
  334. <textarea class="um-textarea" v-model="modalRemark" placeholder="在此输入备注信息..."
  335. placeholder-style="color:#ccc; font-size:26rpx;"></textarea>
  336. </view>
  337. <view class="um-footer">
  338. <view class="um-submit-btn" :class="{ 'active': modalMediaList.length > 0 }"
  339. @click="handleConfirmUpload">
  340. 确认提交</view>
  341. </view>
  342. </view>
  343. </view>
  344. <!-- 宠护小结弹窗 -->
  345. <view class="sum-modal-mask" v-if="showSumModal" @click="closeSumModal">
  346. <view class="sum-modal-card" @click.stop>
  347. <scroll-view scroll-y class="sum-modal-scroll">
  348. <view class="sum-modal-inner">
  349. <text class="sum-modal-title">宠物护理工作小结</text>
  350. <view class="sum-meta-row">
  351. <text class="sum-meta-label">日期:</text>
  352. <text class="sum-meta-val">{{ sumDate }}</text>
  353. </view>
  354. <view class="sum-meta-row">
  355. <text class="sum-meta-label">客户住址:</text>
  356. <text class="sum-meta-val">{{ orderDetail.endAddress }}</text>
  357. </view>
  358. <view class="sum-meta-row">
  359. <text class="sum-meta-label">宠主姓名:</text>
  360. <text class="sum-meta-val">{{ orderDetail.ownerName || '未知' }}</text>
  361. </view>
  362. <view class="sum-section-title">宠物信息</view>
  363. <view class="sum-pet-card">
  364. <image class="sum-pet-avatar" :src="orderDetail.petAvatar" mode="aspectFill"></image>
  365. <view class="sum-pet-info">
  366. <view class="sum-pet-name-row">
  367. <text class="sum-pet-name">{{ orderDetail.petName || '未知' }}</text>
  368. <text class="sum-pet-breed">品种: {{ orderDetail.petBreed || '未知' }}</text>
  369. </view>
  370. <text class="sum-pet-remark">{{ orderDetail.petNotes || '暂无备注' }}</text>
  371. </view>
  372. </view>
  373. <view class="sum-section-title">服务内容记录</view>
  374. <textarea class="sum-textarea" v-model="sumContent" auto-height placeholder="请填写服务内容..."
  375. placeholder-style="color:#ccc"></textarea>
  376. <view class="sum-sign-row">
  377. <text class="sum-sign-label">护宠师签名:</text>
  378. <text class="sum-sign-val">{{ sumSigner }}</text>
  379. </view>
  380. <view style="height: 20rpx;"></view>
  381. </view>
  382. </scroll-view>
  383. <view class="sum-footer">
  384. <button class="sum-submit-btn" @click="submitSumModal">提交小结</button>
  385. </view>
  386. </view>
  387. </view>
  388. <!-- 异常记录弹窗 -->
  389. <view class="modal-mask" v-if="showAnomalyModal" @click="closeAnomalyModal">
  390. <view class="anomaly-modal-content" @click.stop>
  391. <view class="am-header">
  392. <text class="am-title">历史异常记录</text>
  393. <view class="close-icon-btn" @click="closeAnomalyModal">×</view>
  394. </view>
  395. <scroll-view scroll-y class="am-scroll-list">
  396. <view class="empty-list" v-if="anomalyList.length === 0">
  397. <image class="empty-icon" src="/static/empty-rest.png" mode="aspectFit"></image>
  398. <text class="empty-text">暂无异常记录</text>
  399. </view>
  400. <view class="am-item" v-for="(item, index) in anomalyList" :key="index">
  401. <view class="am-item-header">
  402. <text class="am-item-type">{{ item.typeLabel }}</text>
  403. <text class="am-item-status" :class="'status-' + item.status">{{
  404. getAnomalyStatusLabel(item.status)
  405. }}</text>
  406. </view>
  407. <text class="am-item-content">{{ item.content }}</text>
  408. <view class="am-item-photos" v-if="item.photos && item.photos.length > 0">
  409. <view class="am-photo-item" v-for="(photoUrl, pIdx) in item.photoUrls" :key="pIdx"
  410. @click="previewMedia(item.photoUrls, pIdx)">
  411. <image v-if="!isVideo(photoUrl)" class="am-photo" :src="photoUrl" mode="aspectFill">
  412. </image>
  413. <view v-else class="tl-video-placeholder miniaturized">
  414. <view class="tl-play-icon small">
  415. </view>
  416. <text class="tl-video-label small">视频</text>
  417. </view>
  418. </view>
  419. </view>
  420. <view class="am-audit-box" v-if="item.status !== 0">
  421. <view class="am-audit-header">
  422. <text class="am-audit-label">{{ item.status === 1 ? '审核通过' : '驳回理由' }}</text>
  423. <text class="am-audit-time">{{ item.auditTime }}</text>
  424. </view>
  425. <text class="am-audit-remark">{{ item.auditRemark || '无' }}</text>
  426. </view>
  427. </view>
  428. </scroll-view>
  429. </view>
  430. </view>
  431. <!-- 视频播放弹窗 -->
  432. <view class="video-player-mask" v-if="videoPlayerShow" @click="closeVideoPlayer">
  433. <view class="video-player-content" @click.stop>
  434. <video class="v-player" :src="videoPlayerUrl" autoplay controls></video>
  435. <view class="v-close" @click="closeVideoPlayer">×</view>
  436. </view>
  437. </view>
  438. </template>
  439. </view>
  440. </template>
  441. <script>
  442. import { getOrderInfo, clockIn, submitNursingSummary } from '@/api/order/subOrder'
  443. import { getOrderLogs } from '@/api/order/subOrderLog'
  444. import { uploadFile } from '@/api/fulfiller/app'
  445. import { getAnomalyList } from '@/api/fulfiller/anamaly'
  446. import { listAllService } from '@/api/service/list'
  447. import { reportGps } from '@/utils/gps'
  448. import { getDictDataByType } from '@/api/system/dict'
  449. import { getPetDetail, submitPetRemark as apiSubmitPetRemark } from '@/api/archieves/pet'
  450. import { listChangeLog } from '@/api/archieves/changeLog'
  451. export default {
  452. data() {
  453. return {
  454. orderId: null,
  455. pageLoading: true,
  456. orderType: 1,
  457. orderStatus: 2,
  458. serviceId: null,
  459. serviceMode: null,
  460. petId: null,
  461. petDetail: null,
  462. clockInSteps: [],
  463. currentClockIn: null,
  464. currentStep: 0,
  465. orderDetail: {
  466. type: 1, fulfillmentCommission: '0.00', timeLabel: '服务时间', time: '',
  467. petAvatar: '/static/dog.png', petName: '', petBreed: '',
  468. serviceTag: '', startLocation: '', startAddress: '', endAddress: '',
  469. customerPhone: '', serviceContent: '', remark: '',
  470. orderNo: '', createTime: '', serviceName: '',
  471. progressLogs: [], nursingSummary: ''
  472. },
  473. serviceList: [],
  474. showPetModal: false,
  475. currentPetInfo: {},
  476. showNavModal: false,
  477. navTargetPointType: '',
  478. showUploadModal: false,
  479. modalMediaList: [],
  480. modalRemark: '',
  481. showSumModal: false,
  482. sumContent: '',
  483. sumDate: '',
  484. sumSigner: '未知',
  485. showPetRemarkInput: false,
  486. petRemarkText: '',
  487. showAnomalyModal: false,
  488. anomalyList: [],
  489. anomalyTypeDict: [],
  490. videoPlayerShow: false,
  491. videoPlayerUrl: ''
  492. }
  493. },
  494. computed: {
  495. steps() {
  496. if (this.clockInSteps.length > 0) return this.clockInSteps.map(s => s.title)
  497. return this.orderType === 1
  498. ? ['到达打卡', '确认出发', '送达打卡']
  499. : ['到达打卡', '开始服务', '服务结束']
  500. },
  501. progressSteps() { return ['已接单', ...this.steps, '订单完成'] },
  502. progressIndex() { return this.currentStep + 1 },
  503. displayStatusText() {
  504. if (this.currentStep >= this.steps.length) return '已完成';
  505. if (this.currentStep > 0) return this.orderType === 1 ? '配送中' : '服务中';
  506. return this.orderType === 1 ? '待接送' : '待服务';
  507. },
  508. currentStatusText() {
  509. return this.currentStep >= this.steps.length ? '已完成' : this.steps[this.currentStep];
  510. },
  511. currentTaskTitle() {
  512. if (this.currentStep >= this.steps.length) return '订单已完成';
  513. if (this.currentClockIn) return this.currentClockIn.title;
  514. return this.steps[this.currentStep] || '打卡';
  515. },
  516. currentTaskDesc() {
  517. if (this.currentStep >= this.steps.length) return '感谢您的服务,请注意休息';
  518. if (this.currentClockIn && this.currentClockIn.remark) return this.currentClockIn.remark;
  519. return '请按要求提交照片或视频及备注';
  520. }
  521. },
  522. async onLoad(options) {
  523. if (options.id) this.orderId = options.id
  524. this.pageLoading = true
  525. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  526. try {
  527. await this.loadAnomalyTypeDict()
  528. await this.loadServiceList()
  529. await this.loadOrderDetail()
  530. } finally {
  531. this.pageLoading = false
  532. }
  533. },
  534. methods: {
  535. async loadServiceList() {
  536. try {
  537. const res = await listAllService()
  538. this.serviceList = res.data || []
  539. } catch (err) { console.error('获取服务类型失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  540. },
  541. loadServiceDetail(serviceId) {
  542. const serviceInfo = (this.serviceList || []).find(s => s.id === serviceId)
  543. if (serviceInfo) {
  544. this.serviceMode = serviceInfo.mode
  545. this.orderDetail.serviceName = serviceInfo.name
  546. if (serviceInfo.clockInRemark) {
  547. try {
  548. const parsed = JSON.parse(serviceInfo.clockInRemark)
  549. if (Array.isArray(parsed) && parsed.length > 0) this.clockInSteps = parsed
  550. } catch (parseErr) { console.error('解析 clockInRemark 失败:', parseErr) }
  551. }
  552. }
  553. },
  554. async loadOrderDetail() {
  555. if (!this.orderId) { uni.showToast({ title: '订单ID缺失', icon: 'none' }); return }
  556. try {
  557. const res = await getOrderInfo(this.orderId)
  558. const order = res.data
  559. if (!order) { uni.showToast({ title: '订单不存在', icon: 'none' }); return }
  560. this.serviceId = order.service
  561. this.petId = order.usrPet || null
  562. this.transformOrderData(order)
  563. if (this.serviceId) this.loadServiceDetail(this.serviceId)
  564. if (this.petId) await this.loadPetDetail(this.petId)
  565. await this.loadOrderLogs()
  566. } catch (err) {
  567. console.error('获取订单详情失败:', err)
  568. uni.showToast({ title: err.message || err.msg || '加载失败', icon: 'none' })
  569. }
  570. },
  571. async loadOrderLogs() {
  572. try {
  573. const res = await getOrderLogs(this.orderId)
  574. const logs = res.data || []
  575. const progressLogs = logs.filter(log => log.logType === 1)
  576. this.orderDetail.progressLogs = progressLogs.map(log => ({
  577. status: log.title || '', time: log.createTime || '',
  578. medias: log.photoUrls || [], remark: log.content || ''
  579. }))
  580. const validLogs = logs.filter(log => log.logType === 1 && log.step !== undefined && log.step !== null)
  581. .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
  582. if (validLogs.length > 0) {
  583. const latestStep = validLogs[0].step
  584. const stepIndex = this.clockInSteps.findIndex(s => s.step === latestStep)
  585. this.currentStep = stepIndex >= 0 ? stepIndex + 1 : Number(latestStep)
  586. } else { this.currentStep = 0 }
  587. this.updateCurrentClockIn()
  588. } catch (err) { console.error('获取订单日志失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  589. },
  590. updateCurrentClockIn() {
  591. this.currentClockIn = this.currentStep < this.clockInSteps.length ? this.clockInSteps[this.currentStep] : null
  592. },
  593. transformOrderData(order) {
  594. const mode = order.mode || 0
  595. const isRoundTrip = mode === 1
  596. this.orderType = isRoundTrip ? 1 : 2
  597. this.orderStatus = order.status || 2
  598. this.orderDetail = {
  599. type: this.orderType,
  600. fulfillmentCommission: (order.fulfillmentCommission / 100).toFixed(2),
  601. timeLabel: isRoundTrip ? '取货时间' : '服务时间',
  602. time: order.serviceTime || '',
  603. petAvatar: '/static/dog.png',
  604. petName: order.petName || order.contact || '',
  605. petBreed: order.breed || '',
  606. serviceTag: order.groupPurchasePackageName || '',
  607. startLocation: order.fromAddress || '暂无起点',
  608. startAddress: order.fromAddress || '',
  609. fromAddress: order.fromAddress || '',
  610. fromLat: order.fromLat, fromLng: order.fromLng,
  611. endLocation: (order.contact || '') + ' ' + (order.contactPhoneNumber || ''),
  612. endAddress: order.toAddress || '',
  613. toAddress: order.toAddress || '',
  614. toLat: order.toLat, toLng: order.toLng,
  615. customerPhone: order.contactPhoneNumber || '',
  616. ownerName: order.contact || '',
  617. serviceContent: order.remark || '', remark: order.remark || '',
  618. orderNo: order.code || 'T' + order.id,
  619. createTime: order.serviceTime || '',
  620. nursingSummary: order.nursingSummary || '',
  621. fulfillerName: order.fulfillerName || '',
  622. progressLogs: [{ status: '您已接单', time: order.serviceTime || '' }]
  623. }
  624. if (this.orderDetail.fulfillerName) this.sumSigner = this.orderDetail.fulfillerName
  625. },
  626. async loadPetDetail(petId) {
  627. try {
  628. const res = await getPetDetail(petId)
  629. const pet = res.data
  630. if (pet) {
  631. this.petDetail = pet
  632. this.orderDetail.petAvatar = pet.avatarUrl || '/static/dog.png'
  633. this.orderDetail.petName = pet.name || this.orderDetail.petName
  634. this.orderDetail.petBreed = pet.breed || this.orderDetail.petBreed
  635. this.orderDetail.ownerName = pet.ownerName || this.orderDetail.ownerName
  636. }
  637. } catch (err) { console.error('获取宠物档案失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  638. },
  639. async loadAnomalyList() {
  640. if (!this.orderId) return
  641. try {
  642. const res = await getAnomalyList(this.orderId)
  643. const list = res.data || []
  644. this.anomalyList = list.map(item => {
  645. const dict = this.anomalyTypeDict.find(d => d.value === item.type)
  646. return { ...item, typeLabel: dict ? dict.label : item.type, photoUrls: item.photoUrls || [] }
  647. })
  648. } catch (err) { console.error('获取异常列表失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  649. },
  650. async loadAnomalyTypeDict() {
  651. try {
  652. const res = await getDictDataByType('flf_anamaly_type')
  653. this.anomalyTypeDict = res.data.map(item => ({ label: item.dictLabel, value: item.dictValue }))
  654. } catch (err) { console.error('获取异常字典失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  655. },
  656. openAnomalyModal() { this.showAnomalyModal = true; this.loadAnomalyList() },
  657. closeAnomalyModal() { this.showAnomalyModal = false },
  658. getAnomalyStatusLabel(status) {
  659. return { 0: '待审核', 1: '已通过', 2: '已驳回' }[status] || '未知'
  660. },
  661. showPetProfile() {
  662. const pet = this.petDetail
  663. if (pet) {
  664. this.currentPetInfo = {
  665. petAvatar: pet.avatarUrl || '/static/dog.png',
  666. petName: pet.name || '', petBreed: pet.breed || '',
  667. petGender: pet.gender === 1 ? 'M' : (pet.gender === 2 ? 'F' : ''),
  668. petAge: pet.age ? pet.age + '岁' : '未知',
  669. petWeight: pet.weight ? pet.weight + 'kg' : '未知',
  670. petPersonality: pet.personality || pet.cutePersonality || '无',
  671. petHobby: '', petRemark: pet.remark || '无',
  672. petTags: (pet.tags || []).map(t => t.name), petLogs: [],
  673. petSize: pet.size || '', petIsSterilized: pet.isSterilized,
  674. petHealthStatus: pet.healthStatus || '', petAllergies: pet.allergies || '',
  675. petMedicalHistory: pet.medicalHistory || '', petVaccineStatus: pet.vaccineStatus || '',
  676. ownerName: pet.ownerName || '', ownerPhone: pet.ownerPhone || ''
  677. }
  678. this.loadPetChangeLogs(pet.id)
  679. } else {
  680. this.currentPetInfo = {
  681. ...this.orderDetail, petGender: '', petAge: '未知', petWeight: '未知',
  682. petPersonality: '无', petHobby: '', petRemark: '无', petTags: [], petLogs: []
  683. }
  684. }
  685. this.showPetModal = true
  686. },
  687. async loadPetChangeLogs(petId) {
  688. if (!petId) return
  689. try {
  690. const res = await listChangeLog({ targetId: petId, targetType: 'pet' })
  691. const logs = res.data || []
  692. this.currentPetInfo.petLogs = logs.map(item => ({
  693. date: item.createTime || '', content: item.content || '', recorder: item.operatorName || '未知'
  694. }))
  695. } catch (err) { console.error('获取宠物备注列表失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  696. },
  697. closePetProfile() { this.showPetModal = false },
  698. openPetRemarkInput() { this.petRemarkText = ''; this.showPetRemarkInput = true },
  699. closePetRemarkInput() { this.showPetRemarkInput = false },
  700. async submitPetRemark() {
  701. if (!this.petRemarkText.trim()) { uni.showToast({ title: '备注内容不能为空', icon: 'none' }); return }
  702. if (!this.petId) { uni.showToast({ title: '宠物信息缺失', icon: 'none' }); return }
  703. uni.showLoading({ title: '提交中...', mask: true });
  704. try {
  705. await apiSubmitPetRemark({ petId: this.petId, content: this.petRemarkText });
  706. uni.hideLoading(); uni.showToast({ title: '备注已添加', icon: 'success' });
  707. this.closePetRemarkInput(); this.loadPetChangeLogs(this.petId);
  708. } catch (err) { uni.hideLoading(); console.error('提交宠物备注失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) }
  709. },
  710. goToAnomaly() {
  711. uni.navigateTo({ url: '/pages/orders/anomaly/index?orderId=' + (this.orderId || '') });
  712. },
  713. callPhone() {
  714. const phoneNum = this.orderDetail.customerPhone || '18900008451'
  715. if (!phoneNum) { uni.showToast({ title: '手机号不存在', icon: 'none' }); return }
  716. uni.showModal({
  717. title: '拨号提示',
  718. content: `系统将为您拨打手机号: ${phoneNum},请授予拨号权限以正常通话。`,
  719. confirmText: '呼叫', cancelText: '取消',
  720. success: (res) => {
  721. if (res.confirm) {
  722. uni.makePhoneCall({
  723. phoneNumber: phoneNum,
  724. fail: () => { uni.showToast({ title: '无法唤起拨号盘,请检查权限设置', icon: 'none' }) }
  725. });
  726. }
  727. }
  728. });
  729. },
  730. openNavigation(type) { this.navTargetPointType = type; this.showNavModal = true },
  731. closeNavModal() { this.showNavModal = false },
  732. chooseMap(mapType) {
  733. let pointType = this.navTargetPointType;
  734. let name = pointType === 'start' ? (this.orderDetail.fromAddress || '起点') : (this.orderDetail.toAddress || '终点');
  735. let address = pointType === 'start' ? (this.orderDetail.fromAddress || '起点地址') : (this.orderDetail.toAddress || '终点地址');
  736. let latitude = pointType === 'start' ? Number(this.orderDetail.fromLat) : Number(this.orderDetail.toLat);
  737. let longitude = pointType === 'start' ? Number(this.orderDetail.fromLng) : Number(this.orderDetail.toLng);
  738. this.showNavModal = false;
  739. const navigateTo = (lat, lng, addrName, addrDesc) => {
  740. uni.openLocation({
  741. latitude: lat, longitude: lng, name: addrName,
  742. address: addrDesc || '无法获取详细地址',
  743. success: () => { console.log('打开导航成功: ' + mapType) },
  744. fail: (err) => { console.error('打开导航失败:', err); uni.showToast({ title: '打开地图失败', icon: 'none' }) }
  745. });
  746. };
  747. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  748. navigateTo(latitude, longitude, name, address);
  749. } else {
  750. uni.showLoading({ title: '获取当前位置...', mask: true });
  751. reportGps(true).then(res => {
  752. uni.hideLoading(); navigateTo(res.latitude, res.longitude, name, address);
  753. }).catch(err => { uni.hideLoading(); console.error('获取地理位置失败:', err); uni.showToast({ title: err.message || err.msg || '请求失败', icon: 'none' }) });
  754. }
  755. },
  756. openUploadModal() { this.modalMediaList = []; this.modalRemark = ''; this.showUploadModal = true },
  757. closeUploadModal() { this.showUploadModal = false },
  758. handleConfirmUpload() { this.confirmUploadModal() },
  759. chooseModalMedia() {
  760. uni.showActionSheet({
  761. itemList: ['选择图片', '选择视频'],
  762. success: (asRes) => {
  763. if (asRes.tapIndex === 0) {
  764. uni.chooseImage({
  765. count: 6 - this.modalMediaList.length,
  766. sizeType: ['compressed'],
  767. sourceType: ['album', 'camera'],
  768. success: async (res) => {
  769. uni.showLoading({ title: '上传中...', mask: true });
  770. try {
  771. for (const filePath of res.tempFilePaths) {
  772. const uploadRes = await uploadFile(filePath);
  773. if (uploadRes.code === 200) {
  774. this.modalMediaList.push({
  775. url: uploadRes.data.url, ossId: uploadRes.data.ossId,
  776. localPath: filePath, mediaType: 'image'
  777. });
  778. }
  779. }
  780. uni.hideLoading(); uni.showToast({ title: '上传成功', icon: 'success' });
  781. } catch (err) { uni.hideLoading(); uni.showToast({ title: err.message || err.msg || '上传失败', icon: 'none' }) }
  782. },
  783. fail: (err) => { console.error('选择图片失败:', err) }
  784. });
  785. } else if (asRes.tapIndex === 1) {
  786. uni.chooseVideo({
  787. sourceType: ['album', 'camera'],
  788. compressed: true,
  789. success: async (res) => {
  790. uni.showLoading({ title: '上传中...', mask: true });
  791. try {
  792. const uploadRes = await uploadFile(res.tempFilePath);
  793. if (uploadRes.code === 200) {
  794. this.modalMediaList.push({
  795. url: uploadRes.data.url, ossId: uploadRes.data.ossId,
  796. localPath: res.tempFilePath, mediaType: 'video',
  797. thumb: res.thumbTempFilePath || res.tempFilePath
  798. });
  799. }
  800. uni.hideLoading(); uni.showToast({ title: '上传成功', icon: 'success' });
  801. } catch (err) { uni.hideLoading(); uni.showToast({ title: err.message || err.msg || '上传失败', icon: 'none' }) }
  802. },
  803. fail: (err) => { console.error('选择视频失败:', err) }
  804. });
  805. }
  806. }
  807. });
  808. },
  809. removeModalMedia(index) { this.modalMediaList.splice(index, 1) },
  810. async confirmUploadModal() {
  811. if (this.modalMediaList.length === 0) { uni.showToast({ title: '请上传至少一张图片或视频', icon: 'none' }); return }
  812. try {
  813. uni.showLoading({ title: '提交中...' });
  814. const ossIds = this.modalMediaList.map(item => item.ossId);
  815. const clockInType = this.currentClockIn ? this.currentClockIn.step : (this.currentStep + 1);
  816. const clockInData = {
  817. orderId: this.orderId, photos: ossIds,
  818. content: this.modalRemark || '', step: clockInType,
  819. title: this.currentTaskTitle,
  820. startFlag: Number(clockInType) === 1,
  821. endFlag: Number(this.currentStep) === this.steps.length - 1
  822. };
  823. await clockIn(clockInData);
  824. uni.hideLoading(); this.closeUploadModal();
  825. uni.showToast({ title: '打卡成功', icon: 'success' });
  826. await this.loadOrderDetail();
  827. } catch (err) {
  828. uni.hideLoading(); console.error('打卡失败:', err);
  829. uni.showToast({ title: err.message || err.msg || '打卡失败', icon: 'none' });
  830. }
  831. },
  832. copyOrderNo() {
  833. uni.setClipboardData({ data: this.orderDetail.orderNo, success: () => { uni.showToast({ title: '复制成功', icon: 'none' }) } });
  834. },
  835. openSumModal() {
  836. let displayDate = '';
  837. if (this.orderDetail.time) {
  838. displayDate = this.orderDetail.time.split(' ')[0].replace(/-/g, '/');
  839. } else {
  840. const now = new Date();
  841. displayDate = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
  842. }
  843. this.sumDate = displayDate;
  844. if (this.orderDetail.nursingSummary) {
  845. this.sumContent = this.orderDetail.nursingSummary;
  846. } else if (!this.sumContent) {
  847. this.sumContent = '1. 精神/身体状态:\n2. 进食/饮水:\n3. 排泤情况:\n4. 卫生情况:\n5. 互动情况:\n6. 特殊情况/备注:';
  848. }
  849. this.showSumModal = true;
  850. },
  851. closeSumModal() { this.showSumModal = false },
  852. async submitSumModal() {
  853. if (!this.sumContent.trim()) { uni.showToast({ title: '请填写服务内容', icon: 'none' }); return }
  854. uni.showLoading({ title: '提交中...', mask: true });
  855. try {
  856. const res = await submitNursingSummary({ orderId: this.orderId, content: this.sumContent });
  857. uni.hideLoading();
  858. if (res.code === 200) {
  859. uni.showToast({ title: '小结已提交', icon: 'success' });
  860. this.closeSumModal(); await this.loadOrderDetail();
  861. } else {
  862. // 对于业务失败,如果 request.js 没弹,这里补一下,但要用原信息
  863. uni.showToast({ title: res.msg || '提交小结失败', icon: 'none' })
  864. }
  865. } catch (err) {
  866. uni.hideLoading(); console.error('提交宠护小结失败:', err);
  867. uni.showToast({ title: err.message || err.msg || '提交小结失败', icon: 'none' });
  868. }
  869. },
  870. isVideo(url) {
  871. if (!url) return false;
  872. const videoExts = ['.mp4', '.mov', '.m4v', '.3gp', '.avi', '.wmv'];
  873. return videoExts.some(ext => url.toLowerCase().includes(ext));
  874. },
  875. getVideoPoster(url) {
  876. if (!this.isVideo(url)) return url;
  877. if (url.includes('?x-oss-process') || url.includes('?ci-process') || url.includes('?vframe')) return url;
  878. if (url.includes('myqcloud.com')) return url + '?ci-process=snapshot&time=1';
  879. return url + '?x-oss-process=video/snapshot,t_1,f_jpg,w_300,m_fast';
  880. },
  881. previewMedia(medias, currentIdx) {
  882. const url = medias[currentIdx];
  883. if (this.isVideo(url)) {
  884. this.videoPlayerUrl = url; this.videoPlayerShow = true;
  885. } else {
  886. const imageUrls = medias.filter(m => !this.isVideo(m));
  887. const newIdx = imageUrls.indexOf(url);
  888. uni.previewImage({ current: newIdx >= 0 ? newIdx : 0, urls: imageUrls });
  889. }
  890. },
  891. closeVideoPlayer() { this.videoPlayerShow = false; this.videoPlayerUrl = '' }
  892. }
  893. }
  894. </script>
  895. <style>
  896. page {
  897. background-color: #F8F8F8;
  898. }
  899. .detail-container {
  900. padding-bottom: 200rpx;
  901. }
  902. .loading-container {
  903. padding: 30rpx;
  904. }
  905. .skeleton-header {
  906. background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
  907. border-radius: 20rpx;
  908. padding: 40rpx;
  909. display: flex;
  910. justify-content: space-between;
  911. align-items: center;
  912. margin-bottom: 20rpx;
  913. }
  914. .skeleton-progress {
  915. background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
  916. border-radius: 0 0 20rpx 20rpx;
  917. margin-top: -40rpx;
  918. padding: 30rpx 40rpx 40rpx;
  919. display: flex;
  920. justify-content: space-around;
  921. margin-bottom: 25rpx;
  922. }
  923. .skeleton-circle {
  924. width: 40rpx;
  925. height: 40rpx;
  926. border-radius: 50%;
  927. background-color: rgba(255, 255, 255, 0.3);
  928. }
  929. .skeleton-card {
  930. background-color: #fff;
  931. border-radius: 20rpx;
  932. padding: 30rpx;
  933. margin-bottom: 25rpx;
  934. box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.03);
  935. }
  936. .skeleton-line {
  937. border-radius: 8rpx;
  938. background-color: #f0f0f0;
  939. }
  940. .skeleton-ani {
  941. background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 37%, #f0f0f0 63%);
  942. background-size: 400% 100%;
  943. animation: skeleton-loading 1.4s ease infinite;
  944. }
  945. .skeleton-header .skeleton-ani {
  946. background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.35) 37%, rgba(255, 255, 255, 0.2) 63%);
  947. background-size: 400% 100%;
  948. animation: skeleton-loading 1.4s ease infinite;
  949. }
  950. .skeleton-progress .skeleton-ani {
  951. background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 25%, rgba(255, 255, 255, 0.35) 37%, rgba(255, 255, 255, 0.2) 63%);
  952. background-size: 400% 100%;
  953. animation: skeleton-loading 1.4s ease infinite;
  954. }
  955. @keyframes skeleton-loading {
  956. 0% {
  957. background-position: 100% 50%;
  958. }
  959. 100% {
  960. background-position: 0 50%;
  961. }
  962. }
  963. .detail-header {
  964. background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
  965. padding: 40rpx 40rpx 50rpx 40rpx;
  966. color: #fff;
  967. border-radius: 20rpx;
  968. margin: 30rpx 30rpx 25rpx 30rpx;
  969. box-shadow: 0 5rpx 15rpx rgba(255, 87, 34, 0.2);
  970. }
  971. .status-row {
  972. display: flex;
  973. justify-content: space-between;
  974. align-items: center;
  975. margin-bottom: 40rpx;
  976. }
  977. .status-title {
  978. font-size: 28rpx;
  979. font-weight: bold;
  980. }
  981. .status-fulfillmentCommission {
  982. font-size: 40rpx;
  983. font-weight: bold;
  984. }
  985. .progress-bar {
  986. display: flex;
  987. justify-content: space-between;
  988. align-items: flex-start;
  989. padding: 0 10rpx;
  990. }
  991. .step-item {
  992. display: flex;
  993. flex-direction: column;
  994. align-items: center;
  995. flex: 1;
  996. position: relative;
  997. opacity: 0.5;
  998. }
  999. .step-item.done {
  1000. opacity: 1;
  1001. }
  1002. .step-item.done .step-circle {
  1003. background-color: #fff;
  1004. color: #FF5722;
  1005. font-weight: bold;
  1006. }
  1007. .step-item.done .step-text {
  1008. font-weight: 500;
  1009. }
  1010. .step-item.active {
  1011. opacity: 1;
  1012. }
  1013. .step-item.active .step-circle {
  1014. background-color: #fff;
  1015. color: #FF5722;
  1016. font-weight: bold;
  1017. width: 48rpx;
  1018. height: 48rpx;
  1019. font-size: 24rpx;
  1020. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
  1021. }
  1022. .step-item.active .step-text {
  1023. font-weight: bold;
  1024. }
  1025. .step-circle-wrapper {
  1026. position: relative;
  1027. width: 100%;
  1028. display: flex;
  1029. justify-content: center;
  1030. margin-bottom: 10rpx;
  1031. }
  1032. .step-circle {
  1033. width: 40rpx;
  1034. height: 40rpx;
  1035. border-radius: 50%;
  1036. background-color: rgba(255, 255, 255, 0.3);
  1037. color: rgba(255, 255, 255, 0.8);
  1038. font-size: 20rpx;
  1039. display: flex;
  1040. justify-content: center;
  1041. align-items: center;
  1042. position: relative;
  1043. z-index: 2;
  1044. transition: all 0.2s;
  1045. }
  1046. .step-line {
  1047. position: absolute;
  1048. top: 50%;
  1049. right: calc(50% + 20rpx);
  1050. width: calc(100% - 40rpx);
  1051. height: 2rpx;
  1052. background-color: rgba(255, 255, 255, 0.3);
  1053. z-index: 1;
  1054. transform: translateY(-50%);
  1055. }
  1056. .step-line.active-line {
  1057. background-color: #fff;
  1058. height: 3rpx;
  1059. }
  1060. .step-text {
  1061. font-size: 24rpx;
  1062. }
  1063. .detail-content {
  1064. width: 100%;
  1065. box-sizing: border-box;
  1066. padding: 0 30rpx;
  1067. position: relative;
  1068. z-index: 10;
  1069. }
  1070. .white-card {
  1071. background-color: #fff;
  1072. border-radius: 20rpx;
  1073. padding: 30rpx;
  1074. margin-bottom: 25rpx;
  1075. box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.03);
  1076. }
  1077. .pet-bar {
  1078. display: flex;
  1079. align-items: center;
  1080. }
  1081. .pb-avatar {
  1082. width: 100rpx;
  1083. height: 100rpx;
  1084. border-radius: 50%;
  1085. margin-right: 20rpx;
  1086. }
  1087. .pb-info {
  1088. flex: 1;
  1089. display: flex;
  1090. flex-direction: column;
  1091. }
  1092. .pb-name-row {
  1093. margin-bottom: 10rpx;
  1094. }
  1095. .pb-name {
  1096. font-size: 32rpx;
  1097. font-weight: bold;
  1098. color: #333;
  1099. margin-right: 15rpx;
  1100. }
  1101. .pb-breed {
  1102. font-size: 26rpx;
  1103. color: #999;
  1104. }
  1105. .pb-tags {
  1106. display: flex;
  1107. }
  1108. .pb-tag {
  1109. background-color: #FFF3E0;
  1110. color: #FF9800;
  1111. font-size: 22rpx;
  1112. padding: 4rpx 16rpx;
  1113. border-radius: 8rpx;
  1114. }
  1115. .pb-actions {
  1116. display: flex;
  1117. align-items: center;
  1118. }
  1119. .pb-btn {
  1120. display: flex;
  1121. align-items: center;
  1122. justify-content: center;
  1123. }
  1124. .profile-btn {
  1125. border: 2rpx solid #FF9800;
  1126. color: #FF9800;
  1127. font-size: 24rpx;
  1128. padding: 8rpx 20rpx;
  1129. border-radius: 30rpx;
  1130. margin-right: 20rpx;
  1131. }
  1132. .phone-btn {
  1133. width: 60rpx;
  1134. height: 60rpx;
  1135. border-radius: 50%;
  1136. background-color: #FFF3E0;
  1137. }
  1138. .phone-icon {
  1139. width: 32rpx;
  1140. height: 32rpx;
  1141. }
  1142. .si-row {
  1143. display: flex;
  1144. align-items: flex-start;
  1145. margin-bottom: 30rpx;
  1146. }
  1147. .si-row:last-child {
  1148. margin-bottom: 0;
  1149. }
  1150. .si-icon {
  1151. width: 36rpx;
  1152. height: 36rpx;
  1153. margin-right: 20rpx;
  1154. margin-top: 4rpx;
  1155. }
  1156. .si-icon-uni {
  1157. margin-right: 20rpx;
  1158. margin-top: 4rpx;
  1159. display: flex;
  1160. align-items: center;
  1161. justify-content: center;
  1162. }
  1163. .si-content {
  1164. flex: 1;
  1165. display: flex;
  1166. flex-direction: column;
  1167. }
  1168. .si-label {
  1169. font-size: 24rpx;
  1170. color: #999;
  1171. margin-bottom: 6rpx;
  1172. }
  1173. .icon-bg {
  1174. width: 44rpx;
  1175. height: 44rpx;
  1176. border-radius: 8rpx;
  1177. display: flex;
  1178. justify-content: center;
  1179. align-items: center;
  1180. margin-right: 20rpx;
  1181. margin-top: 4rpx;
  1182. flex-shrink: 0;
  1183. }
  1184. .icon-bg.grey-bg {
  1185. background-color: #F8F8F8;
  1186. }
  1187. .custom-icon {
  1188. width: 28rpx;
  1189. height: 28rpx;
  1190. }
  1191. .custom-icon-file {
  1192. width: 36rpx;
  1193. height: 36rpx;
  1194. }
  1195. .si-val {
  1196. font-size: 28rpx;
  1197. color: #333;
  1198. }
  1199. .record-btn {
  1200. font-size: 24rpx;
  1201. color: #FF5722;
  1202. display: flex;
  1203. align-items: center;
  1204. }
  1205. .record-arrow {
  1206. width: 24rpx;
  1207. height: 24rpx;
  1208. margin-left: 6rpx;
  1209. }
  1210. .record-history-row {
  1211. margin-top: 30rpx;
  1212. padding-top: 25rpx;
  1213. border-top: 1px dashed #f0f0f0;
  1214. display: flex;
  1215. justify-content: space-between;
  1216. align-items: center;
  1217. }
  1218. .addr-row {
  1219. position: relative;
  1220. align-items: stretch;
  1221. }
  1222. .icon-circle {
  1223. width: 40rpx;
  1224. height: 40rpx;
  1225. border-radius: 50%;
  1226. color: #fff;
  1227. font-size: 22rpx;
  1228. display: flex;
  1229. align-items: center;
  1230. justify-content: center;
  1231. margin-right: 20rpx;
  1232. flex-shrink: 0;
  1233. font-weight: bold;
  1234. margin-top: 6rpx;
  1235. position: relative;
  1236. z-index: 1;
  1237. }
  1238. .icon-circle.start {
  1239. background-color: #FFB74D;
  1240. }
  1241. .icon-circle.end {
  1242. background-color: #81C784;
  1243. }
  1244. .icon-circle.service {
  1245. background-color: #81C784;
  1246. }
  1247. .route-line-vertical {
  1248. position: absolute;
  1249. left: 19rpx;
  1250. top: 46rpx;
  1251. bottom: -6rpx;
  1252. border-left: 2rpx dashed #E0E0E0;
  1253. width: 0;
  1254. z-index: 0;
  1255. }
  1256. .si-addr-title {
  1257. font-size: 30rpx;
  1258. font-weight: bold;
  1259. color: #333;
  1260. margin-bottom: 6rpx;
  1261. }
  1262. .si-addr-desc {
  1263. font-size: 24rpx;
  1264. color: #999;
  1265. }
  1266. .nav-btn-circle {
  1267. width: 50rpx;
  1268. height: 50rpx;
  1269. background-color: #FFF3E0;
  1270. border-radius: 50%;
  1271. display: flex;
  1272. align-items: center;
  1273. justify-content: center;
  1274. margin-top: 6rpx;
  1275. }
  1276. .nav-arrow {
  1277. width: 28rpx;
  1278. height: 28rpx;
  1279. }
  1280. .task-card {
  1281. display: flex;
  1282. flex-direction: column;
  1283. }
  1284. .tc-title {
  1285. font-size: 30rpx;
  1286. font-weight: bold;
  1287. color: #333;
  1288. margin-bottom: 15rpx;
  1289. }
  1290. .tc-desc {
  1291. font-size: 22rpx;
  1292. color: #666;
  1293. margin-bottom: 12rpx;
  1294. }
  1295. .media-grid {
  1296. display: flex;
  1297. flex-wrap: wrap;
  1298. gap: 20rpx;
  1299. margin-bottom: 20rpx;
  1300. }
  1301. .media-item {
  1302. width: 160rpx;
  1303. height: 160rpx;
  1304. position: relative;
  1305. border-radius: 12rpx;
  1306. }
  1307. .media-preview {
  1308. width: 100%;
  1309. height: 100%;
  1310. border-radius: 12rpx;
  1311. }
  1312. .media-del {
  1313. position: absolute;
  1314. top: -10rpx;
  1315. right: -10rpx;
  1316. width: 36rpx;
  1317. height: 36rpx;
  1318. background-color: rgba(0, 0, 0, 0.5);
  1319. color: #fff;
  1320. border-radius: 50%;
  1321. display: flex;
  1322. align-items: center;
  1323. justify-content: center;
  1324. font-size: 24rpx;
  1325. z-index: 2;
  1326. }
  1327. .media-add {
  1328. width: 160rpx;
  1329. height: 160rpx;
  1330. border: 2rpx dashed #ccc;
  1331. border-radius: 12rpx;
  1332. display: flex;
  1333. flex-direction: column;
  1334. align-items: center;
  1335. justify-content: center;
  1336. background-color: #FAFAFA;
  1337. box-sizing: border-box;
  1338. }
  1339. .upload-icon-small {
  1340. width: 40rpx;
  1341. height: 40rpx;
  1342. margin-bottom: 10rpx;
  1343. }
  1344. .upload-text-small {
  1345. font-size: 22rpx;
  1346. color: #999;
  1347. }
  1348. .full-media-add {
  1349. width: 100%;
  1350. height: 140rpx;
  1351. background-color: #FAFAFA;
  1352. border: 2rpx dashed #E0E0E0;
  1353. border-radius: 12rpx;
  1354. display: flex;
  1355. flex-direction: column;
  1356. justify-content: center;
  1357. align-items: center;
  1358. margin-top: 10rpx;
  1359. }
  1360. .upload-icon-large {
  1361. width: 50rpx;
  1362. height: 50rpx;
  1363. margin-bottom: 10rpx;
  1364. opacity: 0.5;
  1365. }
  1366. .upload-text-large {
  1367. font-size: 24rpx;
  1368. color: #999;
  1369. }
  1370. .bi-row {
  1371. display: flex;
  1372. align-items: flex-start;
  1373. margin-bottom: 25rpx;
  1374. }
  1375. .bi-row:last-child {
  1376. margin-bottom: 0;
  1377. }
  1378. .bi-icon {
  1379. width: 32rpx;
  1380. height: 32rpx;
  1381. margin-right: 20rpx;
  1382. margin-top: 4rpx;
  1383. }
  1384. .bi-icon-uni {
  1385. margin-right: 20rpx;
  1386. margin-top: 4rpx;
  1387. display: flex;
  1388. align-items: center;
  1389. justify-content: center;
  1390. }
  1391. .bi-content {
  1392. flex: 1;
  1393. display: flex;
  1394. flex-direction: column;
  1395. }
  1396. .bi-label {
  1397. font-size: 24rpx;
  1398. color: #999;
  1399. margin-bottom: 6rpx;
  1400. }
  1401. .bi-val-row {
  1402. display: flex;
  1403. align-items: center;
  1404. }
  1405. .bi-val {
  1406. font-size: 28rpx;
  1407. color: #333;
  1408. }
  1409. .bi-copy {
  1410. background-color: #F0F0F0;
  1411. color: #666;
  1412. font-size: 20rpx;
  1413. padding: 2rpx 10rpx;
  1414. border-radius: 6rpx;
  1415. margin-left: 15rpx;
  1416. }
  1417. .tl-title-row {
  1418. display: flex;
  1419. align-items: center;
  1420. margin-bottom: 30rpx;
  1421. }
  1422. .orange-bar {
  1423. width: 8rpx;
  1424. height: 32rpx;
  1425. background-color: #FF9800;
  1426. margin-right: 16rpx;
  1427. border-radius: 4rpx;
  1428. }
  1429. .tl-title {
  1430. font-size: 30rpx;
  1431. font-weight: bold;
  1432. color: #333;
  1433. }
  1434. .tl-list {
  1435. display: flex;
  1436. flex-direction: column;
  1437. padding-left: 10rpx;
  1438. }
  1439. .tl-item {
  1440. display: flex;
  1441. position: relative;
  1442. padding-bottom: 40rpx;
  1443. }
  1444. .tl-item:last-child {
  1445. padding-bottom: 0;
  1446. }
  1447. .tl-marker {
  1448. width: 16rpx;
  1449. height: 16rpx;
  1450. border-radius: 50%;
  1451. background-color: #E0E0E0;
  1452. position: absolute;
  1453. left: 0;
  1454. top: 6rpx;
  1455. z-index: 2;
  1456. display: flex;
  1457. justify-content: center;
  1458. align-items: center;
  1459. }
  1460. .tl-marker.active {
  1461. background-color: #fff;
  1462. border: 3rpx solid #FF9800;
  1463. width: 18rpx;
  1464. height: 18rpx;
  1465. left: -1rpx;
  1466. }
  1467. .tl-dot-inner {
  1468. width: 10rpx;
  1469. height: 10rpx;
  1470. border-radius: 50%;
  1471. background-color: #FF9800;
  1472. }
  1473. .tl-item:not(:last-child)::after {
  1474. content: '';
  1475. position: absolute;
  1476. left: 7rpx;
  1477. top: 24rpx;
  1478. bottom: -6rpx;
  1479. width: 2rpx;
  1480. background-color: #FFE0B2;
  1481. z-index: 1;
  1482. }
  1483. .tl-content-row {
  1484. margin-left: 40rpx;
  1485. display: flex;
  1486. flex-direction: column;
  1487. width: 100%;
  1488. }
  1489. .tl-header {
  1490. display: flex;
  1491. justify-content: space-between;
  1492. align-items: center;
  1493. width: 100%;
  1494. margin-bottom: 10rpx;
  1495. }
  1496. .tl-status {
  1497. font-size: 28rpx;
  1498. color: #333;
  1499. font-weight: 500;
  1500. }
  1501. .tl-time {
  1502. font-size: 24rpx;
  1503. color: #999;
  1504. }
  1505. .tl-medias {
  1506. display: flex;
  1507. flex-wrap: wrap;
  1508. gap: 15rpx;
  1509. margin-bottom: 15rpx;
  1510. }
  1511. .tl-img {
  1512. width: 140rpx;
  1513. height: 140rpx;
  1514. border-radius: 8rpx;
  1515. }
  1516. .tl-remark {
  1517. font-size: 24rpx;
  1518. color: #666;
  1519. background-color: #F9F9F9;
  1520. padding: 15rpx;
  1521. border-radius: 8rpx;
  1522. line-height: 1.5;
  1523. }
  1524. .bottom-action-bar {
  1525. position: fixed;
  1526. bottom: 0;
  1527. left: 0;
  1528. width: 100%;
  1529. height: 120rpx;
  1530. background-color: #fff;
  1531. display: flex;
  1532. align-items: center;
  1533. justify-content: space-between;
  1534. padding: 0 30rpx;
  1535. box-shadow: 0 -5rpx 20rpx rgba(0, 0, 0, 0.05);
  1536. box-sizing: border-box;
  1537. padding-bottom: constant(safe-area-inset-bottom);
  1538. padding-bottom: env(safe-area-inset-bottom);
  1539. z-index: 100;
  1540. }
  1541. .action-left,
  1542. .action-right {
  1543. display: flex;
  1544. align-items: center;
  1545. }
  1546. .action-btn {
  1547. height: 64rpx;
  1548. line-height: 64rpx;
  1549. border-radius: 32rpx;
  1550. font-size: 26rpx;
  1551. padding: 0 35rpx;
  1552. margin: 0;
  1553. }
  1554. .action-left .action-btn:first-child {
  1555. margin-right: 20rpx;
  1556. }
  1557. .action-btn.grey-outline {
  1558. background-color: #fff;
  1559. color: #666;
  1560. border: 1px solid #E0E0E0;
  1561. }
  1562. .action-btn.orange-outline {
  1563. background-color: #FFF8F0;
  1564. color: #FF9800;
  1565. border: 1px solid #FF9800;
  1566. }
  1567. .action-btn.primary {
  1568. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1569. color: #fff;
  1570. box-shadow: 0 4rpx 12rpx rgba(255, 87, 34, 0.2);
  1571. border: none;
  1572. }
  1573. .action-btn.grey-bg {
  1574. background: #E0E0E0;
  1575. color: #999;
  1576. box-shadow: none;
  1577. border: none;
  1578. }
  1579. .action-btn::after {
  1580. border: none;
  1581. }
  1582. .upload-modal-mask {
  1583. position: fixed;
  1584. top: 0;
  1585. left: 0;
  1586. width: 100%;
  1587. height: 100%;
  1588. background-color: rgba(0, 0, 0, 0.4);
  1589. z-index: 999;
  1590. display: flex;
  1591. justify-content: center;
  1592. align-items: center;
  1593. }
  1594. .upload-modal-content {
  1595. width: 600rpx;
  1596. background-color: #ffffff;
  1597. border-radius: 20rpx;
  1598. padding: 40rpx;
  1599. box-sizing: border-box;
  1600. display: flex;
  1601. flex-direction: column;
  1602. }
  1603. .um-header {
  1604. text-align: center;
  1605. margin-bottom: 40rpx;
  1606. }
  1607. .um-title {
  1608. font-size: 32rpx;
  1609. font-weight: bold;
  1610. color: #333;
  1611. }
  1612. .um-remark-hint {
  1613. display: block;
  1614. font-size: 22rpx;
  1615. color: #999;
  1616. margin-top: 16rpx;
  1617. line-height: 1.4;
  1618. }
  1619. .um-grid {
  1620. display: flex;
  1621. flex-wrap: wrap;
  1622. margin-bottom: 30rpx;
  1623. }
  1624. .um-item {
  1625. width: 130rpx;
  1626. height: 130rpx;
  1627. border-radius: 8rpx;
  1628. margin-right: 20rpx;
  1629. margin-bottom: 20rpx;
  1630. position: relative;
  1631. background-color: #f5f5f5;
  1632. overflow: hidden;
  1633. }
  1634. .um-item:nth-child(4n) {
  1635. margin-right: 0;
  1636. }
  1637. .um-preview {
  1638. width: 100%;
  1639. height: 100%;
  1640. }
  1641. .um-del {
  1642. position: absolute;
  1643. top: 4rpx;
  1644. right: 4rpx;
  1645. width: 36rpx;
  1646. height: 36rpx;
  1647. line-height: 32rpx;
  1648. text-align: center;
  1649. background-color: rgba(0, 0, 0, 0.5);
  1650. color: #fff;
  1651. border-radius: 50%;
  1652. font-size: 28rpx;
  1653. z-index: 10;
  1654. }
  1655. .um-video-badge {
  1656. position: absolute;
  1657. top: 0;
  1658. left: 0;
  1659. right: 0;
  1660. bottom: 0;
  1661. display: flex;
  1662. justify-content: center;
  1663. align-items: center;
  1664. background-color: rgba(0, 0, 0, 0.2);
  1665. z-index: 5;
  1666. }
  1667. .play-icon-small {
  1668. width: 48rpx;
  1669. height: 48rpx;
  1670. opacity: 0.9;
  1671. }
  1672. .um-add {
  1673. width: 130rpx;
  1674. height: 130rpx;
  1675. border-radius: 8rpx;
  1676. border: 1px dashed #e5e5e5;
  1677. display: flex;
  1678. flex-direction: column;
  1679. justify-content: center;
  1680. align-items: center;
  1681. box-sizing: border-box;
  1682. margin-bottom: 20rpx;
  1683. }
  1684. .um-add-icon {
  1685. width: 44rpx;
  1686. height: 44rpx;
  1687. margin-bottom: 10rpx;
  1688. opacity: 0.4;
  1689. }
  1690. .um-add-text {
  1691. font-size: 24rpx;
  1692. color: #ccc;
  1693. }
  1694. .um-textarea {
  1695. width: 100%;
  1696. height: 160rpx;
  1697. background-color: #ffffff;
  1698. border-radius: 12rpx;
  1699. padding: 24rpx;
  1700. font-size: 28rpx;
  1701. color: #333;
  1702. box-sizing: border-box;
  1703. border: 1px solid #f0f0f0;
  1704. }
  1705. .um-footer {
  1706. margin-top: 40rpx;
  1707. display: flex;
  1708. justify-content: center;
  1709. }
  1710. .um-submit-btn {
  1711. width: 100%;
  1712. height: 88rpx;
  1713. line-height: 88rpx;
  1714. border-radius: 44rpx;
  1715. font-size: 32rpx;
  1716. background-color: #E0E0E0;
  1717. color: #fff;
  1718. border: none;
  1719. text-align: center;
  1720. }
  1721. .um-submit-btn.active {
  1722. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1723. color: #fff;
  1724. box-shadow: 0 4rpx 10rpx rgba(255, 87, 34, 0.3);
  1725. }
  1726. .um-submit-btn::after {
  1727. border: none;
  1728. }
  1729. .pet-modal-mask {
  1730. position: fixed;
  1731. top: 0;
  1732. left: 0;
  1733. width: 100%;
  1734. height: 100%;
  1735. background-color: rgba(0, 0, 0, 0.5);
  1736. z-index: 999;
  1737. display: flex;
  1738. justify-content: center;
  1739. align-items: center;
  1740. }
  1741. .pet-modal-content {
  1742. width: 680rpx;
  1743. max-height: 85vh;
  1744. background-color: #fff;
  1745. border-radius: 20rpx;
  1746. display: flex;
  1747. flex-direction: column;
  1748. overflow: hidden;
  1749. box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
  1750. }
  1751. .pet-modal-header {
  1752. display: flex;
  1753. align-items: center;
  1754. padding: 30rpx 30rpx 20rpx;
  1755. border-bottom: 1px solid #f5f5f5;
  1756. flex-shrink: 0;
  1757. }
  1758. .pet-modal-title {
  1759. font-size: 32rpx;
  1760. font-weight: bold;
  1761. color: #333;
  1762. }
  1763. .close-icon-btn {
  1764. width: 52rpx;
  1765. height: 52rpx;
  1766. line-height: 52rpx;
  1767. text-align: center;
  1768. font-size: 36rpx;
  1769. color: #999;
  1770. border-radius: 50%;
  1771. background-color: #f5f5f5;
  1772. flex-shrink: 0;
  1773. }
  1774. .pm-remark-btn {
  1775. height: 52rpx;
  1776. line-height: 52rpx;
  1777. padding: 0 24rpx;
  1778. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  1779. color: #fff;
  1780. font-size: 26rpx;
  1781. border-radius: 26rpx;
  1782. margin-right: 16rpx;
  1783. font-weight: bold;
  1784. flex-shrink: 0;
  1785. }
  1786. .pet-modal-scroll {
  1787. flex: 1;
  1788. height: 65vh;
  1789. overflow: auto;
  1790. }
  1791. .pet-base-info {
  1792. display: flex;
  1793. align-items: center;
  1794. padding: 24rpx 30rpx;
  1795. border-bottom: 1px solid #f9f9f9;
  1796. }
  1797. .pm-avatar {
  1798. width: 100rpx;
  1799. height: 100rpx;
  1800. border-radius: 50%;
  1801. margin-right: 20rpx;
  1802. flex-shrink: 0;
  1803. }
  1804. .pm-info-text {
  1805. flex: 1;
  1806. display: flex;
  1807. flex-direction: column;
  1808. }
  1809. .pm-name-row {
  1810. display: flex;
  1811. align-items: center;
  1812. margin-bottom: 8rpx;
  1813. }
  1814. .pm-name {
  1815. font-size: 32rpx;
  1816. font-weight: bold;
  1817. color: #333;
  1818. margin-right: 12rpx;
  1819. }
  1820. .pm-gender {
  1821. display: flex;
  1822. align-items: center;
  1823. background-color: #E3F2FD;
  1824. color: #1976D2;
  1825. font-size: 22rpx;
  1826. padding: 2rpx 10rpx;
  1827. border-radius: 10rpx;
  1828. }
  1829. .pm-gender.female {
  1830. background-color: #FCE4EC;
  1831. color: #C2185B;
  1832. }
  1833. .gender-icon {
  1834. font-size: 22rpx;
  1835. margin-right: 4rpx;
  1836. }
  1837. .pm-breed {
  1838. font-size: 26rpx;
  1839. color: #999;
  1840. }
  1841. .pm-detail-grid {
  1842. padding: 20rpx 30rpx;
  1843. display: flex;
  1844. flex-wrap: wrap;
  1845. gap: 16rpx;
  1846. border-bottom: 1px solid #f9f9f9;
  1847. }
  1848. .pm-grid-item {
  1849. background-color: #FAFAFA;
  1850. border-radius: 12rpx;
  1851. padding: 16rpx 20rpx;
  1852. display: flex;
  1853. flex-direction: column;
  1854. }
  1855. .pm-grid-item.half {
  1856. width: calc(50% - 8rpx);
  1857. }
  1858. .pm-grid-item.full {
  1859. width: 100%;
  1860. }
  1861. .pm-label {
  1862. font-size: 24rpx;
  1863. color: #999;
  1864. margin-bottom: 8rpx;
  1865. }
  1866. .pm-val {
  1867. font-size: 28rpx;
  1868. color: #333;
  1869. line-height: 1.5;
  1870. }
  1871. .pm-tags-row {
  1872. display: flex;
  1873. flex-wrap: wrap;
  1874. padding: 0 24rpx 20rpx;
  1875. gap: 16rpx;
  1876. }
  1877. .pm-tag-chip {
  1878. background-color: #FFF3E0;
  1879. border: 1px solid #FFB74D;
  1880. border-radius: 20rpx;
  1881. padding: 6rpx 20rpx;
  1882. }
  1883. .pm-tag-chip-text {
  1884. font-size: 24rpx;
  1885. color: #FF9800;
  1886. }
  1887. .pm-log-section {
  1888. padding: 20rpx 24rpx;
  1889. border-top: 1px solid #f5f5f5;
  1890. margin-top: 10rpx;
  1891. }
  1892. .pm-log-header {
  1893. display: flex;
  1894. align-items: center;
  1895. margin-bottom: 20rpx;
  1896. }
  1897. .pm-log-section-title {
  1898. font-size: 28rpx;
  1899. font-weight: bold;
  1900. color: #333;
  1901. }
  1902. .pm-log-item {
  1903. padding: 16rpx 0;
  1904. border-bottom: 1px solid #f9f9f9;
  1905. display: flex;
  1906. flex-direction: column;
  1907. }
  1908. .pm-log-date {
  1909. font-size: 24rpx;
  1910. color: #999;
  1911. margin-bottom: 8rpx;
  1912. }
  1913. .pm-log-text {
  1914. font-size: 28rpx;
  1915. color: #333;
  1916. line-height: 1.6;
  1917. margin-bottom: 6rpx;
  1918. }
  1919. .pm-log-recorder {
  1920. font-size: 22rpx;
  1921. color: #FF9800;
  1922. text-align: right;
  1923. }
  1924. .pm-log-recorder.system {
  1925. color: #999;
  1926. }
  1927. .pm-bottom-close {
  1928. margin: 16rpx 30rpx;
  1929. height: 72rpx;
  1930. line-height: 72rpx;
  1931. border-radius: 36rpx;
  1932. font-size: 28rpx;
  1933. background-color: #f5f5f5;
  1934. color: #666;
  1935. border: none;
  1936. }
  1937. .pm-bottom-close::after {
  1938. border: none;
  1939. }
  1940. .sum-modal-mask {
  1941. position: fixed;
  1942. top: 0;
  1943. left: 0;
  1944. width: 100%;
  1945. height: 100%;
  1946. background-color: rgba(0, 0, 0, 0.5);
  1947. z-index: 999;
  1948. display: flex;
  1949. justify-content: center;
  1950. align-items: center;
  1951. }
  1952. .sum-modal-card {
  1953. width: 660rpx;
  1954. max-height: 80vh;
  1955. background-color: #fff;
  1956. border-radius: 20rpx;
  1957. display: flex;
  1958. flex-direction: column;
  1959. overflow: hidden;
  1960. box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
  1961. }
  1962. .sum-modal-scroll {
  1963. flex: 1;
  1964. overflow: hidden;
  1965. }
  1966. .sum-modal-inner {
  1967. padding: 32rpx 36rpx 0;
  1968. }
  1969. .sum-modal-title {
  1970. display: block;
  1971. font-size: 34rpx;
  1972. font-weight: bold;
  1973. color: #333;
  1974. text-align: center;
  1975. margin-bottom: 30rpx;
  1976. }
  1977. .sum-meta-row {
  1978. display: flex;
  1979. margin-bottom: 16rpx;
  1980. align-items: flex-start;
  1981. }
  1982. .sum-meta-label {
  1983. font-size: 26rpx;
  1984. color: #999;
  1985. flex-shrink: 0;
  1986. width: 140rpx;
  1987. }
  1988. .sum-meta-val {
  1989. font-size: 24rpx;
  1990. color: #333;
  1991. flex: 1;
  1992. }
  1993. .sum-section-title {
  1994. font-size: 26rpx;
  1995. font-weight: bold;
  1996. color: #333;
  1997. padding-left: 12rpx;
  1998. border-left: 6rpx solid #FF9800;
  1999. margin-top: 24rpx;
  2000. margin-bottom: 16rpx;
  2001. }
  2002. .sum-pet-card {
  2003. background-color: #FAFAFA;
  2004. border-radius: 12rpx;
  2005. padding: 20rpx;
  2006. display: flex;
  2007. align-items: center;
  2008. }
  2009. .sum-pet-avatar {
  2010. width: 80rpx;
  2011. height: 80rpx;
  2012. border-radius: 50%;
  2013. margin-right: 20rpx;
  2014. flex-shrink: 0;
  2015. }
  2016. .sum-pet-info {
  2017. flex: 1;
  2018. display: flex;
  2019. flex-direction: column;
  2020. }
  2021. .sum-pet-name-row {
  2022. display: flex;
  2023. align-items: center;
  2024. margin-bottom: 8rpx;
  2025. }
  2026. .sum-pet-name {
  2027. font-size: 26rpx;
  2028. font-weight: bold;
  2029. color: #333;
  2030. margin-right: 12rpx;
  2031. }
  2032. .sum-pet-breed {
  2033. font-size: 24rpx;
  2034. color: #999;
  2035. }
  2036. .sum-pet-remark {
  2037. font-size: 24rpx;
  2038. color: #666;
  2039. line-height: 1.5;
  2040. }
  2041. .sum-textarea {
  2042. width: 100%;
  2043. min-height: 220rpx;
  2044. background-color: #fff;
  2045. border: 1px solid #eeeeee;
  2046. border-radius: 12rpx;
  2047. padding: 18rpx;
  2048. font-size: 26rpx;
  2049. color: #333;
  2050. box-sizing: border-box;
  2051. line-height: 1.8;
  2052. }
  2053. .sum-sign-row {
  2054. display: flex;
  2055. align-items: center;
  2056. margin-top: 30rpx;
  2057. padding: 16rpx 0;
  2058. border-top: 1px solid #f5f5f5;
  2059. }
  2060. .sum-sign-label {
  2061. font-size: 26rpx;
  2062. color: #999;
  2063. margin-right: 10rpx;
  2064. }
  2065. .sum-sign-val {
  2066. font-size: 26rpx;
  2067. color: #333;
  2068. }
  2069. .sum-footer {
  2070. padding: 20rpx 36rpx 36rpx;
  2071. padding-bottom: max(36rpx, constant(safe-area-inset-bottom));
  2072. padding-bottom: max(36rpx, env(safe-area-inset-bottom));
  2073. background-color: #fff;
  2074. }
  2075. .sum-submit-btn {
  2076. width: 100%;
  2077. height: 84rpx;
  2078. line-height: 84rpx;
  2079. border-radius: 42rpx;
  2080. font-size: 30rpx;
  2081. background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
  2082. color: #fff;
  2083. border: none;
  2084. box-shadow: 0 4rpx 16rpx rgba(255, 87, 34, 0.25);
  2085. }
  2086. .sum-submit-btn::after {
  2087. border: none;
  2088. }
  2089. .modal-mask {
  2090. position: fixed;
  2091. top: 0;
  2092. left: 0;
  2093. right: 0;
  2094. bottom: 0;
  2095. background-color: rgba(0, 0, 0, 0.5);
  2096. z-index: 999;
  2097. display: flex;
  2098. align-items: center;
  2099. justify-content: center;
  2100. }
  2101. .anomaly-modal-content {
  2102. width: 650rpx;
  2103. background-color: #fff;
  2104. border-radius: 20rpx;
  2105. padding: 30rpx;
  2106. position: relative;
  2107. max-height: 80vh;
  2108. display: flex;
  2109. flex-direction: column;
  2110. }
  2111. .am-header {
  2112. display: flex;
  2113. justify-content: space-between;
  2114. align-items: center;
  2115. margin-bottom: 30rpx;
  2116. padding-bottom: 20rpx;
  2117. border-bottom: 1px solid #f5f5f5;
  2118. }
  2119. .am-title {
  2120. font-size: 32rpx;
  2121. font-weight: bold;
  2122. color: #333;
  2123. }
  2124. .am-scroll-list {
  2125. flex: 1;
  2126. }
  2127. .am-item {
  2128. padding: 24rpx;
  2129. background-color: #F9FAFB;
  2130. border-radius: 12rpx;
  2131. margin-bottom: 24rpx;
  2132. }
  2133. .am-item-header {
  2134. display: flex;
  2135. justify-content: space-between;
  2136. align-items: center;
  2137. margin-bottom: 16rpx;
  2138. }
  2139. .am-item-type {
  2140. font-size: 28rpx;
  2141. font-weight: bold;
  2142. color: #333;
  2143. }
  2144. .am-item-status {
  2145. font-size: 22rpx;
  2146. padding: 4rpx 16rpx;
  2147. border-radius: 20rpx;
  2148. }
  2149. .am-item-status.status-0 {
  2150. background-color: #FFF3E0;
  2151. color: #FF9800;
  2152. }
  2153. .am-item-status.status-1 {
  2154. background-color: #E8F5E9;
  2155. color: #4CAF50;
  2156. }
  2157. .am-item-status.status-2 {
  2158. background-color: #FFEBEE;
  2159. color: #F44336;
  2160. }
  2161. .am-item-content {
  2162. font-size: 26rpx;
  2163. color: #666;
  2164. line-height: 1.6;
  2165. display: block;
  2166. margin-bottom: 16rpx;
  2167. }
  2168. .am-item-photos {
  2169. display: flex;
  2170. flex-wrap: wrap;
  2171. gap: 12rpx;
  2172. margin-bottom: 20rpx;
  2173. }
  2174. .am-photo {
  2175. width: 130rpx;
  2176. height: 130rpx;
  2177. border-radius: 8rpx;
  2178. }
  2179. .am-audit-box {
  2180. margin-top: 20rpx;
  2181. padding-top: 20rpx;
  2182. border-top: 1px dashed #E0E0E0;
  2183. }
  2184. .am-audit-header {
  2185. display: flex;
  2186. justify-content: space-between;
  2187. align-items: center;
  2188. margin-bottom: 8rpx;
  2189. }
  2190. .am-audit-label {
  2191. font-size: 24rpx;
  2192. font-weight: bold;
  2193. color: #333;
  2194. }
  2195. .am-audit-time {
  2196. font-size: 22rpx;
  2197. color: #999;
  2198. }
  2199. .am-audit-remark {
  2200. font-size: 24rpx;
  2201. color: #666;
  2202. line-height: 1.5;
  2203. }
  2204. .empty-list {
  2205. display: flex;
  2206. flex-direction: column;
  2207. align-items: center;
  2208. padding: 100rpx 0;
  2209. }
  2210. .empty-icon {
  2211. width: 200rpx;
  2212. height: 200rpx;
  2213. margin-bottom: 20rpx;
  2214. opacity: 0.5;
  2215. }
  2216. .empty-text {
  2217. font-size: 26rpx;
  2218. color: #999;
  2219. }
  2220. .tl-media-item {
  2221. position: relative;
  2222. width: 140rpx;
  2223. height: 140rpx;
  2224. }
  2225. .tl-video-placeholder {
  2226. width: 100%;
  2227. height: 100%;
  2228. background: linear-gradient(135deg, #444 0%, #222 100%);
  2229. border-radius: 8rpx;
  2230. display: flex;
  2231. flex-direction: column;
  2232. justify-content: center;
  2233. align-items: center;
  2234. }
  2235. .tl-video-placeholder.miniaturized {
  2236. border-radius: 8rpx;
  2237. overflow: hidden;
  2238. }
  2239. .tl-video-label {
  2240. color: #fff;
  2241. font-size: 20rpx;
  2242. margin-top: 50rpx;
  2243. opacity: 0.8;
  2244. }
  2245. .tl-video-label.small {
  2246. margin-top: 35rpx;
  2247. font-size: 18rpx;
  2248. }
  2249. .tl-play-icon {
  2250. position: absolute;
  2251. top: 50%;
  2252. left: 50%;
  2253. transform: translate(-50%, -50%);
  2254. width: 60rpx;
  2255. height: 60rpx;
  2256. display: flex;
  2257. justify-content: center;
  2258. align-items: center;
  2259. background-color: rgba(255, 255, 255, 0.2);
  2260. border: 2rpx solid #fff;
  2261. border-radius: 50%;
  2262. }
  2263. .tl-play-icon::after {
  2264. content: '';
  2265. display: block;
  2266. width: 0;
  2267. height: 0;
  2268. border-top: 15rpx solid transparent;
  2269. border-bottom: 15rpx solid transparent;
  2270. border-left: 20rpx solid #fff;
  2271. margin-left: 6rpx;
  2272. }
  2273. .tl-play-icon.small {
  2274. width: 40rpx;
  2275. height: 40rpx;
  2276. }
  2277. .tl-play-icon.small::after {
  2278. border-top: 10rpx solid transparent;
  2279. border-bottom: 10rpx solid transparent;
  2280. border-left: 14rpx solid #fff;
  2281. margin-left: 4rpx;
  2282. }
  2283. .video-player-mask {
  2284. position: fixed;
  2285. top: 0;
  2286. left: 0;
  2287. right: 0;
  2288. bottom: 0;
  2289. background-color: rgba(0, 0, 0, 0.9);
  2290. z-index: 999;
  2291. display: flex;
  2292. justify-content: center;
  2293. align-items: center;
  2294. }
  2295. .video-player-content {
  2296. position: relative;
  2297. width: 100%;
  2298. height: 600rpx;
  2299. }
  2300. .v-player {
  2301. width: 100%;
  2302. height: 100%;
  2303. }
  2304. .v-close {
  2305. position: absolute;
  2306. top: -80rpx;
  2307. right: 40rpx;
  2308. width: 60rpx;
  2309. height: 60rpx;
  2310. background-color: rgba(255, 255, 255, 0.2);
  2311. color: #fff;
  2312. border-radius: 50%;
  2313. display: flex;
  2314. justify-content: center;
  2315. align-items: center;
  2316. font-size: 40rpx;
  2317. }
  2318. .am-photo-item {
  2319. position: relative;
  2320. width: 150rpx;
  2321. height: 150rpx;
  2322. border-radius: 8rpx;
  2323. overflow: hidden;
  2324. }
  2325. .am-photo {
  2326. width: 150rpx;
  2327. height: 150rpx;
  2328. border-radius: 8rpx;
  2329. }
  2330. .nav-modal-mask {
  2331. position: fixed;
  2332. top: 0;
  2333. left: 0;
  2334. right: 0;
  2335. bottom: 0;
  2336. background-color: rgba(0, 0, 0, 0.6);
  2337. z-index: 1000;
  2338. display: flex;
  2339. align-items: flex-end;
  2340. }
  2341. .nav-action-sheet {
  2342. width: 100%;
  2343. background-color: #fff;
  2344. border-radius: 30rpx 30rpx 0 0;
  2345. padding-bottom: constant(safe-area-inset-bottom);
  2346. padding-bottom: env(safe-area-inset-bottom);
  2347. overflow: hidden;
  2348. }
  2349. .nav-sheet-title {
  2350. display: block;
  2351. text-align: center;
  2352. font-size: 26rpx;
  2353. color: #999;
  2354. padding: 30rpx 0;
  2355. border-bottom: 1px solid #f5f5f5;
  2356. }
  2357. .nav-sheet-item {
  2358. display: block;
  2359. text-align: center;
  2360. font-size: 32rpx;
  2361. color: #333;
  2362. padding: 35rpx 0;
  2363. border-bottom: 1px solid #f5f5f5;
  2364. background-color: #fff;
  2365. }
  2366. .nav-sheet-item:active {
  2367. background-color: #f9f9f9;
  2368. }
  2369. .nav-sheet-item.cancel {
  2370. color: #999;
  2371. }
  2372. .nav-sheet-gap {
  2373. height: 12rpx;
  2374. background-color: #f5f5f5;
  2375. }
  2376. </style>