create_order.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. <template>
  2. <view class="order-container">
  3. <!-- 用户信息卡片 -->
  4. <view class="info-card">
  5. <view class="address-item">
  6. <view class="img-status-text">
  7. <image src="/static/img/icon-send.png" mode="" class="img"></image>
  8. </view>
  9. <view class="user-info">
  10. <AddressInfo v-if="addressSend.addressId" :address="addressSend" />
  11. <view v-else class="create-btn" @click="handleAddAddress('sender')">新建寄件人</view>
  12. </view>
  13. <view class="img-status-text right" @click="openAddressBook('sender')">
  14. <image src="/static/img/create-order-address.png" mode="" class="address-image"></image>
  15. <view class="status-text">地址薄</view>
  16. </view>
  17. </view>
  18. <view class="address-item">
  19. <view class="img-status-text">
  20. <image src="/static/img/create-order-change.png" mode="" class="img-change"></image>
  21. </view>
  22. <view class="user-info">
  23. <view class="line"></view>
  24. </view>
  25. </view>
  26. <view class="address-item">
  27. <view class="img-status-text">
  28. <image src="/static/img/icon-receive.png" mode="" class="img"></image>
  29. </view>
  30. <view class="user-info">
  31. <AddressInfo v-if="addressReceive.addressId" :address="addressReceive" />
  32. <view v-else class="create-btn" @click="handleAddAddress('receiver')">新建收件人</view>
  33. </view>
  34. <view class="img-status-text right" @click="openAddressBook('receiver')">
  35. <image src="/static/img/create-order-address.png" mode="" class="address-image"></image>
  36. <view class="status-text">地址薄</view>
  37. </view>
  38. </view>
  39. </view>
  40. <view class="pickup-title">上门取件</view>
  41. <!-- 物品信息 -->
  42. <view class="goods-card">
  43. <view class="goods-item">
  44. <view class="item-label required">期望上门时间</view>
  45. <view class="time-value" @click="handleTimeClick">
  46. <text class="value">{{ selectedTimeLabel }}</text>
  47. <u-icon class="arrow" name='arrow-right' size="18"></u-icon>
  48. </view>
  49. </view>
  50. <view class="goods-item">
  51. <view class="item-label required">快递类型</view>
  52. <view class="item-control">
  53. <picker :range="expressTypes" :range-key="'label'" @change="onExpressTypeChange"
  54. :value="expressTypeIndex" :disabled="!expressTypes.length">
  55. <view class="picker-text">
  56. {{ expressTypes[expressTypeIndex]?.label || (expressTypes.length ? '请选择快递类型' : '加载中...') }}
  57. </view>
  58. </picker>
  59. <u-icon class="arrow" name='arrow-right' size="18"></u-icon>
  60. </view>
  61. </view>
  62. <view class="goods-item">
  63. <view class="item-label required">物品信息</view>
  64. <view class="item-control">
  65. <input class="input-field" placeholder="请输入物品信息" placeholder-class="placeholder" v-model="goodsInfo"
  66. maxlength="50" @input="onGoodsInfoInput" />
  67. </view>
  68. </view>
  69. <!-- 总体重 -->
  70. <view class="goods-item">
  71. <view class="item-label">总体重(KG)</view>
  72. <view class="item-control btn">
  73. <view class="control-btn minus" @click="decreaseWeight">-</view>
  74. <view class="control-value">
  75. <input class="input-field" placeholder="" placeholder-class="placeholder" :value="weight"
  76. @input="handleWeightInput" type="digit" />
  77. </view>
  78. <view class="control-btn plus" @click="increaseWeight">+</view>
  79. </view>
  80. </view>
  81. <!-- 总体积 -->
  82. <view class="goods-item">
  83. <view class="item-label">总体积(m³)</view>
  84. <view class="item-control btn">
  85. <view class="control-btn minus" @click="decreaseVolume">-</view>
  86. <view class="control-value">
  87. <input class="input-field" placeholder="" placeholder-class="placeholder" :value="volume"
  88. @input="handleVolumeInput" type="digit" />
  89. </view>
  90. <view class="control-btn plus" @click="increaseVolume">+</view>
  91. </view>
  92. </view>
  93. <!-- 件数 -->
  94. <view class="goods-item">
  95. <view class="item-label">件数(件)</view>
  96. <view class="item-control btn">
  97. <view class="control-btn minus" @click="decreaseQuantity">-</view>
  98. <view class="control-value">
  99. <input class="input-field" placeholder="" placeholder-class="placeholder" :value="quantity"
  100. @input="handleQuantityInput" type="number" />
  101. </view>
  102. <view class="control-btn plus" @click="increaseQuantity">+</view>
  103. </view>
  104. </view>
  105. </view>
  106. <view class="pickup-title">增值服务</view>
  107. <!-- 增值服务卡片 -->
  108. <view class="goods-card">
  109. <view class="goods-item">
  110. <view class="item-label">包装服务</view>
  111. <view class="item-control">
  112. <switch color="#007AFF" :checked="valueServices.isPack" @change="onPackagingChange" />
  113. </view>
  114. </view>
  115. <view class="goods-item">
  116. <view class="item-label">保价</view>
  117. <view class="item-control">
  118. <switch color="#007AFF" :checked="!!insuranceAmount" @change="onInsuranceChange" />
  119. </view>
  120. </view>
  121. <view v-if="insuranceAmount !== '' && insuranceAmount !== null" class="goods-item">
  122. <view class="item-label">保价金额(元)</view>
  123. <view class="item-control">
  124. <input class="input-field" placeholder="请输入保价金额" placeholder-class="placeholder"
  125. v-model="insuranceAmount" type="digit" maxlength="10" @input="validateInsuranceAmount" />
  126. </view>
  127. </view>
  128. <!-- 超长超重(顺丰/京东) -->
  129. <view class="goods-item">
  130. <view class="item-label">超长超重</view>
  131. <view class="item-control">
  132. <switch color="#007AFF" :checked="valueServices.isOverLongWeight" @change="onOverweightChange" />
  133. </view>
  134. </view>
  135. <!-- 签单返还 -->
  136. <view class="goods-item">
  137. <view class="item-label">签单返还</view>
  138. <view class="item-control">
  139. <switch color="#007AFF" :checked="valueServices.isReceiptCollect" @change="onSignReturnChange" />
  140. </view>
  141. </view>
  142. <!-- 顺丰:签单返还类型选择 -->
  143. <view v-if="product === '2' && valueServices.isReceiptCollect" class="goods-item">
  144. <view class="item-label">返还类型</view>
  145. <view class="item-control">
  146. <picker :range="receiptReturnTypes" :range-key="'label'" @change="onReceiptReturnTypeChange"
  147. :value="receiptReturnTypeIndex">
  148. <view class="picker-text">{{ receiptReturnTypes[receiptReturnTypeIndex]?.label || '请选择返还类型' }}
  149. </view>
  150. </picker>
  151. <u-icon class="arrow" name='arrow-right' size="18"></u-icon>
  152. </view>
  153. </view>
  154. <!-- 打木架(仅顺丰) -->
  155. <view v-if="product === '2'" class="goods-item">
  156. <view class="item-label">打木架</view>
  157. <view class="item-control">
  158. <switch color="#007AFF" :checked="valueServices.isWoodenCrate" @change="onWoodenFrameChange" />
  159. </view>
  160. </view>
  161. </view>
  162. <!-- 协议同意 -->
  163. <view class="agreement-card">
  164. <view class="agreement-item">
  165. <checkbox color="#1B64F0" :checked="agreed" @click="toggleAgreement" style="transform:scale(0.8)" />
  166. <text class="agreement-text">我已理解并同意遵守《快件服务协议》</text>
  167. </view>
  168. <view class="price-notice">
  169. 实际费用以快递员核实为准,四舍五入取整
  170. </view>
  171. </view>
  172. <!-- 下单按钮 -->
  173. <view class="submit-btn" :class="{ disabled: !agreed }" :disabled="!agreed" @click="submitOrder">
  174. 提交订单
  175. </view>
  176. <!-- 安全区域占位 -->
  177. <view class="safe-area"></view>
  178. <!-- 时间选择弹窗 -->
  179. <TimePopup :visible="showTimePicker" @close="showTimePicker = false" @confirm="handleTimeConfirm" />
  180. </view>
  181. </template>
  182. <script setup>
  183. import {
  184. ref,
  185. computed,
  186. onMounted,
  187. onUnmounted,
  188. watch,
  189. } from 'vue'
  190. import {
  191. onLoad
  192. } from '@dcloudio/uni-app'
  193. import TimePopup from './components/TimePopup.vue'
  194. import AddressInfo from '@/components/AddressInfo.vue'
  195. import {
  196. createOrder,
  197. dictList
  198. } from '../../api/order'
  199. import {
  200. getDefaultAddress
  201. } from '../../api/address'
  202. // ==================== 数据存储 ====================
  203. const getAddressFromStorage = (key) => {
  204. const address = uni.getStorageSync(key)
  205. return address || {
  206. id: '',
  207. name: '',
  208. phone: '',
  209. address: '',
  210. provinceName: '',
  211. cityName: '',
  212. countyName: '',
  213. isDefault: false
  214. }
  215. }
  216. const saveAddressToStorage = (key, address) => {
  217. uni.setStorageSync(key, address)
  218. }
  219. // ==================== 页面参数 ====================
  220. const product = ref('1') // 1-京东,2-顺丰
  221. // ==================== 地址信息 ====================
  222. const addressSend = ref(getAddressFromStorage('senderAddress'))
  223. const addressReceive = ref(getAddressFromStorage('receiverAddress'))
  224. // ==================== 物品信息 ====================
  225. const goodsInfo = ref('')
  226. const weight = ref(1)
  227. const volume = ref(0.001)
  228. const quantity = ref(1)
  229. // ==================== 时间选择 ====================
  230. const showTimePicker = ref(false)
  231. const selectedTimeData = ref(null)
  232. const selectedTimeLabel = computed(() => {
  233. if (!selectedTimeData.value) return '请选择时间'
  234. const {
  235. dateLabel,
  236. timeLabel,
  237. isToday,
  238. isImmediate
  239. } = selectedTimeData.value
  240. if (isToday && isImmediate) return '一小时内'
  241. return `${dateLabel} ${timeLabel}`
  242. })
  243. // ==================== 快递类型字典(动态获取)====================
  244. const jdDictList = ref([]) // 京东快递类型字典
  245. const sfDictList = ref([]) // 顺丰快递类型字典
  246. const expressTypeIndex = ref(0)
  247. const expressTypes = ref([]) // 当前显示的快递类型列表(已转换为 {label, value} 格式)
  248. // 从后端获取字典数据
  249. const fetchDictData = async () => {
  250. try {
  251. uni.showLoading({
  252. title: '加载字典...'
  253. })
  254. // 并发请求两个字典
  255. const [jdRes, sfRes] = await Promise.all([
  256. dictList('jd_logistics_product_code'),
  257. dictList('sf_logistics_product_code')
  258. ])
  259. if (jdRes.code === 200) {
  260. jdDictList.value = jdRes.data || []
  261. }
  262. if (sfRes.code === 200) {
  263. sfDictList.value = sfRes.data || []
  264. }
  265. updateExpressTypes()
  266. } catch (error) {
  267. console.error('获取快递类型字典失败', error)
  268. uni.showToast({
  269. title: '字典加载失败',
  270. icon: 'none'
  271. })
  272. } finally {
  273. uni.hideLoading()
  274. }
  275. }
  276. // 根据 product 更新 expressTypes
  277. const updateExpressTypes = () => {
  278. let sourceList = []
  279. if (product.value === '1') {
  280. sourceList = jdDictList.value
  281. } else if (product.value === '2') {
  282. sourceList = sfDictList.value
  283. }
  284. // 转换为 picker 需要的格式:{ label: dictLabel, value: dictValue }
  285. expressTypes.value = sourceList.map(item => ({
  286. label: item.dictLabel,
  287. value: item.dictValue
  288. }))
  289. // 重置选中索引
  290. expressTypeIndex.value = 0
  291. }
  292. // ==================== 增值服务 ====================
  293. const valueServices = ref({
  294. isPack: false,
  295. isOverLongWeight: false,
  296. isReceiptCollect: false,
  297. isWoodenCrate: false
  298. })
  299. // 保价金额
  300. const insuranceAmount = ref('')
  301. const insuranceAmountError = ref('')
  302. // 签单返还类型(顺丰)
  303. const receiptReturnTypeIndex = ref(0)
  304. const receiptReturnTypes = ref([{
  305. label: '电子凭证',
  306. value: 'electronic'
  307. },
  308. {
  309. label: '纸质凭证',
  310. value: 'paper'
  311. }
  312. ])
  313. // ==================== 协议 ====================
  314. const agreed = ref(false)
  315. // ==================== 表单操作 ====================
  316. // 重量
  317. const increaseWeight = () => {
  318. weight.value = parseFloat((weight.value + 0.5).toFixed(3))
  319. if (weight.value > 100) weight.value = 100
  320. }
  321. const decreaseWeight = () => {
  322. weight.value = parseFloat((weight.value - 0.5).toFixed(3))
  323. if (weight.value < 0.5) weight.value = 0.5
  324. }
  325. const handleWeightInput = (e) => {
  326. let value = e.detail.value.replace(/[^\d.]/g, '')
  327. const parts = value.split('.')
  328. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  329. if (value.includes('.')) {
  330. const decimal = value.split('.')[1]
  331. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  332. }
  333. const num = parseFloat(value) || 0.5
  334. weight.value = Math.max(0.5, Math.min(100, num))
  335. }
  336. // 体积
  337. const increaseVolume = () => {
  338. volume.value = parseFloat((volume.value + 0.001).toFixed(3))
  339. if (volume.value > 10) volume.value = 10
  340. }
  341. const decreaseVolume = () => {
  342. volume.value = parseFloat((volume.value - 0.001).toFixed(3))
  343. if (volume.value < 0.001) volume.value = 0.001
  344. }
  345. const handleVolumeInput = (e) => {
  346. let value = e.detail.value.replace(/[^\d.]/g, '')
  347. const parts = value.split('.')
  348. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  349. if (value.includes('.')) {
  350. const decimal = value.split('.')[1]
  351. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  352. }
  353. const num = parseFloat(value) || 0.001
  354. volume.value = Math.max(0.001, Math.min(10, num))
  355. }
  356. // 件数
  357. const increaseQuantity = () => {
  358. quantity.value += 1
  359. if (quantity.value > 99) quantity.value = 99
  360. }
  361. const decreaseQuantity = () => {
  362. quantity.value -= 1
  363. if (quantity.value < 1) quantity.value = 1
  364. }
  365. const handleQuantityInput = (e) => {
  366. let value = e.detail.value.replace(/\D/g, '')
  367. const int = parseInt(value) || 1
  368. quantity.value = Math.max(1, Math.min(99, int))
  369. }
  370. // 物品信息
  371. const onGoodsInfoInput = (e) => {
  372. goodsInfo.value = e.detail.value
  373. if (goodsInfo.value.length > 50) goodsInfo.value = goodsInfo.value.substring(0, 50)
  374. }
  375. // 快递类型
  376. const onExpressTypeChange = (e) => {
  377. expressTypeIndex.value = e.detail.value
  378. }
  379. // 增值服务开关
  380. const onPackagingChange = (e) => {
  381. valueServices.value.isPack = e.detail.value
  382. }
  383. const onOverweightChange = (e) => {
  384. valueServices.value.isOverLongWeight = e.detail.value
  385. }
  386. const onWoodenFrameChange = (e) => {
  387. valueServices.value.isWoodenCrate = e.detail.value
  388. }
  389. const onInsuranceChange = (e) => {
  390. if (e.detail.value) {
  391. insuranceAmount.value = '0'
  392. } else {
  393. insuranceAmount.value = ''
  394. insuranceAmountError.value = ''
  395. }
  396. }
  397. const onSignReturnChange = (e) => {
  398. valueServices.value.isReceiptCollect = e.detail.value
  399. if (!e.detail.value) {
  400. receiptReturnTypeIndex.value = 0
  401. }
  402. }
  403. const onReceiptReturnTypeChange = (e) => {
  404. receiptReturnTypeIndex.value = e.detail.value
  405. }
  406. // 保价金额验证
  407. const validateInsuranceAmount = () => {
  408. if (!insuranceAmount.value) return true
  409. const amount = parseFloat(insuranceAmount.value)
  410. if (isNaN(amount) || amount < 0) {
  411. insuranceAmountError.value = '保价金额必须为数字且≥0'
  412. return false
  413. }
  414. if (amount > 1000000) {
  415. insuranceAmountError.value = '保价金额不能超过100万'
  416. return false
  417. }
  418. const decimal = insuranceAmount.value.toString().split('.')[1]
  419. if (decimal && decimal.length > 2) {
  420. insuranceAmountError.value = '最多两位小数'
  421. return false
  422. }
  423. insuranceAmountError.value = ''
  424. return true
  425. }
  426. // 时间选择
  427. const handleTimeClick = () => {
  428. showTimePicker.value = true
  429. }
  430. const handleTimeConfirm = (time) => {
  431. selectedTimeData.value = time
  432. showTimePicker.value = false
  433. }
  434. // 地址簿相关
  435. const openAddressBook = (type) => {
  436. uni.navigateTo({
  437. url: `/pages/address/address_list?addType=${type}&from=order`
  438. })
  439. }
  440. const handleAddAddress = (type) => {
  441. uni.navigateTo({
  442. url: `/pages/address/edit?addType=${type}&from=order`
  443. })
  444. }
  445. // 协议切换
  446. const toggleAgreement = () => {
  447. agreed.value = !agreed.value
  448. }
  449. // ==================== 表单验证(增强) ====================
  450. const validateForm = () => {
  451. // 地址
  452. if (!addressSend.value.addressId) {
  453. uni.showToast({
  454. title: '请选择寄件人地址',
  455. icon: 'none'
  456. });
  457. return false
  458. }
  459. if (!addressReceive.value.addressId) {
  460. uni.showToast({
  461. title: '请选择收件人地址',
  462. icon: 'none'
  463. });
  464. return false
  465. }
  466. // // 寄件人信息完整性
  467. // if (!addressSend.value.contactName || !addressSend.value.contactPhone) {
  468. // uni.showToast({
  469. // title: '寄件人信息不完整',
  470. // icon: 'none'
  471. // });
  472. // return false
  473. // }
  474. // if (!/^1[3-9]\d{9}$/.test(addressSend.value.contactPhone)) {
  475. // uni.showToast({
  476. // title: '寄件人手机号格式错误',
  477. // icon: 'none'
  478. // });
  479. // return false
  480. // }
  481. // if (addressSend.value.contactName.length < 1 || addressSend.value.contactName.length > 20) {
  482. // uni.showToast({
  483. // title: '寄件人姓名长度1-20字符',
  484. // icon: 'none'
  485. // });
  486. // return false
  487. // }
  488. // if (!addressSend.value.detailedAddress) {
  489. // uni.showToast({
  490. // title: '请填写寄件人人详细地址',
  491. // icon: 'none'
  492. // });
  493. // return false
  494. // }
  495. // if (!addressReceive.value.contactName || !addressReceive.value.contactPhone) {
  496. // uni.showToast({
  497. // title: '收件人信息不完整',
  498. // icon: 'none'
  499. // });
  500. // return false
  501. // }
  502. // if (!/^1[3-9]\d{9}$/.test(addressReceive.value.contactPhone)) {
  503. // uni.showToast({
  504. // title: '收件人手机号格式错误',
  505. // icon: 'none'
  506. // });
  507. // return false
  508. // }
  509. // if (addressReceive.value.contactName.length < 1 || addressReceive.value.contactName.length > 20) {
  510. // uni.showToast({
  511. // title: '收件人姓名长度1-20字符',
  512. // icon: 'none'
  513. // });
  514. // return false
  515. // }
  516. // if (!addressReceive.value.detailedAddress) {
  517. // uni.showToast({
  518. // title: '请填写收件人详细地址',
  519. // icon: 'none'
  520. // });
  521. // return false
  522. // }
  523. // 时间
  524. if (!selectedTimeData.value) {
  525. uni.showToast({
  526. title: '请选择期望上门时间',
  527. icon: 'none'
  528. });
  529. return false
  530. }
  531. // 快递类型
  532. if (!expressTypes.value.length) {
  533. uni.showToast({
  534. title: '快递类型字典加载中,请稍后',
  535. icon: 'none'
  536. });
  537. return false
  538. }
  539. if (!expressTypes.value[expressTypeIndex.value]) {
  540. uni.showToast({
  541. title: '请选择快递类型',
  542. icon: 'none'
  543. });
  544. return false
  545. }
  546. // 物品信息
  547. if (!goodsInfo.value.trim()) {
  548. uni.showToast({
  549. title: '请输入物品信息',
  550. icon: 'none'
  551. });
  552. return false
  553. }
  554. if (goodsInfo.value.length > 50) {
  555. uni.showToast({
  556. title: '物品信息不能超过50字',
  557. icon: 'none'
  558. });
  559. return false
  560. }
  561. // 重量/体积/件数范围
  562. if (weight.value < 0.1 || weight.value > 100) {
  563. uni.showToast({
  564. title: '重量范围0.1-100KG',
  565. icon: 'none'
  566. });
  567. return false
  568. }
  569. if (volume.value < 0.001 || volume.value > 10) {
  570. uni.showToast({
  571. title: '体积范围0.001-10m³',
  572. icon: 'none'
  573. });
  574. return false
  575. }
  576. if (quantity.value < 1 || quantity.value > 99) {
  577. uni.showToast({
  578. title: '件数范围1-99件',
  579. icon: 'none'
  580. });
  581. return false
  582. }
  583. // 保价金额验证
  584. if (insuranceAmount.value !== '' && insuranceAmount.value !== null) {
  585. if (!validateInsuranceAmount()) {
  586. uni.showToast({
  587. title: insuranceAmountError.value || '保价金额无效',
  588. icon: 'none'
  589. })
  590. return false
  591. }
  592. }
  593. // 顺丰签单返还类型
  594. if (product.value === '2' && valueServices.value.isReceiptCollect && !receiptReturnTypes.value[
  595. receiptReturnTypeIndex.value]) {
  596. uni.showToast({
  597. title: '请选择签单返还类型',
  598. icon: 'none'
  599. });
  600. return false
  601. }
  602. // 协议
  603. if (!agreed.value) {
  604. uni.showToast({
  605. title: '请同意服务协议',
  606. icon: 'none'
  607. });
  608. return false
  609. }
  610. return true
  611. }
  612. // ==================== 提交订单 ====================
  613. const submitOrder = async () => {
  614. if (!validateForm()) return
  615. // 构建增值服务JSON
  616. const addedService = {
  617. isPack: valueServices.value.isPack || null,
  618. guaranteeMoney: insuranceAmount.value && insuranceAmount.value !== '' ? parseFloat(insuranceAmount
  619. .value) : null,
  620. isReceiptCollect: product.value === '2' ?
  621. (valueServices.value.isReceiptCollect ? receiptReturnTypes.value[receiptReturnTypeIndex.value]
  622. ?.value : null) :
  623. (valueServices.value.isReceiptCollect ? true : null),
  624. isOverLongWeight: valueServices.value.isOverLongWeight || null,
  625. isWoodenCrate: valueServices.value.isWoodenCrate || null
  626. }
  627. // 移除null字段
  628. Object.keys(addedService).forEach(key => {
  629. if (addedService[key] === null || addedService[key] === undefined) {
  630. delete addedService[key]
  631. }
  632. })
  633. const orderData = {
  634. orderType: product.value,
  635. senderName: addressSend.value.contactName,
  636. senderPhone: addressSend.value.contactPhone,
  637. senderProvince: addressSend.value.provinceName,
  638. senderCity: addressSend.value.cityName,
  639. senderCounty: addressSend.value.countyName,
  640. senderAddress: addressSend.value.detailedAddress,
  641. receiverName: addressReceive.value.contactName,
  642. receiverPhone: addressReceive.value.contactPhone,
  643. receiverProvince: addressReceive.value.provinceName,
  644. receiverCity: addressReceive.value.cityName,
  645. receiverCounty: addressReceive.value.countyName,
  646. receiverAddress: addressReceive.value.detailedAddress,
  647. goodsName: goodsInfo.value,
  648. goodsWeight: weight.value,
  649. goodsVolume: volume.value,
  650. goodsQty: quantity.value,
  651. sendStartTime: selectedTimeData.value.startTime,
  652. sendEndTime: selectedTimeData.value.endTime,
  653. productCode: expressTypes.value[expressTypeIndex.value]?.value, // 直接从动态字典取 dictValue
  654. addedService: JSON.stringify(addedService)
  655. }
  656. uni.showLoading({
  657. title: '提交中...'
  658. })
  659. try {
  660. const res = await createOrder(orderData)
  661. uni.hideLoading()
  662. if (res.code === 200) {
  663. uni.showToast({
  664. title: '下单成功',
  665. icon: 'success'
  666. })
  667. setTimeout(() => {
  668. uni.switchTab({
  669. url: '/pages/order/index'
  670. })
  671. }, 1000)
  672. } else {
  673. uni.showToast({
  674. title: res.msg || '下单失败',
  675. icon: 'none'
  676. })
  677. }
  678. } catch (error) {
  679. uni.hideLoading()
  680. uni.showToast({
  681. title: '网络错误,请重试',
  682. icon: 'none'
  683. })
  684. console.error('订单提交失败', error)
  685. }
  686. }
  687. // ==================== 获取默认寄件地址 ====================
  688. const fetchDefaultSenderAddress = async () => {
  689. try {
  690. const res = await getDefaultAddress()
  691. if (res.code === 200 && res.data) {
  692. addressSend.value = res.data
  693. addressSend.value.defaultFlag = res.data.defaultFlag === '1'
  694. saveAddressToStorage('senderAddress', res.data)
  695. }
  696. } catch (error) {
  697. console.error('获取默认地址失败', error)
  698. }
  699. }
  700. // ==================== 生命周期 ====================
  701. onLoad((option) => {
  702. if (option.product) {
  703. product.value = option.product
  704. }
  705. // 获取默认寄件地址
  706. fetchDefaultSenderAddress()
  707. // 获取快递类型字典
  708. fetchDictData()
  709. })
  710. onMounted(() => {
  711. // 监听地址选择返回
  712. uni.$on('addressSelected', (data) => {
  713. if (data.type === 'sender') {
  714. addressSend.value = data.address
  715. saveAddressToStorage('senderAddress', data.address)
  716. } else if (data.type === 'receiver') {
  717. addressReceive.value = data.address
  718. saveAddressToStorage('receiverAddress', data.address)
  719. }
  720. })
  721. })
  722. // 组件卸载时取消监听
  723. onUnmounted(() => {
  724. uni.$off('addressSelected')
  725. })
  726. </script>
  727. <style lang="scss" scoped>
  728. /* 完全保持原有样式,未做任何修改 */
  729. .order-container {
  730. min-height: 100vh;
  731. background-color: #F5F7FA;
  732. padding: 20rpx;
  733. padding-bottom: 180rpx;
  734. box-sizing: border-box;
  735. }
  736. .info-card {
  737. background: #fff;
  738. border-radius: 32rpx;
  739. padding: 20rpx;
  740. margin-bottom: 20rpx;
  741. .address-item {
  742. display: flex;
  743. align-items: center;
  744. &:first-child {
  745. padding-bottom: 20rpx;
  746. }
  747. &:last-child {
  748. padding-top: 20rpx;
  749. }
  750. }
  751. .user-info {
  752. flex: 1;
  753. margin-left: 26rpx;
  754. .line {
  755. width: 590rpx;
  756. height: 3rpx;
  757. background-color: #F1F3F8;
  758. }
  759. .create-btn {
  760. height: 88rpx;
  761. line-height: 88rpx;
  762. font-size: 28rpx;
  763. color: #1B64F0;
  764. }
  765. }
  766. .right {
  767. margin-left: 20rpx;
  768. }
  769. .img-status-text {
  770. align-self: center;
  771. text-align: center;
  772. flex-shrink: 0;
  773. .img {
  774. width: 56rpx;
  775. height: 56rpx;
  776. }
  777. .img-change {
  778. width: 40rpx;
  779. height: 40rpx;
  780. }
  781. .address-image {
  782. width: 48rpx;
  783. height: 48rpx;
  784. }
  785. .status-text {
  786. font-size: 28rpx;
  787. color: #333;
  788. font-weight: 400;
  789. }
  790. }
  791. }
  792. .pickup-title {
  793. font-size: 32rpx;
  794. font-weight: 600;
  795. color: #333;
  796. margin-bottom: 20rpx;
  797. }
  798. .goods-card {
  799. background-color: #fff;
  800. border-radius: 32rpx;
  801. padding: 0rpx 20rpx;
  802. margin-bottom: 20rpx;
  803. }
  804. .goods-item {
  805. display: flex;
  806. justify-content: space-between;
  807. align-items: center;
  808. height: 100rpx;
  809. border-bottom: 1rpx solid #F1F3F8;
  810. &:last-child {
  811. border-bottom: none;
  812. margin-bottom: 0;
  813. }
  814. .item-label {
  815. font-size: 28rpx;
  816. color: #666;
  817. font-weight: 400;
  818. &.required::before {
  819. content: '*';
  820. color: #ff4444;
  821. margin-right: 8rpx;
  822. }
  823. }
  824. .item-control {
  825. display: flex;
  826. align-items: center;
  827. .picker-text {
  828. font-size: 28rpx;
  829. color: #333;
  830. text-align: right;
  831. margin-right: 8rpx;
  832. }
  833. &.btn {
  834. background: #fff;
  835. border: 1rpx solid #dcdfe6;
  836. border-radius: 5rpx;
  837. height: 59rpx;
  838. width: 266rpx;
  839. box-sizing: border-box;
  840. }
  841. .control-btn {
  842. width: 56rpx;
  843. height: 56rpx;
  844. background: #F5F7FA;
  845. border-radius: 4rpx;
  846. display: flex;
  847. align-items: center;
  848. justify-content: center;
  849. font-size: 36rpx;
  850. color: #666;
  851. &.minus {
  852. color: #999;
  853. border-right: 1rpx solid #dcdfe6;
  854. }
  855. &.plus {
  856. color: #999;
  857. border-left: 1rpx solid #dcdfe6;
  858. }
  859. }
  860. .control-value {
  861. width: 154rpx;
  862. text-align: center;
  863. font-size: 24rpx;
  864. color: #333;
  865. .input-field {
  866. width: 100%;
  867. height: 100rpx;
  868. line-height: 100rpx;
  869. padding: 0 24rpx;
  870. font-size: 28rpx;
  871. color: #333;
  872. text-align: center;
  873. &::placeholder {
  874. color: #999;
  875. }
  876. }
  877. }
  878. }
  879. .input-field {
  880. width: 100%;
  881. height: 100rpx;
  882. line-height: 100rpx;
  883. padding: 0 24rpx;
  884. font-size: 28rpx;
  885. color: #333;
  886. text-align: right;
  887. &::placeholder {
  888. color: #999;
  889. }
  890. }
  891. .time-value {
  892. display: flex;
  893. align-items: center;
  894. font-size: 28rpx;
  895. color: #333;
  896. line-height: 88rpx;
  897. height: 88rpx;
  898. .value {
  899. margin-right: 16rpx;
  900. }
  901. .arrow {
  902. margin-left: 32rpx;
  903. color: #999;
  904. font-size: 28rpx;
  905. font-weight: bold;
  906. }
  907. }
  908. .arrow {
  909. color: #999;
  910. }
  911. }
  912. .agreement-card {
  913. background-color: #fff;
  914. border-radius: 32rpx;
  915. padding: 30rpx 20rpx;
  916. margin-bottom: 20rpx;
  917. .agreement-item {
  918. display: flex;
  919. align-items: center;
  920. margin-bottom: 20rpx;
  921. .agreement-text {
  922. font-size: 28rpx;
  923. color: #333;
  924. margin-left: 10rpx;
  925. }
  926. }
  927. .price-notice {
  928. font-size: 24rpx;
  929. color: #ff7d00;
  930. text-align: center;
  931. }
  932. }
  933. .submit-btn {
  934. position: fixed;
  935. bottom: 40rpx;
  936. left: 30rpx;
  937. right: 30rpx;
  938. width: 686rpx;
  939. height: 88rpx;
  940. background: #1B64F0;
  941. border-radius: 44rpx;
  942. display: flex;
  943. justify-content: center;
  944. align-items: center;
  945. font-size: 32rpx;
  946. color: #FFFFFF;
  947. line-height: 88rpx;
  948. text-align: center;
  949. z-index: 10;
  950. &.disabled {
  951. background: #f5f5f5;
  952. color: #999;
  953. }
  954. }
  955. .safe-area {
  956. height: 140rpx;
  957. }
  958. </style>