create_order.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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(0.5)
  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(true)
  315. // ==================== 表单操作 ====================
  316. // 重量
  317. const increaseWeight = () => {
  318. weight.value = parseFloat((weight.value + 0.1).toFixed(3))
  319. if (weight.value > 100) weight.value = 100
  320. }
  321. const decreaseWeight = () => {
  322. weight.value = parseFloat((weight.value - 0.1).toFixed(3))
  323. if (weight.value < 0.1) weight.value = 0.1
  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.1
  334. weight.value = Math.max(0.1, 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. const isSubmit = ref(false)
  613. // ==================== 提交订单 ====================
  614. const submitOrder = async () => {
  615. if (!validateForm()) return
  616. if (isSubmit.value) return
  617. isSubmit.value = true
  618. // 构建增值服务JSON
  619. const addedService = {
  620. isPack: valueServices.value.isPack || null,
  621. guaranteeMoney: insuranceAmount.value && insuranceAmount.value !== '' ? parseFloat(insuranceAmount
  622. .value) : null,
  623. isReceiptCollect: product.value === '2' ?
  624. (valueServices.value.isReceiptCollect ? receiptReturnTypes.value[receiptReturnTypeIndex.value]
  625. ?.value : null) :
  626. (valueServices.value.isReceiptCollect ? true : null),
  627. isOverLongWeight: valueServices.value.isOverLongWeight || null,
  628. isWoodenCrate: valueServices.value.isWoodenCrate || null
  629. }
  630. // 移除null字段
  631. Object.keys(addedService).forEach(key => {
  632. if (addedService[key] === null || addedService[key] === undefined) {
  633. delete addedService[key]
  634. }
  635. })
  636. const orderData = {
  637. orderType: product.value,
  638. senderName: addressSend.value.contactName,
  639. senderPhone: addressSend.value.contactPhone,
  640. senderProvince: addressSend.value.provinceName,
  641. senderCity: addressSend.value.cityName,
  642. senderCounty: addressSend.value.countyName,
  643. senderAddress: addressSend.value.detailedAddress,
  644. receiverName: addressReceive.value.contactName,
  645. receiverPhone: addressReceive.value.contactPhone,
  646. receiverProvince: addressReceive.value.provinceName,
  647. receiverCity: addressReceive.value.cityName,
  648. receiverCounty: addressReceive.value.countyName,
  649. receiverAddress: addressReceive.value.detailedAddress,
  650. goodsName: goodsInfo.value,
  651. goodsWeight: weight.value,
  652. goodsVolume: volume.value,
  653. goodsQty: quantity.value,
  654. sendStartTime: selectedTimeData.value.startTime,
  655. sendEndTime: selectedTimeData.value.endTime,
  656. productCode: expressTypes.value[expressTypeIndex.value]?.value, // 直接从动态字典取 dictValue
  657. addedService: JSON.stringify(addedService)
  658. }
  659. uni.showLoading({
  660. title: '提交中...',
  661. mask:true
  662. })
  663. try {
  664. const res = await createOrder(orderData)
  665. // uni.hideLoading()
  666. if (res.code === 200) {
  667. uni.showToast({
  668. title: '下单成功',
  669. icon: 'success',
  670. mask: true
  671. })
  672. uni.redirectTo({
  673. url: '/pages/order/index'
  674. })
  675. } else {
  676. isSubmit.value = false
  677. uni.showToast({
  678. title: res.msg || '下单失败',
  679. icon: 'none'
  680. })
  681. }
  682. } catch (error) {
  683. isSubmit.value = false
  684. uni.hideLoading()
  685. uni.showToast({
  686. title: '网络错误,请重试',
  687. icon: 'none'
  688. })
  689. console.error('订单提交失败', error)
  690. } finally{
  691. uni.hideLoading()
  692. }
  693. }
  694. // ==================== 获取默认寄件地址 ====================
  695. const fetchDefaultSenderAddress = async () => {
  696. try {
  697. const res = await getDefaultAddress()
  698. if (res.code === 200 && res.data) {
  699. addressSend.value = res.data
  700. addressSend.value.defaultFlag = res.data.defaultFlag === '1'
  701. saveAddressToStorage('senderAddress', res.data)
  702. }
  703. } catch (error) {
  704. console.error('获取默认地址失败', error)
  705. }
  706. }
  707. // ==================== 生命周期 ====================
  708. onLoad((option) => {
  709. if (option.product) {
  710. product.value = option.product
  711. }
  712. // 获取快递类型字典
  713. fetchDictData()
  714. })
  715. onMounted(() => {
  716. // 监听地址选择返回
  717. uni.$on('addressSelected', (data) => {
  718. if (data.type === 'sender') {
  719. addressSend.value = data.address
  720. saveAddressToStorage('senderAddress', data.address)
  721. } else if (data.type === 'receiver') {
  722. addressReceive.value = data.address
  723. saveAddressToStorage('receiverAddress', data.address)
  724. }
  725. })
  726. if(!addressSend.addressId){
  727. fetchDefaultSenderAddress()
  728. }
  729. })
  730. // 组件卸载时取消监听
  731. onUnmounted(() => {
  732. uni.$off('addressSelected')
  733. })
  734. </script>
  735. <style lang="scss" scoped>
  736. /* 完全保持原有样式,未做任何修改 */
  737. .order-container {
  738. min-height: 100vh;
  739. background-color: #F5F7FA;
  740. padding: 20rpx;
  741. padding-bottom: 180rpx;
  742. box-sizing: border-box;
  743. }
  744. .info-card {
  745. background: #fff;
  746. border-radius: 32rpx;
  747. padding: 20rpx;
  748. margin-bottom: 20rpx;
  749. .address-item {
  750. display: flex;
  751. align-items: center;
  752. &:first-child {
  753. padding-bottom: 20rpx;
  754. }
  755. &:last-child {
  756. padding-top: 20rpx;
  757. }
  758. }
  759. .user-info {
  760. flex: 1;
  761. margin-left: 26rpx;
  762. .line {
  763. width: 590rpx;
  764. height: 3rpx;
  765. background-color: #F1F3F8;
  766. }
  767. .create-btn {
  768. height: 88rpx;
  769. line-height: 88rpx;
  770. font-size: 28rpx;
  771. color: #1B64F0;
  772. }
  773. }
  774. .right {
  775. margin-left: 20rpx;
  776. }
  777. .img-status-text {
  778. align-self: center;
  779. text-align: center;
  780. flex-shrink: 0;
  781. .img {
  782. width: 56rpx;
  783. height: 56rpx;
  784. }
  785. .img-change {
  786. width: 40rpx;
  787. height: 40rpx;
  788. }
  789. .address-image {
  790. width: 48rpx;
  791. height: 48rpx;
  792. }
  793. .status-text {
  794. font-size: 28rpx;
  795. color: #333;
  796. font-weight: 400;
  797. }
  798. }
  799. }
  800. .pickup-title {
  801. font-size: 32rpx;
  802. font-weight: 600;
  803. color: #333;
  804. margin-bottom: 20rpx;
  805. }
  806. .goods-card {
  807. background-color: #fff;
  808. border-radius: 32rpx;
  809. padding: 0rpx 20rpx;
  810. margin-bottom: 20rpx;
  811. }
  812. .goods-item {
  813. display: flex;
  814. justify-content: space-between;
  815. align-items: center;
  816. height: 100rpx;
  817. border-bottom: 1rpx solid #F1F3F8;
  818. &:last-child {
  819. border-bottom: none;
  820. margin-bottom: 0;
  821. }
  822. .item-label {
  823. font-size: 28rpx;
  824. color: #666;
  825. font-weight: 400;
  826. &.required::before {
  827. content: '*';
  828. color: #ff4444;
  829. margin-right: 8rpx;
  830. }
  831. }
  832. .item-control {
  833. display: flex;
  834. align-items: center;
  835. .picker-text {
  836. font-size: 28rpx;
  837. color: #333;
  838. text-align: right;
  839. margin-right: 8rpx;
  840. }
  841. &.btn {
  842. background: #fff;
  843. border: 1rpx solid #dcdfe6;
  844. border-radius: 5rpx;
  845. height: 59rpx;
  846. width: 266rpx;
  847. box-sizing: border-box;
  848. }
  849. .control-btn {
  850. width: 56rpx;
  851. height: 56rpx;
  852. background: #F5F7FA;
  853. border-radius: 4rpx;
  854. display: flex;
  855. align-items: center;
  856. justify-content: center;
  857. font-size: 36rpx;
  858. color: #666;
  859. &.minus {
  860. color: #999;
  861. border-right: 1rpx solid #dcdfe6;
  862. }
  863. &.plus {
  864. color: #999;
  865. border-left: 1rpx solid #dcdfe6;
  866. }
  867. }
  868. .control-value {
  869. width: 154rpx;
  870. text-align: center;
  871. font-size: 24rpx;
  872. color: #333;
  873. .input-field {
  874. width: 100%;
  875. height: 100rpx;
  876. line-height: 100rpx;
  877. padding: 0 24rpx;
  878. font-size: 28rpx;
  879. color: #333;
  880. text-align: center;
  881. &::placeholder {
  882. color: #999;
  883. }
  884. }
  885. }
  886. }
  887. .input-field {
  888. width: 100%;
  889. height: 100rpx;
  890. line-height: 100rpx;
  891. padding: 0 24rpx;
  892. font-size: 28rpx;
  893. color: #333;
  894. text-align: right;
  895. &::placeholder {
  896. color: #999;
  897. }
  898. }
  899. .time-value {
  900. display: flex;
  901. align-items: center;
  902. font-size: 28rpx;
  903. color: #333;
  904. line-height: 88rpx;
  905. height: 88rpx;
  906. .value {
  907. margin-right: 16rpx;
  908. }
  909. .arrow {
  910. margin-left: 32rpx;
  911. color: #999;
  912. font-size: 28rpx;
  913. font-weight: bold;
  914. }
  915. }
  916. .arrow {
  917. color: #999;
  918. }
  919. }
  920. .agreement-card {
  921. background-color: #fff;
  922. border-radius: 32rpx;
  923. padding: 30rpx 20rpx;
  924. margin-bottom: 20rpx;
  925. .agreement-item {
  926. display: flex;
  927. align-items: center;
  928. margin-bottom: 20rpx;
  929. .agreement-text {
  930. font-size: 28rpx;
  931. color: #333;
  932. margin-left: 10rpx;
  933. }
  934. }
  935. .price-notice {
  936. font-size: 24rpx;
  937. color: #ff7d00;
  938. text-align: center;
  939. }
  940. }
  941. .submit-btn {
  942. position: fixed;
  943. bottom: 40rpx;
  944. left: 30rpx;
  945. right: 30rpx;
  946. width: 686rpx;
  947. height: 88rpx;
  948. background: #1B64F0;
  949. border-radius: 44rpx;
  950. display: flex;
  951. justify-content: center;
  952. align-items: center;
  953. font-size: 32rpx;
  954. color: #FFFFFF;
  955. line-height: 88rpx;
  956. text-align: center;
  957. z-index: 10;
  958. &.disabled {
  959. background: #f5f5f5;
  960. color: #999;
  961. }
  962. }
  963. .safe-area {
  964. height: 140rpx;
  965. }
  966. </style>