create_order.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  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. mask: true
  667. })
  668. setTimeout(() => {
  669. uni.navigateTo({
  670. url: '/pages/order/index'
  671. })
  672. }, 1000)
  673. } else {
  674. uni.showToast({
  675. title: res.msg || '下单失败',
  676. icon: 'none'
  677. })
  678. }
  679. } catch (error) {
  680. uni.hideLoading()
  681. uni.showToast({
  682. title: '网络错误,请重试',
  683. icon: 'none'
  684. })
  685. console.error('订单提交失败', error)
  686. }
  687. }
  688. // ==================== 获取默认寄件地址 ====================
  689. const fetchDefaultSenderAddress = async () => {
  690. try {
  691. const res = await getDefaultAddress()
  692. if (res.code === 200 && res.data) {
  693. addressSend.value = res.data
  694. addressSend.value.defaultFlag = res.data.defaultFlag === '1'
  695. saveAddressToStorage('senderAddress', res.data)
  696. }
  697. } catch (error) {
  698. console.error('获取默认地址失败', error)
  699. }
  700. }
  701. // ==================== 生命周期 ====================
  702. onLoad((option) => {
  703. if (option.product) {
  704. product.value = option.product
  705. }
  706. // 获取快递类型字典
  707. fetchDictData()
  708. })
  709. onMounted(() => {
  710. // 监听地址选择返回
  711. uni.$on('addressSelected', (data) => {
  712. if (data.type === 'sender') {
  713. addressSend.value = data.address
  714. saveAddressToStorage('senderAddress', data.address)
  715. } else if (data.type === 'receiver') {
  716. addressReceive.value = data.address
  717. saveAddressToStorage('receiverAddress', data.address)
  718. }
  719. })
  720. })
  721. // 组件卸载时取消监听
  722. onUnmounted(() => {
  723. uni.$off('addressSelected')
  724. })
  725. </script>
  726. <style lang="scss" scoped>
  727. /* 完全保持原有样式,未做任何修改 */
  728. .order-container {
  729. min-height: 100vh;
  730. background-color: #F5F7FA;
  731. padding: 20rpx;
  732. padding-bottom: 180rpx;
  733. box-sizing: border-box;
  734. }
  735. .info-card {
  736. background: #fff;
  737. border-radius: 32rpx;
  738. padding: 20rpx;
  739. margin-bottom: 20rpx;
  740. .address-item {
  741. display: flex;
  742. align-items: center;
  743. &:first-child {
  744. padding-bottom: 20rpx;
  745. }
  746. &:last-child {
  747. padding-top: 20rpx;
  748. }
  749. }
  750. .user-info {
  751. flex: 1;
  752. margin-left: 26rpx;
  753. .line {
  754. width: 590rpx;
  755. height: 3rpx;
  756. background-color: #F1F3F8;
  757. }
  758. .create-btn {
  759. height: 88rpx;
  760. line-height: 88rpx;
  761. font-size: 28rpx;
  762. color: #1B64F0;
  763. }
  764. }
  765. .right {
  766. margin-left: 20rpx;
  767. }
  768. .img-status-text {
  769. align-self: center;
  770. text-align: center;
  771. flex-shrink: 0;
  772. .img {
  773. width: 56rpx;
  774. height: 56rpx;
  775. }
  776. .img-change {
  777. width: 40rpx;
  778. height: 40rpx;
  779. }
  780. .address-image {
  781. width: 48rpx;
  782. height: 48rpx;
  783. }
  784. .status-text {
  785. font-size: 28rpx;
  786. color: #333;
  787. font-weight: 400;
  788. }
  789. }
  790. }
  791. .pickup-title {
  792. font-size: 32rpx;
  793. font-weight: 600;
  794. color: #333;
  795. margin-bottom: 20rpx;
  796. }
  797. .goods-card {
  798. background-color: #fff;
  799. border-radius: 32rpx;
  800. padding: 0rpx 20rpx;
  801. margin-bottom: 20rpx;
  802. }
  803. .goods-item {
  804. display: flex;
  805. justify-content: space-between;
  806. align-items: center;
  807. height: 100rpx;
  808. border-bottom: 1rpx solid #F1F3F8;
  809. &:last-child {
  810. border-bottom: none;
  811. margin-bottom: 0;
  812. }
  813. .item-label {
  814. font-size: 28rpx;
  815. color: #666;
  816. font-weight: 400;
  817. &.required::before {
  818. content: '*';
  819. color: #ff4444;
  820. margin-right: 8rpx;
  821. }
  822. }
  823. .item-control {
  824. display: flex;
  825. align-items: center;
  826. .picker-text {
  827. font-size: 28rpx;
  828. color: #333;
  829. text-align: right;
  830. margin-right: 8rpx;
  831. }
  832. &.btn {
  833. background: #fff;
  834. border: 1rpx solid #dcdfe6;
  835. border-radius: 5rpx;
  836. height: 59rpx;
  837. width: 266rpx;
  838. box-sizing: border-box;
  839. }
  840. .control-btn {
  841. width: 56rpx;
  842. height: 56rpx;
  843. background: #F5F7FA;
  844. border-radius: 4rpx;
  845. display: flex;
  846. align-items: center;
  847. justify-content: center;
  848. font-size: 36rpx;
  849. color: #666;
  850. &.minus {
  851. color: #999;
  852. border-right: 1rpx solid #dcdfe6;
  853. }
  854. &.plus {
  855. color: #999;
  856. border-left: 1rpx solid #dcdfe6;
  857. }
  858. }
  859. .control-value {
  860. width: 154rpx;
  861. text-align: center;
  862. font-size: 24rpx;
  863. color: #333;
  864. .input-field {
  865. width: 100%;
  866. height: 100rpx;
  867. line-height: 100rpx;
  868. padding: 0 24rpx;
  869. font-size: 28rpx;
  870. color: #333;
  871. text-align: center;
  872. &::placeholder {
  873. color: #999;
  874. }
  875. }
  876. }
  877. }
  878. .input-field {
  879. width: 100%;
  880. height: 100rpx;
  881. line-height: 100rpx;
  882. padding: 0 24rpx;
  883. font-size: 28rpx;
  884. color: #333;
  885. text-align: right;
  886. &::placeholder {
  887. color: #999;
  888. }
  889. }
  890. .time-value {
  891. display: flex;
  892. align-items: center;
  893. font-size: 28rpx;
  894. color: #333;
  895. line-height: 88rpx;
  896. height: 88rpx;
  897. .value {
  898. margin-right: 16rpx;
  899. }
  900. .arrow {
  901. margin-left: 32rpx;
  902. color: #999;
  903. font-size: 28rpx;
  904. font-weight: bold;
  905. }
  906. }
  907. .arrow {
  908. color: #999;
  909. }
  910. }
  911. .agreement-card {
  912. background-color: #fff;
  913. border-radius: 32rpx;
  914. padding: 30rpx 20rpx;
  915. margin-bottom: 20rpx;
  916. .agreement-item {
  917. display: flex;
  918. align-items: center;
  919. margin-bottom: 20rpx;
  920. .agreement-text {
  921. font-size: 28rpx;
  922. color: #333;
  923. margin-left: 10rpx;
  924. }
  925. }
  926. .price-notice {
  927. font-size: 24rpx;
  928. color: #ff7d00;
  929. text-align: center;
  930. }
  931. }
  932. .submit-btn {
  933. position: fixed;
  934. bottom: 40rpx;
  935. left: 30rpx;
  936. right: 30rpx;
  937. width: 686rpx;
  938. height: 88rpx;
  939. background: #1B64F0;
  940. border-radius: 44rpx;
  941. display: flex;
  942. justify-content: center;
  943. align-items: center;
  944. font-size: 32rpx;
  945. color: #FFFFFF;
  946. line-height: 88rpx;
  947. text-align: center;
  948. z-index: 10;
  949. &.disabled {
  950. background: #f5f5f5;
  951. color: #999;
  952. }
  953. }
  954. .safe-area {
  955. height: 140rpx;
  956. }
  957. </style>