create_order.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  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" v-if="product === '1'">
  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="insuranceAmountChecked" @change="onInsuranceChange" />
  119. </view>
  120. </view>
  121. <view v-if="insuranceAmountChecked" 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" v-if="product === '2'">
  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" v-if="product === '1'">
  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 insuranceAmountChecked = ref(false)
  301. const insuranceAmount = ref('')
  302. const insuranceAmountError = ref('')
  303. // 签单返还类型(顺丰)
  304. const receiptReturnTypeIndex = ref(0)
  305. const receiptReturnTypes = ref([{
  306. label: '电子凭证',
  307. value: 'electronic'
  308. },
  309. {
  310. label: '纸质凭证',
  311. value: 'paper'
  312. }
  313. ])
  314. // ==================== 协议 ====================
  315. const agreed = ref(true)
  316. // ==================== 表单操作 ====================
  317. // 重量
  318. const increaseWeight = () => {
  319. weight.value = parseFloat((weight.value + 0.1).toFixed(3))
  320. if (weight.value > 100) weight.value = 100
  321. }
  322. const decreaseWeight = () => {
  323. weight.value = parseFloat((weight.value - 0.1).toFixed(3))
  324. if (weight.value < 0.1) weight.value = 0.1
  325. }
  326. const handleWeightInput = (e) => {
  327. let value = e.detail.value.replace(/[^\d.]/g, '')
  328. const parts = value.split('.')
  329. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  330. if (value.includes('.')) {
  331. const decimal = value.split('.')[1]
  332. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  333. }
  334. const num = parseFloat(value) || 0.1
  335. weight.value = Math.max(0.1, Math.min(100, num))
  336. }
  337. // 体积
  338. const increaseVolume = () => {
  339. volume.value = parseFloat((volume.value + 0.001).toFixed(3))
  340. // if (volume.value > 10) volume.value = 10
  341. }
  342. const decreaseVolume = () => {
  343. volume.value = parseFloat((volume.value - 0.001).toFixed(3))
  344. if (volume.value < 0.001) volume.value = 0.001
  345. }
  346. const handleVolumeInput = (e) => {
  347. let value = e.detail.value.replace(/[^\d.]/g, '')
  348. const parts = value.split('.')
  349. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  350. if (value.includes('.')) {
  351. const decimal = value.split('.')[1]
  352. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  353. }
  354. const num = parseFloat(value) || 0.001
  355. volume.value = Math.max(0.001, Math.min(10, num))
  356. }
  357. // 件数
  358. const increaseQuantity = () => {
  359. quantity.value += 1
  360. // if (quantity.value > 99) quantity.value = 99
  361. }
  362. const decreaseQuantity = () => {
  363. quantity.value -= 1
  364. if (quantity.value < 1) quantity.value = 1
  365. }
  366. const handleQuantityInput = (e) => {
  367. let value = e.detail.value.replace(/\D/g, '')
  368. const int = parseInt(value) || 1
  369. // quantity.value = Math.max(1, Math.min(99, int))
  370. }
  371. // 物品信息
  372. const onGoodsInfoInput = (e) => {
  373. goodsInfo.value = e.detail.value
  374. if (goodsInfo.value.length > 50) goodsInfo.value = goodsInfo.value.substring(0, 50)
  375. }
  376. // 快递类型
  377. const onExpressTypeChange = (e) => {
  378. expressTypeIndex.value = e.detail.value
  379. }
  380. // 增值服务开关
  381. const onPackagingChange = (e) => {
  382. valueServices.value.isPack = e.detail.value
  383. }
  384. const onOverweightChange = (e) => {
  385. valueServices.value.isOverLongWeight = e.detail.value
  386. }
  387. const onWoodenFrameChange = (e) => {
  388. valueServices.value.isWoodenCrate = e.detail.value
  389. }
  390. const onInsuranceChange = (e) => {
  391. insuranceAmountChecked.value = e.detail.value
  392. if (e.detail.value) {
  393. insuranceAmount.value = '0'
  394. } else {
  395. insuranceAmount.value = ''
  396. insuranceAmountError.value = ''
  397. }
  398. }
  399. const onSignReturnChange = (e) => {
  400. valueServices.value.isReceiptCollect = e.detail.value
  401. if (!e.detail.value) {
  402. receiptReturnTypeIndex.value = 0
  403. }
  404. }
  405. const onReceiptReturnTypeChange = (e) => {
  406. receiptReturnTypeIndex.value = e.detail.value
  407. }
  408. // 保价金额验证
  409. const validateInsuranceAmount = () => {
  410. if (!insuranceAmount.value) return true
  411. const amount = parseFloat(insuranceAmount.value)
  412. if (isNaN(amount) || amount < 0) {
  413. insuranceAmountError.value = '保价金额必须为数字且大于0'
  414. return false
  415. }
  416. // if (amount > 1000000) {
  417. // insuranceAmountError.value = '保价金额不能超过100万'
  418. // return false
  419. // }
  420. const decimal = insuranceAmount.value.toString().split('.')[1]
  421. if (decimal && decimal.length > 2) {
  422. insuranceAmountError.value = '最多两位小数'
  423. return false
  424. }
  425. insuranceAmountError.value = ''
  426. return true
  427. }
  428. // 时间选择
  429. const handleTimeClick = () => {
  430. showTimePicker.value = true
  431. }
  432. const handleTimeConfirm = (time) => {
  433. selectedTimeData.value = time
  434. showTimePicker.value = false
  435. }
  436. // 地址簿相关
  437. const openAddressBook = (type) => {
  438. uni.navigateTo({
  439. url: `/pages/address/address_list?addType=${type}&from=order`
  440. })
  441. }
  442. const handleAddAddress = (type) => {
  443. uni.navigateTo({
  444. url: `/pages/address/edit?addType=${type}&from=order`
  445. })
  446. }
  447. // 协议切换
  448. const toggleAgreement = () => {
  449. agreed.value = !agreed.value
  450. }
  451. // ==================== 表单验证(增强) ====================
  452. const validateForm = () => {
  453. // 地址
  454. if (!addressSend.value.addressId) {
  455. uni.showToast({
  456. title: '请选择寄件人地址',
  457. icon: 'none'
  458. });
  459. return false
  460. }
  461. if (!addressReceive.value.addressId) {
  462. uni.showToast({
  463. title: '请选择收件人地址',
  464. icon: 'none'
  465. });
  466. return false
  467. }
  468. // // 寄件人信息完整性
  469. // if (!addressSend.value.contactName || !addressSend.value.contactPhone) {
  470. // uni.showToast({
  471. // title: '寄件人信息不完整',
  472. // icon: 'none'
  473. // });
  474. // return false
  475. // }
  476. // if (!/^1[3-9]\d{9}$/.test(addressSend.value.contactPhone)) {
  477. // uni.showToast({
  478. // title: '寄件人手机号格式错误',
  479. // icon: 'none'
  480. // });
  481. // return false
  482. // }
  483. // if (addressSend.value.contactName.length < 1 || addressSend.value.contactName.length > 20) {
  484. // uni.showToast({
  485. // title: '寄件人姓名长度1-20字符',
  486. // icon: 'none'
  487. // });
  488. // return false
  489. // }
  490. // if (!addressSend.value.detailedAddress) {
  491. // uni.showToast({
  492. // title: '请填写寄件人人详细地址',
  493. // icon: 'none'
  494. // });
  495. // return false
  496. // }
  497. // if (!addressReceive.value.contactName || !addressReceive.value.contactPhone) {
  498. // uni.showToast({
  499. // title: '收件人信息不完整',
  500. // icon: 'none'
  501. // });
  502. // return false
  503. // }
  504. // if (!/^1[3-9]\d{9}$/.test(addressReceive.value.contactPhone)) {
  505. // uni.showToast({
  506. // title: '收件人手机号格式错误',
  507. // icon: 'none'
  508. // });
  509. // return false
  510. // }
  511. // if (addressReceive.value.contactName.length < 1 || addressReceive.value.contactName.length > 20) {
  512. // uni.showToast({
  513. // title: '收件人姓名长度1-20字符',
  514. // icon: 'none'
  515. // });
  516. // return false
  517. // }
  518. // if (!addressReceive.value.detailedAddress) {
  519. // uni.showToast({
  520. // title: '请填写收件人详细地址',
  521. // icon: 'none'
  522. // });
  523. // return false
  524. // }
  525. // 时间
  526. if (!selectedTimeData.value) {
  527. uni.showToast({
  528. title: '请选择期望上门时间',
  529. icon: 'none'
  530. });
  531. return false
  532. }
  533. // 快递类型
  534. if (!expressTypes.value.length) {
  535. uni.showToast({
  536. title: '快递类型字典加载中,请稍后',
  537. icon: 'none'
  538. });
  539. return false
  540. }
  541. if (!expressTypes.value[expressTypeIndex.value]) {
  542. uni.showToast({
  543. title: '请选择快递类型',
  544. icon: 'none'
  545. });
  546. return false
  547. }
  548. // 物品信息
  549. if (!goodsInfo.value.trim()) {
  550. uni.showToast({
  551. title: '请输入物品信息',
  552. icon: 'none'
  553. });
  554. return false
  555. }
  556. if (goodsInfo.value.length > 50) {
  557. uni.showToast({
  558. title: '物品信息不能超过50字',
  559. icon: 'none'
  560. });
  561. return false
  562. }
  563. // 重量/体积/件数范围
  564. if (weight.value < 0) {
  565. uni.showToast({
  566. title: '请输入正确的重量',
  567. icon: 'none'
  568. });
  569. return false
  570. }
  571. if (volume.value < 0) {
  572. uni.showToast({
  573. title: '请输入正确的体积',
  574. icon: 'none'
  575. });
  576. return false
  577. }
  578. if (quantity.value < 1) {
  579. uni.showToast({
  580. title: '请输入正确的件数',
  581. icon: 'none'
  582. });
  583. return false
  584. }
  585. // 保价金额验证
  586. if (insuranceAmount.value !== '' && insuranceAmount.value !== null) {
  587. if (!validateInsuranceAmount()) {
  588. uni.showToast({
  589. title: insuranceAmountError.value || '保价金额无效',
  590. icon: 'none'
  591. })
  592. return false
  593. }
  594. }
  595. // 顺丰签单返还类型
  596. if (product.value === '2' && valueServices.value.isReceiptCollect && !receiptReturnTypes.value[
  597. receiptReturnTypeIndex.value]) {
  598. uni.showToast({
  599. title: '请选择签单返还类型',
  600. icon: 'none'
  601. });
  602. return false
  603. }
  604. // 协议
  605. if (!agreed.value) {
  606. uni.showToast({
  607. title: '请同意服务协议',
  608. icon: 'none'
  609. });
  610. return false
  611. }
  612. return true
  613. }
  614. const isSubmit = ref(false)
  615. // ==================== 提交订单 ====================
  616. const submitOrder = async () => {
  617. if (!validateForm()) return
  618. if (isSubmit.value) return
  619. isSubmit.value = true
  620. // 构建增值服务JSON
  621. const addedService = {
  622. isPack: valueServices.value.isPack || null,
  623. guaranteeMoney: insuranceAmount.value && insuranceAmount.value !== '' ? parseFloat(insuranceAmount
  624. .value) : null,
  625. isReceiptCollect: product.value === '2' ?
  626. (valueServices.value.isReceiptCollect ? receiptReturnTypes.value[receiptReturnTypeIndex.value]
  627. ?.value : null) :
  628. (valueServices.value.isReceiptCollect ? true : null),
  629. isOverLongWeight: valueServices.value.isOverLongWeight || null,
  630. isWoodenCrate: valueServices.value.isWoodenCrate || null
  631. }
  632. // 移除null字段
  633. Object.keys(addedService).forEach(key => {
  634. if (addedService[key] === null || addedService[key] === undefined) {
  635. delete addedService[key]
  636. }
  637. })
  638. const orderData = {
  639. orderType: product.value,
  640. senderName: addressSend.value.contactName,
  641. senderPhone: addressSend.value.contactPhone,
  642. senderProvince: addressSend.value.provinceName,
  643. senderCity: addressSend.value.cityName,
  644. senderCounty: addressSend.value.countyName,
  645. senderAddress: addressSend.value.detailedAddress,
  646. receiverName: addressReceive.value.contactName,
  647. receiverPhone: addressReceive.value.contactPhone,
  648. receiverProvince: addressReceive.value.provinceName,
  649. receiverCity: addressReceive.value.cityName,
  650. receiverCounty: addressReceive.value.countyName,
  651. receiverAddress: addressReceive.value.detailedAddress,
  652. goodsName: goodsInfo.value,
  653. goodsWeight: weight.value,
  654. goodsVolume: volume.value,
  655. goodsQty: quantity.value,
  656. sendStartTime: selectedTimeData.value.startTime,
  657. sendEndTime: selectedTimeData.value.endTime,
  658. productCode: expressTypes.value[expressTypeIndex.value]?.value, // 直接从动态字典取 dictValue
  659. addedService: JSON.stringify(addedService)
  660. }
  661. uni.showLoading({
  662. title: '提交中...',
  663. mask:true
  664. })
  665. try {
  666. const res = await createOrder(orderData)
  667. uni.hideLoading()
  668. if (res.code === 200) {
  669. uni.showToast({
  670. title: '下单成功',
  671. icon: 'success',
  672. mask: true
  673. })
  674. uni.redirectTo({
  675. url: '/pages/order/index'
  676. })
  677. } else {
  678. isSubmit.value = false
  679. uni.showToast({
  680. title: res.msg || '下单失败',
  681. icon: 'none'
  682. })
  683. }
  684. } catch (error) {
  685. isSubmit.value = false
  686. uni.showToast({
  687. title: error,
  688. icon: 'none'
  689. })
  690. console.error('订单提交失败', error)
  691. } finally{
  692. // uni.hideLoading()
  693. }
  694. }
  695. // ==================== 获取默认寄件地址 ====================
  696. const fetchDefaultSenderAddress = async () => {
  697. try {
  698. const res = await getDefaultAddress()
  699. if (res.code === 200 && res.data) {
  700. addressSend.value = res.data
  701. addressSend.value.defaultFlag = res.data.defaultFlag === '1'
  702. saveAddressToStorage('senderAddress', res.data)
  703. }
  704. } catch (error) {
  705. console.error('获取默认地址失败', error)
  706. }
  707. }
  708. // ==================== 生命周期 ====================
  709. onLoad((option) => {
  710. if (option.product) {
  711. product.value = option.product
  712. }
  713. // 获取快递类型字典
  714. fetchDictData()
  715. })
  716. onMounted(() => {
  717. // 监听地址选择返回
  718. uni.$on('addressSelected', (data) => {
  719. if (data.type === 'sender') {
  720. addressSend.value = data.address
  721. saveAddressToStorage('senderAddress', data.address)
  722. } else if (data.type === 'receiver') {
  723. addressReceive.value = data.address
  724. saveAddressToStorage('receiverAddress', data.address)
  725. }
  726. })
  727. if(!addressSend.addressId){
  728. fetchDefaultSenderAddress()
  729. }
  730. })
  731. // 组件卸载时取消监听
  732. onUnmounted(() => {
  733. uni.$off('addressSelected')
  734. })
  735. </script>
  736. <style lang="scss" scoped>
  737. /* 完全保持原有样式,未做任何修改 */
  738. .order-container {
  739. min-height: 100vh;
  740. background-color: #F5F7FA;
  741. padding: 20rpx;
  742. padding-bottom: 180rpx;
  743. box-sizing: border-box;
  744. }
  745. .info-card {
  746. background: #fff;
  747. border-radius: 32rpx;
  748. padding: 20rpx;
  749. margin-bottom: 20rpx;
  750. .address-item {
  751. display: flex;
  752. align-items: center;
  753. &:first-child {
  754. padding-bottom: 20rpx;
  755. }
  756. &:last-child {
  757. padding-top: 20rpx;
  758. }
  759. }
  760. .user-info {
  761. flex: 1;
  762. margin-left: 26rpx;
  763. .line {
  764. width: 590rpx;
  765. height: 3rpx;
  766. background-color: #F1F3F8;
  767. }
  768. .create-btn {
  769. height: 88rpx;
  770. font-weight: 400;
  771. font-size: 40rpx;
  772. color: #999999;
  773. line-height: 88rpx;
  774. }
  775. }
  776. .right {
  777. margin-left: 20rpx;
  778. }
  779. .img-status-text {
  780. align-self: center;
  781. text-align: center;
  782. flex-shrink: 0;
  783. .img {
  784. width: 56rpx;
  785. height: 56rpx;
  786. }
  787. .img-change {
  788. width: 40rpx;
  789. height: 40rpx;
  790. }
  791. .address-image {
  792. width: 48rpx;
  793. height: 48rpx;
  794. }
  795. .status-text {
  796. font-size: 28rpx;
  797. color: #333;
  798. font-weight: 400;
  799. }
  800. }
  801. }
  802. .pickup-title {
  803. font-size: 32rpx;
  804. font-weight: 600;
  805. color: #333;
  806. margin-bottom: 20rpx;
  807. }
  808. .goods-card {
  809. background-color: #fff;
  810. border-radius: 32rpx;
  811. padding: 0rpx 20rpx;
  812. margin-bottom: 20rpx;
  813. }
  814. .goods-item {
  815. display: flex;
  816. justify-content: space-between;
  817. align-items: center;
  818. height: 100rpx;
  819. border-bottom: 1rpx solid #F1F3F8;
  820. &:last-child {
  821. border-bottom: none;
  822. margin-bottom: 0;
  823. }
  824. .item-label {
  825. font-size: 28rpx;
  826. color: #666;
  827. font-weight: 400;
  828. &.required::before {
  829. content: '*';
  830. color: #ff4444;
  831. margin-right: 8rpx;
  832. }
  833. }
  834. .item-control {
  835. display: flex;
  836. align-items: center;
  837. .picker-text {
  838. font-size: 28rpx;
  839. color: #333;
  840. text-align: right;
  841. margin-right: 8rpx;
  842. }
  843. &.btn {
  844. background: #fff;
  845. border: 1rpx solid #dcdfe6;
  846. border-radius: 5rpx;
  847. height: 59rpx;
  848. width: 266rpx;
  849. box-sizing: border-box;
  850. }
  851. .control-btn {
  852. width: 56rpx;
  853. height: 56rpx;
  854. background: #F5F7FA;
  855. border-radius: 4rpx;
  856. display: flex;
  857. align-items: center;
  858. justify-content: center;
  859. font-size: 36rpx;
  860. color: #666;
  861. &.minus {
  862. color: #999;
  863. border-right: 1rpx solid #dcdfe6;
  864. }
  865. &.plus {
  866. color: #999;
  867. border-left: 1rpx solid #dcdfe6;
  868. }
  869. }
  870. .control-value {
  871. width: 154rpx;
  872. text-align: center;
  873. font-size: 24rpx;
  874. color: #333;
  875. .input-field {
  876. width: 100%;
  877. height: 100rpx;
  878. line-height: 100rpx;
  879. padding: 0 24rpx;
  880. font-size: 28rpx;
  881. color: #333;
  882. text-align: center;
  883. &::placeholder {
  884. color: #999;
  885. }
  886. }
  887. }
  888. }
  889. .input-field {
  890. width: 100%;
  891. height: 100rpx;
  892. line-height: 100rpx;
  893. padding: 0 24rpx;
  894. font-size: 28rpx;
  895. color: #333;
  896. text-align: right;
  897. &::placeholder {
  898. color: #999;
  899. }
  900. }
  901. .time-value {
  902. display: flex;
  903. align-items: center;
  904. font-size: 28rpx;
  905. color: #333;
  906. line-height: 88rpx;
  907. height: 88rpx;
  908. .value {
  909. margin-right: 16rpx;
  910. }
  911. .arrow {
  912. margin-left: 32rpx;
  913. color: #999;
  914. font-size: 28rpx;
  915. font-weight: bold;
  916. }
  917. }
  918. .arrow {
  919. color: #999;
  920. }
  921. }
  922. .agreement-card {
  923. background-color: #fff;
  924. border-radius: 32rpx;
  925. padding: 30rpx 20rpx;
  926. margin-bottom: 20rpx;
  927. .agreement-item {
  928. display: flex;
  929. align-items: center;
  930. margin-bottom: 20rpx;
  931. .agreement-text {
  932. font-size: 28rpx;
  933. color: #333;
  934. margin-left: 10rpx;
  935. }
  936. }
  937. .price-notice {
  938. font-size: 24rpx;
  939. color: #ff7d00;
  940. text-align: center;
  941. }
  942. }
  943. .submit-btn {
  944. position: fixed;
  945. bottom: 40rpx;
  946. left: 30rpx;
  947. right: 30rpx;
  948. width: 686rpx;
  949. height: 88rpx;
  950. background: #1B64F0;
  951. border-radius: 44rpx;
  952. display: flex;
  953. justify-content: center;
  954. align-items: center;
  955. font-size: 32rpx;
  956. color: #FFFFFF;
  957. line-height: 88rpx;
  958. text-align: center;
  959. z-index: 10;
  960. &.disabled {
  961. background: #f5f5f5;
  962. color: #999;
  963. }
  964. }
  965. .safe-area {
  966. height: 140rpx;
  967. }
  968. </style>