index.vue 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. <template>
  2. <view class="order-apply-page">
  3. <nav-bar title="下单预约"></nav-bar>
  4. <view class="apply-content">
  5. <!-- 01 服务类型 -->
  6. <text class="section-title">01 服务类型</text>
  7. <view class="card service-info-card">
  8. <view class="service-type-display">
  9. <view :class="['service-icon-box', activeService]">
  10. <!-- CSS 手绘服务图标 @Author: Antigravity -->
  11. <view class="pure-css-icon" :class="serviceIconClass"></view>
  12. </view>
  13. <view class="service-info-text">
  14. <text class="main-name">{{ currentServiceName }}</text>
  15. <text class="sub-desc">{{ serviceDesc }}</text>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- 02 基础信息 -->
  20. <text class="section-title">02 基础信息</text>
  21. <view class="card basic-info-card">
  22. <view class="field-item" @click="showShopSelect = true">
  23. <text class="field-label require">服务门店</text>
  24. <text :class="['field-value', !formData.shopName ? 'placeholder' : '']">{{ formData.shopName ||
  25. '请选择商户门店' }}</text>
  26. <view class="right-arrow"></view>
  27. </view>
  28. <view class="field-item" @click="showUserSelect = true">
  29. <text class="field-label require">宠主用户</text>
  30. <view class="field-value-wrap">
  31. <template v-if="selectedUser">
  32. <text class="selected-name">{{ selectedUser.name }}</text>
  33. <text class="selected-phone">{{ selectedUser.phone || selectedUser.phoneNumber }}</text>
  34. </template>
  35. <text v-else class="placeholder">点击搜索</text>
  36. </view>
  37. <view class="right-arrow"></view>
  38. </view>
  39. <view class="field-item" @click="openPetPicker">
  40. <text class="field-label require">选择宠物</text>
  41. <text :class="['field-value', !formData.petName ? 'placeholder' : '']">{{ formData.petName ||
  42. '选择宠物档案' }}</text>
  43. <view class="right-arrow"></view>
  44. </view>
  45. </view>
  46. <!-- 03 业务表单 - 宠物接送 -->
  47. <template v-if="activeService === 'transport'">
  48. <text class="section-title">03 接送路线与时间</text>
  49. <view class="card transport-card">
  50. <view class="field-item">
  51. <text class="field-label">团购套餐</text>
  52. <input class="field-input" v-model="formData.packageName" placeholder="请输入套餐名称(选填)" />
  53. </view>
  54. <text class="form-item-label require">接送模式</text>
  55. <view class="mode-select">
  56. <view v-for="mode in transportModes" :key="mode.value"
  57. :class="['mode-btn', { active: formData.transportMode === mode.value }]"
  58. @click="formData.transportMode = mode.value">
  59. <text>{{ mode.label }}</text>
  60. </view>
  61. </view>
  62. <!-- 接宠路线 @Author: Antigravity -->
  63. <view class="route-box" v-if="formData.transportMode !== 'return_home'">
  64. <view class="route-icon pick">接</view>
  65. <view class="route-fields">
  66. <text class="addr-label require">起点 (用户家)</text>
  67. <view class="route-picker-trigger" @click="openRegionSelect('pick')">
  68. <text :class="['display-text', !formData.pickArea ? 'placeholder' : '']">{{
  69. pickAreaLabel || '选择省/市/区' }}</text>
  70. <view class="right-arrow"></view>
  71. </view>
  72. <input class="route-input" v-model="formData.pickAddress" placeholder="详细地址" />
  73. <text class="addr-label require">终点 (门店)</text>
  74. <view class="route-picker-trigger" @click="openRegionSelect('pickEnd')">
  75. <text :class="['display-text', !formData.pickEndArea ? 'placeholder' : '']">{{
  76. pickEndAreaLabel || '选择省/市/区' }}</text>
  77. <view class="right-arrow"></view>
  78. </view>
  79. <input class="route-input" v-model="formData.pickEndAddress" placeholder="详细地址" />
  80. <view class="contact-row">
  81. <input class="route-input half" v-model="formData.pickContact" placeholder="联系人" />
  82. <input class="route-input half" v-model="formData.pickPhone" placeholder="电话"
  83. type="tel" />
  84. </view>
  85. <view class="route-time-trigger" @click="openTimeModal('pick')">
  86. <text :class="!formData.pickStartTime ? 'placeholder' : ''">{{
  87. truncateTime(formData.pickStartTime) || '设置接宠时间' }}</text>
  88. </view>
  89. </view>
  90. </view>
  91. <!-- 送宠路线 @Author: Antigravity -->
  92. <view class="route-box" v-if="formData.transportMode !== 'pick_up'">
  93. <view class="route-icon send">送</view>
  94. <view class="route-fields">
  95. <text class="addr-label require">起点 (门店)</text>
  96. <view class="route-picker-trigger" @click="openRegionSelect('sendStart')">
  97. <text :class="['display-text', !formData.sendStartArea ? 'placeholder' : '']">{{
  98. sendStartAreaLabel || '选择省/市/区' }}</text>
  99. <view class="right-arrow"></view>
  100. </view>
  101. <input class="route-input" v-model="formData.sendStartAddress" placeholder="详细地址" />
  102. <text class="addr-label require">终点 (用户家)</text>
  103. <view class="route-picker-trigger" @click="openRegionSelect('send')">
  104. <text :class="['display-text', !formData.sendArea ? 'placeholder' : '']">{{
  105. sendAreaLabel || '选择省/市/区' }}</text>
  106. <view class="right-arrow"></view>
  107. </view>
  108. <input class="route-input" v-model="formData.sendAddress" placeholder="详细地址" />
  109. <view class="contact-row">
  110. <input class="route-input half" v-model="formData.sendContact" placeholder="联系人" />
  111. <input class="route-input half" v-model="formData.sendPhone" placeholder="电话"
  112. type="tel" />
  113. </view>
  114. <view class="route-time-trigger" @click="openTimeModal('send')">
  115. <text :class="!formData.sendStartTime ? 'placeholder' : ''">{{
  116. truncateTime(formData.sendStartTime) || '设置送宠时间' }}</text>
  117. </view>
  118. </view>
  119. </view>
  120. <!-- 接送备注 -->
  121. <text class="remarks-title">备注信息</text>
  122. <textarea class="remarks-textarea" v-model="formData.transportNote"
  123. placeholder="请添加接送备注 (如宠物性格、接送要求等)"></textarea>
  124. </view>
  125. </template>
  126. <!-- 03 业务表单 - 上门 -->
  127. <template v-else>
  128. <text class="section-title">03 服务细则</text>
  129. <view class="card feed-card">
  130. <view class="field-item">
  131. <text class="field-label">团购套餐</text>
  132. <input class="field-input" v-model="formData.packageName" placeholder="请输入套餐名称(选填)" />
  133. </view>
  134. <view class="route-box">
  135. <view class="route-icon service">服</view>
  136. <view class="route-fields">
  137. <text class="addr-label require">上门服务地址</text>
  138. <view class="route-picker-trigger" @click="openRegionSelect('service')">
  139. <text :class="['display-text', !formData.serviceArea ? 'placeholder' : '']">{{
  140. serviceAreaLabel || '请选择省/市/区' }}</text>
  141. <view class="right-arrow"></view>
  142. </view>
  143. <input class="route-input" v-model="formData.serviceAddress"
  144. placeholder="详细地址 (街道/路名/门牌号)" />
  145. </view>
  146. </view>
  147. <view class="booking-section">
  148. <view class="booking-header">
  149. <text class="label require">预约服务时间</text>
  150. <text class="count-tag">共 {{ formData.feedTimes.length }} 次</text>
  151. </view>
  152. <view class="time-item-row" v-for="(time, index) in formData.feedTimes" :key="index">
  153. <view class="flex-time-range" @click="openTimeModal('feed', index)">
  154. <text :class="['time-text', !time.start && !time.end ? 'placeholder' : '']">{{
  155. feedTimeDisplay(time) || '开始 ~ 结束' }}</text>
  156. </view>
  157. <view class="action-buttons">
  158. <view class="circle-btn add" v-if="index === formData.feedTimes.length - 1"
  159. @click="addFeedTime">+</view>
  160. <view class="circle-btn remove" v-if="formData.feedTimes.length > 1"
  161. @click="removeFeedTime(index)">-</view>
  162. </view>
  163. </view>
  164. </view>
  165. <text class="remarks-title">备注信息</text>
  166. <textarea class="remarks-textarea" v-model="formData.otherNote"
  167. placeholder="如有其他注意事项请备注"></textarea>
  168. </view>
  169. </template>
  170. <!-- 04 报价信息 -->
  171. <text class="section-title">04 报价信息</text>
  172. <view class="card quote-card">
  173. <view class="field-item">
  174. <text class="field-label require">报价金额</text>
  175. <input class="field-input quote-input" v-model="formData.quoteAmount" type="digit"
  176. placeholder="填入数字" />
  177. <text class="unit-text">元</text>
  178. </view>
  179. <text class="quote-tips">注:此价格将作为订单最终结算金额。</text>
  180. </view>
  181. </view>
  182. <!-- 底部操作栏 -->
  183. <view class="footer-bar safe-bottom">
  184. <view class="quotation-box">
  185. <text class="p-label">总计报价:</text>
  186. <text class="p-symbol">¥</text>
  187. <text class="p-amount">{{ totalFulfillmentCommission }}</text>
  188. </view>
  189. <button class="submit-btn" @click="onSubmit">立即下单</button>
  190. </view>
  191. <!-- 居中联动选择弹窗群 @Author: Antigravity -->
  192. <!-- 宠主搜索弹窗 -->
  193. <page-select v-model="showUserSelect" title="选择宠主用户" searchable :searchKey="userSearchKey"
  194. searchPlaceholder="搜索宠主姓名/手机号" :options="userList" labelKey="name" valueKey="id"
  195. :value="formData.customerId" :loading="userPage.loading" :finished="userPage.finished" emptyText="未找到相关宠主"
  196. @select="onUserSelect" @loadMore="fetchUsers(false)" @search="onUserSearch">
  197. <template #item="{ item }">
  198. <view class="user-info">
  199. <text class="name">{{ item.name }}</text>
  200. <text class="phone">{{ item.phone || item.phoneNumber }}</text>
  201. </view>
  202. </template>
  203. </page-select>
  204. <!-- 区域选择器 (Cascader) @Author: Antigravity -->
  205. <view class="center-modal-mask" v-if="showRegionModal" @click="showRegionModal = false">
  206. <view class="center-modal-content region-modal" @click.stop>
  207. <view class="modal-header"><text class="modal-title">选择区域</text>
  208. <view class="close-btn" @click="showRegionModal = false"></view>
  209. </view>
  210. <view class="cascade-indicator">
  211. <text v-for="(node, idx) in regionPath" :key="idx" class="path-node" @click="backToLevel(idx)">{{
  212. node.name
  213. }}</text>
  214. <text class="path-node active" v-if="regionPath.length < 3">请选择</text>
  215. </view>
  216. <scroll-view scroll-y class="modal-list-scroll">
  217. <view class="list-item" v-for="item in currentRegionList" :key="item.code"
  218. @click="onRegionStepSelect(item)">
  219. <text class="item-text">{{ item.name }}</text>
  220. <view class="checkmark" v-if="isRegionSelected(item)"></view>
  221. </view>
  222. </scroll-view>
  223. </view>
  224. </view>
  225. <!-- 门店选择 -->
  226. <page-select v-model="showShopSelect" title="选择服务门店" searchable :searchKey="shopSearchKey"
  227. searchPlaceholder="搜索门店名称" :options="shopList" labelKey="name" valueKey="id" :value="formData.merchantId"
  228. :loading="shopPage.loading" :finished="shopPage.finished" @select="onShopSelect"
  229. @loadMore="fetchShops(true)" @search="onShopSearch" />
  230. <!-- 宠物选择 -->
  231. <center-select v-model="showPetPopup" title="选择指定宠物" :options="petOptions" labelKey="_label" valueKey="id"
  232. :value="formData.petId" @select="onPetSelect" />
  233. <!-- 日期时间选择弹窗 @Author: Antigravity -->
  234. <view class="center-modal-mask" v-if="showTimeModal" @click="showTimeModal = false">
  235. <view class="center-modal-content time-modal" @click.stop>
  236. <view class="modal-header"><text class="modal-title">选择预约时间</text></view>
  237. <view class="datetime-picker-body">
  238. <template v-if="isDualTimePicker">
  239. <view class="time-slot-row">
  240. <view :class="['time-slot', { active: activeSlot === 'start' }]"
  241. @click="activeSlot = 'start'">
  242. <text class="slot-label">开始时间</text>
  243. <text :class="['slot-value', !tempStartDisplay ? 'placeholder' : '']">{{
  244. tempStartDisplay || '请选择' }}</text>
  245. </view>
  246. <view :class="['time-slot', { active: activeSlot === 'end' }]" @click="activeSlot = 'end'">
  247. <text class="slot-label">结束时间</text>
  248. <text :class="['slot-value', !tempEndDisplay ? 'placeholder' : '']">{{ tempEndDisplay ||
  249. '请选择' }}</text>
  250. </view>
  251. </view>
  252. <picker-view class="picker-view" :value="tempTimeIdx" @change="onTempTimeChange">
  253. <picker-view-column>
  254. <view class="picker-item" v-for="d in timeRanges[0]" :key="d">{{ d }}</view>
  255. </picker-view-column>
  256. <picker-view-column>
  257. <view class="picker-item" v-for="h in timeRanges[1]" :key="h">{{ h }}时</view>
  258. </picker-view-column>
  259. <picker-view-column>
  260. <view class="picker-item" v-for="m in timeRanges[2]" :key="m">{{ m }}分</view>
  261. </picker-view-column>
  262. </picker-view>
  263. </template>
  264. <template v-else>
  265. <picker-view class="picker-view" :value="tempSingleIdx"
  266. @change="(e) => tempSingleIdx = e.detail.value">
  267. <picker-view-column>
  268. <view class="picker-item" v-for="d in timeRanges[0]" :key="d">{{ d }}</view>
  269. </picker-view-column>
  270. <picker-view-column>
  271. <view class="picker-item" v-for="h in timeRanges[1]" :key="h">{{ h }}时</view>
  272. </picker-view-column>
  273. <picker-view-column>
  274. <view class="picker-item" v-for="m in timeRanges[2]" :key="m">{{ m }}分</view>
  275. </picker-view-column>
  276. </picker-view>
  277. </template>
  278. </view>
  279. <view class="modal-footer">
  280. <view class="modal-cancel" @click="showTimeModal = false">取消</view>
  281. <view class="modal-confirm" @click="confirmTime">确认</view>
  282. </view>
  283. </view>
  284. </view>
  285. </view>
  286. </template>
  287. <script setup>
  288. /**
  289. * @Author: Antigravity
  290. */
  291. import { ref, reactive, computed, watch } from 'vue'
  292. import { onLoad } from '@dcloudio/uni-app'
  293. import navBar from '@/components/nav-bar/index.vue'
  294. import pageSelect from '@/components/page-select/index.vue'
  295. import centerSelect from '@/components/center-select/index.vue'
  296. import { listStoreOnOrder } from '@/api/system/store'
  297. import { listCustomerOnOrder } from '@/api/archieves/customer'
  298. import { listPetByUser } from '@/api/archieves/pet'
  299. import { createOrder } from '@/api/order/order'
  300. import { listRegionTree } from '@/api/system/region'
  301. const activeService = ref('transport')
  302. const serviceInfo = ref(null)
  303. const shopList = ref([])
  304. const userList = ref([])
  305. const petList = ref([])
  306. const regionTree = ref([])
  307. // 弹窗控制
  308. const showShopSelect = ref(false)
  309. const showUserSelect = ref(false)
  310. const showPetPopup = ref(false)
  311. const showRegionModal = ref(false)
  312. const showTimeModal = ref(false)
  313. const userSearchKey = ref('')
  314. const shopSearchKey = ref('')
  315. const selectedUser = ref(null)
  316. const selectedShop = ref(null)
  317. // 门店分页状态
  318. const shopPage = reactive({ pageNum: 1, pageSize: 20, loading: false, finished: false })
  319. // 宠主分页状态
  320. const userPage = reactive({ pageNum: 1, pageSize: 20, loading: false, finished: false })
  321. const pickAreaLabel = ref('')
  322. const pickEndAreaLabel = ref('')
  323. const sendStartAreaLabel = ref('')
  324. const sendAreaLabel = ref('')
  325. const serviceAreaLabel = ref('')
  326. const formData = reactive({
  327. merchantId: '', shopName: '', customerId: '', customerName: '', petId: '', petName: '',
  328. packageName: '', transportMode: 'round_trip',
  329. pickArea: '', pickAddress: '', pickEndArea: '', pickEndAddress: '', pickContact: '', pickPhone: '',
  330. pickStartTime: '', pickEndTime: '',
  331. sendStartArea: '', sendStartAddress: '', sendArea: '', sendAddress: '', sendContact: '', sendPhone: '',
  332. sendStartTime: '', sendEndTime: '',
  333. serviceArea: '', serviceAddress: '', feedTimes: [{ start: '', end: '' }],
  334. otherNote: '', transportNote: '', quoteAmount: ''
  335. })
  336. // 时间选择器逻辑 (5分钟一个间隔 @Author: Antigravity)
  337. const timeRanges = ref([[], [], []])
  338. const activeSlot = ref('start')
  339. const tempStartIdx = ref([0, 0, 0])
  340. const tempEndIdx = ref([0, 0, 0])
  341. const tempTimeIdx = ref([0, 0, 0])
  342. const tempSingleIdx = ref([0, 0, 0])
  343. const timeCtx = reactive({ type: '', index: 0 })
  344. const isDualTimePicker = computed(() => timeCtx.type === 'feed')
  345. onLoad((options) => {
  346. if (options.service) activeService.value = options.service
  347. const stored = uni.getStorageSync('currentService')
  348. if (stored) serviceInfo.value = stored
  349. initTimeRanges()
  350. fetchShops(); fetchUsers(true); fetchRegionTree()
  351. })
  352. const initTimeRanges = () => {
  353. const dates = []
  354. const now = new Date()
  355. for (let i = 0; i < 30; i++) {
  356. const d = new Date(now); d.setDate(d.getDate() + i)
  357. dates.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`)
  358. }
  359. timeRanges.value = [
  360. dates,
  361. Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')),
  362. // 五分钟间隔生成 @Author: Antigravity
  363. Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0'))
  364. ]
  365. }
  366. const openTimeModal = (type, index = 0) => {
  367. timeCtx.type = type; timeCtx.index = index
  368. activeSlot.value = 'start'
  369. const readTime = (t) => {
  370. if (!t) return null
  371. const match = t.match(/(\d{2})-(\d{2}) (\d{2}):(\d{2})/)
  372. if (!match) return null
  373. const monthDay = `${match[1]}-${match[2]}`
  374. const di = timeRanges.value[0].findIndex(d => d === monthDay)
  375. const hi = parseInt(match[3])
  376. const mi = timeRanges.value[2].findIndex(m => parseInt(m) === parseInt(match[4]))
  377. return [di < 0 ? 0 : di, hi < 0 ? 0 : hi, mi < 0 ? 0 : mi]
  378. }
  379. const nowIdx = () => {
  380. const now = new Date()
  381. const monthDay = `${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
  382. const di = timeRanges.value[0].findIndex(d => d === monthDay)
  383. const hi = now.getHours()
  384. const mi = timeRanges.value[2].findIndex(m => parseInt(m) >= now.getMinutes())
  385. return [di < 0 ? 0 : di, hi < 0 ? 0 : hi, mi < 0 ? 0 : mi]
  386. }
  387. if (type === 'feed') {
  388. tempStartIdx.value = readTime(formData.feedTimes[index].start) || nowIdx()
  389. tempEndIdx.value = readTime(formData.feedTimes[index].end) || nowIdx()
  390. tempTimeIdx.value = [...tempStartIdx.value]
  391. } else if (type === 'pick') {
  392. tempSingleIdx.value = readTime(formData.pickStartTime) || nowIdx()
  393. } else if (type === 'send') {
  394. tempSingleIdx.value = readTime(formData.sendStartTime) || nowIdx()
  395. }
  396. showTimeModal.value = true
  397. }
  398. const onTempTimeChange = (e) => {
  399. tempTimeIdx.value = e.detail.value
  400. if (activeSlot.value === 'start') {
  401. tempStartIdx.value = [...e.detail.value]
  402. } else {
  403. tempEndIdx.value = [...e.detail.value]
  404. }
  405. }
  406. const buildTimeLabel = (idx) => {
  407. const d = timeRanges.value[0][idx[0]]
  408. const h = timeRanges.value[1][idx[1]]
  409. const m = timeRanges.value[2][idx[2]]
  410. return d && h !== undefined && m !== undefined ? `${d} ${h}:${m}` : ''
  411. }
  412. const tempStartDisplay = computed(() => buildTimeLabel(tempStartIdx.value))
  413. const tempEndDisplay = computed(() => buildTimeLabel(tempEndIdx.value))
  414. watch(activeSlot, (val) => {
  415. tempTimeIdx.value = val === 'start' ? [...tempStartIdx.value] : [...tempEndIdx.value]
  416. })
  417. const confirmTime = () => {
  418. const buildTime = (idx) => {
  419. const [di, hi, mi] = idx
  420. return `${new Date().getFullYear()}-${timeRanges.value[0][di]} ${timeRanges.value[1][hi]}:${timeRanges.value[2][mi]}:00`
  421. }
  422. if (timeCtx.type === 'feed') {
  423. const startVal = buildTime(tempStartIdx.value)
  424. const endVal = buildTime(tempEndIdx.value)
  425. formData.feedTimes[timeCtx.index].start = startVal
  426. formData.feedTimes[timeCtx.index].end = endVal
  427. } else if (timeCtx.type === 'pick') {
  428. const val = buildTime(tempSingleIdx.value)
  429. formData.pickStartTime = val
  430. formData.pickEndTime = val
  431. } else if (timeCtx.type === 'send') {
  432. const val = buildTime(tempSingleIdx.value)
  433. formData.sendStartTime = val
  434. formData.sendEndTime = val
  435. }
  436. showTimeModal.value = false
  437. }
  438. const truncateTime = (t) => t ? t.substring(5, 16) : ''
  439. const feedTimeDisplay = (time) => {
  440. const s = truncateTime(time.start)
  441. const e = truncateTime(time.end)
  442. return s && e ? `${s} ~ ${e}` : (s || e || '')
  443. }
  444. const pickTimeDisplay = computed(() => {
  445. const s = truncateTime(formData.pickStartTime)
  446. const e = truncateTime(formData.pickEndTime)
  447. return s && e ? `${s} ~ ${e}` : (s || e || '')
  448. })
  449. const sendTimeDisplay = computed(() => {
  450. const s = truncateTime(formData.sendStartTime)
  451. const e = truncateTime(formData.sendEndTime)
  452. return s && e ? `${s} ~ ${e}` : (s || e || '')
  453. })
  454. // 区域选择逻辑
  455. const regionPath = ref([])
  456. const activeRegionType = ref('')
  457. const currentRegionList = computed(() => {
  458. let list = regionTree.value
  459. for (let node of regionPath.value) {
  460. const found = list.find(l => l.code === node.code)
  461. if (found && found.children) list = found.children
  462. else list = []
  463. }
  464. return list
  465. })
  466. const openRegionSelect = (type) => {
  467. activeRegionType.value = type
  468. regionPath.value = []
  469. showRegionModal.value = true
  470. }
  471. const backToLevel = (idx) => { regionPath.value = regionPath.value.slice(0, idx) }
  472. const onRegionStepSelect = (item) => {
  473. regionPath.value.push({ code: item.code, name: item.name })
  474. if (!item.children || item.children.length === 0 || regionPath.value.length >= 3) {
  475. const fullLabel = regionPath.value.map(p => p.name).join(' / ')
  476. const finalCode = item.code
  477. if (activeRegionType.value === 'pick') { formData.pickArea = finalCode; pickAreaLabel.value = fullLabel }
  478. else if (activeRegionType.value === 'pickEnd') { formData.pickEndArea = finalCode; pickEndAreaLabel.value = fullLabel }
  479. else if (activeRegionType.value === 'sendStart') { formData.sendStartArea = finalCode; sendStartAreaLabel.value = fullLabel }
  480. else if (activeRegionType.value === 'send') { formData.sendArea = finalCode; sendAreaLabel.value = fullLabel }
  481. else if (activeRegionType.value === 'service') { formData.serviceArea = finalCode; serviceAreaLabel.value = fullLabel }
  482. showRegionModal.value = false
  483. }
  484. }
  485. const isRegionSelected = (item) => {
  486. const level = regionPath.value.length
  487. return regionPath.value[level]?.code === item.code
  488. }
  489. // 核心回填逻辑修正 @Author: Antigravity
  490. watch([selectedShop, selectedUser, regionTree], ([shop, user, tree]) => {
  491. if (!shop && !user) return
  492. // 处理门店信息
  493. const storeAreaCode = (shop?.areaCode || '').replace(/,/g, '/')
  494. const storeLeaf = storeAreaCode.split('/').pop() || ''
  495. const storePath = findRegionLabel(storeAreaCode, tree)
  496. // 处理用户信息
  497. const userAreaCode = (user?.regionCode || '')
  498. const userLeaf = userAreaCode.split('/').pop() || ''
  499. const userPath = findRegionLabel(userAreaCode, tree)
  500. if (shop) {
  501. formData.merchantId = shop.id; formData.shopName = shop.name
  502. // 接送单终点 = 门店
  503. formData.pickEndArea = storeLeaf; formData.pickEndAddress = shop.address || ''
  504. pickEndAreaLabel.value = storePath
  505. // 接送单起点 = 门店
  506. formData.sendStartArea = storeLeaf; formData.sendStartAddress = shop.address || ''
  507. sendStartAreaLabel.value = storePath
  508. }
  509. if (user) {
  510. formData.customerId = user.id; formData.customerName = user.name
  511. // 接宠单起点 = 宠主家
  512. formData.pickArea = userLeaf; formData.pickAddress = user.address || ''
  513. pickAreaLabel.value = userPath
  514. // 送宠单终点 = 宠主家
  515. formData.sendArea = userLeaf; formData.sendAddress = user.address || ''
  516. sendAreaLabel.value = userPath
  517. // 服务单地址 = 宠主家
  518. formData.serviceArea = userLeaf; formData.serviceAddress = user.address || ''
  519. serviceAreaLabel.value = userPath
  520. formData.pickContact = user.name; formData.pickPhone = user.phoneNumber || user.phone || ''
  521. formData.sendContact = user.name; formData.sendPhone = formData.pickPhone
  522. }
  523. }, { deep: true })
  524. const findRegionLabel = (code, list) => {
  525. if (!code || !list || list.length === 0) return ''
  526. const target = code.split('/').pop()
  527. const find = (nodes, t) => {
  528. for (let n of nodes) {
  529. if (n.code === t) return n.name
  530. if (n.children) {
  531. const res = find(n.children, t)
  532. if (res) return n.name + ' / ' + res
  533. }
  534. }
  535. return null
  536. }
  537. return find(list, target) || ''
  538. }
  539. const fetchShops = (loadMore = false) => {
  540. if (shopPage.loading) return
  541. if (loadMore && shopPage.finished) return
  542. if (!loadMore) {
  543. shopPage.pageNum = 1
  544. shopPage.finished = false
  545. }
  546. shopPage.loading = true
  547. listStoreOnOrder({ pageNum: shopPage.pageNum, pageSize: shopPage.pageSize, serviceId: serviceInfo.value?.id, name: shopSearchKey.value }).then(res => {
  548. const rows = res.rows || []
  549. if (loadMore) {
  550. shopList.value = [...shopList.value, ...rows]
  551. } else {
  552. shopList.value = rows
  553. }
  554. shopPage.finished = rows.length < shopPage.pageSize
  555. shopPage.pageNum++
  556. }).catch(e => {
  557. uni.showToast({ title: typeof e === 'string' ? e : '加载门店失败', icon: 'none' })
  558. }).finally(() => { shopPage.loading = false })
  559. }
  560. const fetchUsers = (reset = false) => {
  561. if (userPage.loading) return
  562. if (!reset && userPage.finished) return
  563. if (reset) {
  564. userPage.pageNum = 1
  565. userPage.finished = false
  566. }
  567. userPage.loading = true
  568. listCustomerOnOrder({ pageNum: userPage.pageNum, pageSize: userPage.pageSize, content: userSearchKey.value }).then(res => {
  569. const rows = res.rows || []
  570. if (reset) {
  571. userList.value = rows
  572. } else {
  573. userList.value = [...userList.value, ...rows]
  574. }
  575. userPage.finished = rows.length < userPage.pageSize
  576. userPage.pageNum++
  577. }).catch(e => {
  578. uni.showToast({ title: typeof e === 'string' ? e : '加载宠主失败', icon: 'none' })
  579. }).finally(() => { userPage.loading = false })
  580. }
  581. const fetchPets = (uid) => listPetByUser(uid).then(res => { petList.value = Array.isArray(res) ? res : (res.rows || []) }).catch(e => { uni.showToast({ title: typeof e === 'string' ? e : '加载宠物列表失败', icon: 'none' }) })
  582. const fetchRegionTree = () => listRegionTree().then(res => { regionTree.value = res || [] }).catch(e => { uni.showToast({ title: typeof e === 'string' ? e : '加载区域数据失败', icon: 'none' }) })
  583. const onShopSelect = (shop) => { selectedShop.value = shop; showShopSelect.value = false }
  584. const onShopSearch = (keyword) => { shopSearchKey.value = keyword; fetchShops(false) }
  585. const onUserSearch = (keyword) => { userSearchKey.value = keyword; fetchUsers(true) }
  586. const onUserSelect = (user) => {
  587. selectedUser.value = user; formData.customerId = user.id;
  588. formData.petId = ''; formData.petName = ''; petList.value = []; fetchPets(user.id)
  589. showUserSelect.value = false
  590. }
  591. const openPetPicker = () => { if (!formData.customerId) return uni.showToast({ title: '先选择宠主', icon: 'none' }); showPetPopup.value = true }
  592. const petOptions = computed(() => petList.value.map(p => ({ ...p, _label: `${p.name} (${p.breed || '未知'})` })))
  593. const onPetSelect = (pet) => { formData.petId = pet.id; formData.petName = pet.name; showPetPopup.value = false }
  594. const currentServiceName = computed(() => serviceInfo.value?.name || (activeService.value === 'transport' ? '宠物接送' : '上门喂遛'))
  595. const serviceIconClass = computed(() => activeService.value)
  596. const serviceDesc = computed(() => serviceInfo.value?.remark || '专人专项 · 贴心呵护')
  597. const transportModes = [{ label: '往返', value: 'round_trip' }, { label: '单程接', value: 'pick_up' }, { label: '单程送', value: 'return_home' }]
  598. const addFeedTime = () => formData.feedTimes.push({ start: '', end: '' })
  599. const removeFeedTime = (idx) => formData.feedTimes.splice(idx, 1)
  600. const totalFulfillmentCommission = computed(() => formData.quoteAmount ? parseFloat(formData.quoteAmount).toFixed(2) : '0.00')
  601. const onSubmit = async () => {
  602. if (!formData.merchantId || !formData.customerId || !formData.petId || !formData.quoteAmount) return uni.showToast({ title: '请完善红星必填项', icon: 'none' })
  603. uni.showLoading({ title: '提交中...', mask: true })
  604. try {
  605. const subOrders = []
  606. const baseMode = serviceInfo.value?.mode || 0
  607. const defC = selectedUser.value?.name; const defP = selectedUser.value?.phone || selectedUser.value?.phoneNumber
  608. if (activeService.value === 'transport') {
  609. if (formData.transportMode !== 'return_home') subOrders.push({ mode: baseMode, type: formData.transportMode === 'round_trip' ? 0 : 2, contact: formData.pickContact || defC, contactPhoneNumber: formData.pickPhone || defP, serviceTime: formData.pickStartTime, endServiceTime: formData.pickEndTime, fromCode: formData.pickArea, fromAddress: formData.pickAddress, toCode: formData.pickEndArea, toAddress: formData.pickEndAddress })
  610. if (formData.transportMode !== 'pick_up') subOrders.push({ mode: baseMode, type: formData.transportMode === 'round_trip' ? 1 : 3, contact: formData.sendContact || defC, contactPhoneNumber: formData.sendPhone || defP, serviceTime: formData.sendStartTime, endServiceTime: formData.sendEndTime, fromCode: formData.sendStartArea, fromAddress: formData.sendStartAddress, toCode: formData.sendArea, toAddress: formData.sendAddress })
  611. } else {
  612. formData.feedTimes.forEach(t => subOrders.push({ mode: baseMode, contact: defC, contactPhoneNumber: defP, serviceTime: t.start, endServiceTime: t.end || t.start, fromCode: formData.serviceArea, fromAddress: formData.serviceAddress, toCode: formData.serviceArea, toAddress: formData.serviceAddress }))
  613. }
  614. // 根据服务类型选择对应备注字段(与Web端逻辑对齐:transport用transportNote,其他用otherNote)
  615. const orderRemark = activeService.value === 'transport' ? formData.transportNote : formData.otherNote
  616. const payload = { store: formData.merchantId, storeSite: selectedShop.value?.site, customer: formData.customerId, pet: formData.petId, groupPurchasePackageName: formData.packageName, service: serviceInfo.value?.id, orderCommission: Math.round(Number(formData.quoteAmount) * 100), remark: orderRemark, tenantId: selectedShop.value?.tenantId, subOrders }
  617. await createOrder(payload)
  618. uni.showToast({ title: '成功', icon: 'success' })
  619. setTimeout(() => uni.reLaunch({ url: '/pages/order/list/index' }), 1000)
  620. } catch (e) {
  621. uni.showToast({ title: typeof e === 'string' ? e : '下单失败', icon: 'none' })
  622. } finally { uni.hideLoading() }
  623. }
  624. </script>
  625. <style lang="scss" scoped>
  626. /* 统一页面字体栈 @Author: Antigravity */
  627. .order-apply-page {
  628. background: #f7f8fa;
  629. min-height: 100vh;
  630. padding-bottom: 220rpx;
  631. font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'STHeitiSTXihei', 'Microsoft YaHei', Arial, sans-serif;
  632. }
  633. .apply-content {
  634. padding: 0 28rpx;
  635. }
  636. .section-title {
  637. display: flex;
  638. align-items: center;
  639. font-size: 28rpx;
  640. font-weight: bold;
  641. color: #333;
  642. margin: 32rpx 0 20rpx;
  643. &::before {
  644. content: '';
  645. width: 8rpx;
  646. height: 26rpx;
  647. background: #f7ca3e;
  648. margin-right: 16rpx;
  649. border-radius: 4rpx;
  650. }
  651. }
  652. .card {
  653. background: #fff;
  654. border-radius: 24rpx;
  655. padding: 24rpx;
  656. margin-bottom: 24rpx;
  657. }
  658. .service-type-display {
  659. display: flex;
  660. align-items: center;
  661. gap: 24rpx;
  662. }
  663. .service-icon-box {
  664. width: 88rpx;
  665. height: 88rpx;
  666. border-radius: 20rpx;
  667. display: flex;
  668. align-items: center;
  669. justify-content: center;
  670. }
  671. .service-icon-box.transport {
  672. background: linear-gradient(135deg, #64b5f6, #2196f3);
  673. }
  674. .service-icon-box.feed {
  675. background: linear-gradient(135deg, #ffb74d, #ff9800);
  676. }
  677. .service-icon-box.wash {
  678. background: linear-gradient(135deg, #81c784, #4caf50);
  679. }
  680. .main-name {
  681. display: block;
  682. font-size: 32rpx;
  683. font-weight: bold;
  684. color: #333;
  685. }
  686. .sub-desc {
  687. display: block;
  688. font-size: 24rpx;
  689. color: #999;
  690. margin-top: 4rpx;
  691. }
  692. /* CSS 手绘图标 @Author: Antigravity */
  693. .pure-css-icon {
  694. width: 40rpx;
  695. height: 40rpx;
  696. border: 4rpx solid #fff;
  697. border-radius: 8rpx;
  698. position: relative;
  699. &::after {
  700. content: '';
  701. position: absolute;
  702. top: 10rpx;
  703. left: 10rpx;
  704. width: 12rpx;
  705. height: 12rpx;
  706. background: #fff;
  707. border-radius: 50%;
  708. }
  709. }
  710. .field-item {
  711. display: flex;
  712. align-items: center;
  713. padding: 28rpx 0;
  714. border-bottom: 2rpx solid #f5f5f5;
  715. height: 44rpx;
  716. &:last-child {
  717. border-bottom: none;
  718. }
  719. }
  720. .field-label {
  721. width: 180rpx;
  722. font-size: 28rpx;
  723. color: #333;
  724. flex-shrink: 0;
  725. }
  726. .require::before {
  727. content: '*';
  728. color: #f56c6c;
  729. margin-right: 4rpx;
  730. }
  731. .field-value {
  732. flex: 1;
  733. font-size: 28rpx;
  734. color: #333;
  735. text-align: right;
  736. margin-right: 16rpx;
  737. }
  738. .field-value.placeholder {
  739. color: #ccc;
  740. }
  741. .field-value-wrap {
  742. flex: 1;
  743. display: flex;
  744. flex-direction: column;
  745. align-items: flex-end;
  746. margin-right: 16rpx;
  747. .selected-name {
  748. font-size: 28rpx;
  749. font-weight: bold;
  750. color: #333;
  751. }
  752. .selected-phone {
  753. font-size: 22rpx;
  754. color: #999;
  755. }
  756. }
  757. .placeholder {
  758. color: #ccc;
  759. font-size: 28rpx;
  760. }
  761. .mode-select {
  762. display: flex;
  763. gap: 16rpx;
  764. margin: 20rpx 0 32rpx;
  765. }
  766. .mode-btn {
  767. flex: 1;
  768. height: 60rpx;
  769. display: flex;
  770. align-items: center;
  771. justify-content: center;
  772. border: 2rpx solid #f0f0f0;
  773. border-radius: 30rpx;
  774. font-size: 24rpx;
  775. color: #666;
  776. &.active {
  777. background: #fef8e5;
  778. border-color: #f7ca3e;
  779. color: #f7ca3e;
  780. font-weight: bold;
  781. }
  782. }
  783. .route-box {
  784. display: flex;
  785. gap: 20rpx;
  786. margin-bottom: 30rpx;
  787. }
  788. .route-icon {
  789. width: 44rpx;
  790. height: 44rpx;
  791. border-radius: 8rpx;
  792. color: #fff;
  793. display: flex;
  794. align-items: center;
  795. justify-content: center;
  796. font-size: 22rpx;
  797. font-weight: bold;
  798. flex-shrink: 0;
  799. margin-top: 10rpx;
  800. }
  801. .route-icon.pick {
  802. background: #5bb7ff;
  803. }
  804. .route-icon.send {
  805. background: #64cf5c;
  806. }
  807. .route-icon.service {
  808. background: #ff9500;
  809. }
  810. .route-fields {
  811. flex: 1;
  812. display: flex;
  813. flex-direction: column;
  814. gap: 6rpx;
  815. }
  816. .addr-label {
  817. font-size: 22rpx;
  818. color: #999;
  819. margin-top: 10rpx;
  820. }
  821. .route-picker-trigger {
  822. height: 64rpx;
  823. border-bottom: 2rpx solid #f5f5f5;
  824. display: flex;
  825. align-items: center;
  826. justify-content: space-between;
  827. .display-text {
  828. font-size: 26rpx;
  829. color: #333;
  830. &.placeholder {
  831. color: #ccc;
  832. }
  833. }
  834. }
  835. .route-input {
  836. height: 72rpx;
  837. font-size: 26rpx;
  838. border-bottom: 2rpx solid #f5f5f5;
  839. &.half {
  840. flex: 1;
  841. }
  842. }
  843. .contact-row {
  844. display: flex;
  845. gap: 16rpx;
  846. }
  847. .route-time-trigger {
  848. height: 72rpx;
  849. background: #f9f9f9;
  850. border-radius: 12rpx;
  851. display: flex;
  852. align-items: center;
  853. padding: 0 20rpx;
  854. font-size: 26rpx;
  855. color: #333;
  856. margin-top: 10rpx;
  857. .placeholder {
  858. color: #ccc;
  859. }
  860. }
  861. .address-title,
  862. .form-item-label,
  863. .remarks-title {
  864. display: block;
  865. font-size: 26rpx;
  866. color: #666;
  867. margin: 20rpx 0 10rpx;
  868. }
  869. .booking-header {
  870. display: flex;
  871. justify-content: space-between;
  872. align-items: center;
  873. margin: 20rpx 0 10rpx;
  874. }
  875. .booking-header .label {
  876. display: block;
  877. font-size: 26rpx;
  878. color: #666;
  879. }
  880. .booking-section {
  881. margin-top: 24rpx;
  882. }
  883. .count-tag {
  884. font-size: 20rpx;
  885. color: #f44336;
  886. padding: 4rpx 0;
  887. }
  888. .time-item-row {
  889. display: flex;
  890. align-items: center;
  891. gap: 12rpx;
  892. margin-bottom: 16rpx;
  893. }
  894. .flex-time-range {
  895. flex: 1;
  896. height: 64rpx;
  897. background: #fcfcfc;
  898. border: 2rpx solid #eee;
  899. border-radius: 10rpx;
  900. display: flex;
  901. align-items: center;
  902. justify-content: center;
  903. .time-text {
  904. font-size: 24rpx;
  905. color: #333;
  906. &.placeholder {
  907. color: #ccc;
  908. }
  909. }
  910. }
  911. .action-buttons {
  912. display: flex;
  913. gap: 12rpx;
  914. margin-left: 8rpx;
  915. }
  916. .circle-btn {
  917. width: 44rpx;
  918. height: 44rpx;
  919. border-radius: 50%;
  920. display: flex;
  921. align-items: center;
  922. justify-content: center;
  923. font-size: 32rpx;
  924. font-weight: bold;
  925. &.add {
  926. background: #e3f2fd;
  927. color: #2196f3;
  928. }
  929. &.remove {
  930. background: #fde2e2;
  931. color: #f56c6c;
  932. }
  933. }
  934. .remarks-textarea {
  935. width: 100%;
  936. height: 140rpx;
  937. font-size: 26rpx;
  938. background: #f9f9f9;
  939. border-radius: 16rpx;
  940. padding: 16rpx;
  941. box-sizing: border-box;
  942. }
  943. .quote-input {
  944. flex: 1;
  945. font-size: 36rpx;
  946. color: #f44336;
  947. font-weight: bold;
  948. text-align: right;
  949. }
  950. .unit-text {
  951. font-size: 28rpx;
  952. color: #333;
  953. margin-left: 8rpx;
  954. }
  955. .quote-tips {
  956. display: block;
  957. font-size: 22rpx;
  958. color: #999;
  959. margin-top: 20rpx;
  960. }
  961. .footer-bar {
  962. position: fixed;
  963. bottom: 0;
  964. left: 0;
  965. right: 0;
  966. background: #fff;
  967. padding: 24rpx 32rpx;
  968. display: flex;
  969. align-items: center;
  970. justify-content: space-between;
  971. box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
  972. z-index: 100;
  973. }
  974. .quotation-box {
  975. display: flex;
  976. align-items: baseline;
  977. .p-label {
  978. font-size: 24rpx;
  979. color: #333;
  980. }
  981. .p-symbol {
  982. font-size: 28rpx;
  983. color: #f44336;
  984. font-weight: bold;
  985. margin-left: 8rpx;
  986. }
  987. .p-amount {
  988. font-size: 40rpx;
  989. font-weight: 900;
  990. color: #f44336;
  991. }
  992. }
  993. .submit-btn {
  994. width: 280rpx;
  995. height: 84rpx;
  996. background: linear-gradient(90deg, #ffd53f, #ff9500);
  997. color: #fff;
  998. border-radius: 42rpx;
  999. font-size: 28rpx;
  1000. font-weight: bold;
  1001. line-height: 84rpx;
  1002. &::after {
  1003. border: none;
  1004. }
  1005. }
  1006. /* CSS Common Modal UI @Author: Antigravity */
  1007. .center-modal-mask {
  1008. position: fixed;
  1009. top: 0;
  1010. left: 0;
  1011. right: 0;
  1012. bottom: 0;
  1013. background: rgba(0, 0, 0, 0.6);
  1014. z-index: 10000;
  1015. display: flex;
  1016. align-items: center;
  1017. justify-content: center;
  1018. backdrop-filter: blur(4rpx);
  1019. }
  1020. .center-modal-content {
  1021. width: 620rpx;
  1022. background: #fff;
  1023. border-radius: 32rpx;
  1024. display: flex;
  1025. flex-direction: column;
  1026. overflow: hidden;
  1027. animation: popIn 0.3s ease-out;
  1028. }
  1029. @keyframes popIn {
  1030. from {
  1031. transform: scale(0.9);
  1032. opacity: 0;
  1033. }
  1034. to {
  1035. transform: scale(1);
  1036. opacity: 1;
  1037. }
  1038. }
  1039. .modal-header {
  1040. padding: 32rpx;
  1041. border-bottom: 2rpx solid #f2f2f2;
  1042. position: relative;
  1043. text-align: center;
  1044. .modal-title {
  1045. font-size: 30rpx;
  1046. font-weight: bold;
  1047. color: #333;
  1048. }
  1049. }
  1050. .close-btn {
  1051. position: absolute;
  1052. right: 24rpx;
  1053. top: 24rpx;
  1054. width: 44rpx;
  1055. height: 44rpx;
  1056. &::before,
  1057. &::after {
  1058. content: '';
  1059. position: absolute;
  1060. top: 20rpx;
  1061. left: 8rpx;
  1062. width: 28rpx;
  1063. height: 4rpx;
  1064. background: #999;
  1065. transform: rotate(45deg);
  1066. border-radius: 4rpx;
  1067. }
  1068. &::after {
  1069. transform: rotate(-45deg);
  1070. }
  1071. }
  1072. .search-box {
  1073. display: flex;
  1074. align-items: center;
  1075. background: #f5f5f5;
  1076. border-radius: 36rpx;
  1077. padding: 0 24rpx;
  1078. height: 72rpx;
  1079. margin: 0 4rpx;
  1080. .search-icon {
  1081. width: 20rpx;
  1082. height: 20rpx;
  1083. border: 3rpx solid #999;
  1084. border-radius: 50%;
  1085. margin-right: 12rpx;
  1086. position: relative;
  1087. &::after {
  1088. content: '';
  1089. width: 10rpx;
  1090. height: 3rpx;
  1091. background: #999;
  1092. position: absolute;
  1093. bottom: -4rpx;
  1094. right: -4rpx;
  1095. transform: rotate(45deg);
  1096. }
  1097. }
  1098. .search-input {
  1099. flex: 1;
  1100. font-size: 26rpx;
  1101. }
  1102. .search-btn {
  1103. font-size: 26rpx;
  1104. color: #ff9500;
  1105. font-weight: bold;
  1106. margin-left: 20rpx;
  1107. }
  1108. }
  1109. .modal-list-scroll {
  1110. flex: 1;
  1111. max-height: 55vh;
  1112. padding: 0 32rpx;
  1113. }
  1114. .list-item {
  1115. display: flex;
  1116. align-items: center;
  1117. justify-content: space-between;
  1118. padding: 30rpx 0;
  1119. border-bottom: 2rpx solid #f9f9f9;
  1120. .user-info {
  1121. display: flex;
  1122. flex-direction: column;
  1123. .name {
  1124. font-size: 28rpx;
  1125. font-weight: bold;
  1126. color: #333;
  1127. }
  1128. .phone {
  1129. font-size: 22rpx;
  1130. color: #999;
  1131. margin-top: 4rpx;
  1132. }
  1133. }
  1134. }
  1135. .checkmark {
  1136. width: 12rpx;
  1137. height: 22rpx;
  1138. border-right: 4rpx solid #ff9500;
  1139. border-bottom: 4rpx solid #ff9500;
  1140. transform: rotate(45deg);
  1141. }
  1142. .cascade-indicator {
  1143. display: flex;
  1144. padding: 20rpx 32rpx;
  1145. background: #fafafa;
  1146. border-bottom: 2rpx solid #f2f2f2;
  1147. flex-wrap: wrap;
  1148. gap: 12rpx;
  1149. .path-node {
  1150. font-size: 24rpx;
  1151. color: #666;
  1152. &.active {
  1153. color: #ff9500;
  1154. font-weight: bold;
  1155. }
  1156. }
  1157. }
  1158. .datetime-picker-body {
  1159. padding: 20rpx 0;
  1160. }
  1161. .time-slot-row {
  1162. display: flex;
  1163. padding: 0 20rpx;
  1164. margin-bottom: 20rpx;
  1165. gap: 16rpx;
  1166. }
  1167. .time-slot {
  1168. flex: 1;
  1169. border: 2rpx solid #eee;
  1170. border-radius: 12rpx;
  1171. padding: 16rpx;
  1172. text-align: center;
  1173. &.active {
  1174. border-color: #ff9500;
  1175. background: #fff8f0;
  1176. }
  1177. }
  1178. .slot-label {
  1179. display: block;
  1180. font-size: 22rpx;
  1181. color: #999;
  1182. margin-bottom: 6rpx;
  1183. }
  1184. .slot-value {
  1185. display: block;
  1186. font-size: 26rpx;
  1187. color: #333;
  1188. font-weight: 500;
  1189. &.placeholder {
  1190. color: #ccc;
  1191. }
  1192. }
  1193. .picker-view {
  1194. width: 100%;
  1195. height: 360rpx;
  1196. }
  1197. .picker-item {
  1198. line-height: 80rpx;
  1199. text-align: center;
  1200. font-size: 28rpx;
  1201. }
  1202. .modal-footer {
  1203. display: flex;
  1204. border-top: 2rpx solid #f2f2f2;
  1205. .modal-cancel,
  1206. .modal-confirm {
  1207. flex: 1;
  1208. height: 96rpx;
  1209. line-height: 96rpx;
  1210. text-align: center;
  1211. font-size: 28rpx;
  1212. }
  1213. .modal-confirm {
  1214. color: #ff9500;
  1215. font-weight: bold;
  1216. border-left: 2rpx solid #f2f2f2;
  1217. }
  1218. }
  1219. .right-arrow {
  1220. width: 12rpx;
  1221. height: 12rpx;
  1222. border-right: 3rpx solid #ccc;
  1223. border-top: 3rpx solid #ccc;
  1224. transform: rotate(45deg);
  1225. flex-shrink: 0;
  1226. }
  1227. .empty-tip {
  1228. padding: 80rpx 0;
  1229. text-align: center;
  1230. color: #ccc;
  1231. font-size: 24rpx;
  1232. }
  1233. .user-info {
  1234. display: flex;
  1235. flex-direction: column;
  1236. flex: 1;
  1237. .name {
  1238. font-size: 28rpx;
  1239. font-weight: bold;
  1240. color: #333;
  1241. }
  1242. .phone {
  1243. font-size: 22rpx;
  1244. color: #999;
  1245. margin-top: 4rpx;
  1246. }
  1247. }
  1248. </style>