create_order.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  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. <picker :range="expressTypes" :range-key="'label'" @change="onExpressTypeChange"
  53. :value="expressTypeIndex" :disabled="!expressTypes.length">
  54. <view class="item-control">
  55. <view class="picker-text">
  56. {{ expressTypes[expressTypeIndex]?.label || (expressTypes.length ? '请选择快递类型' : '加载中...') }}
  57. </view>
  58. <u-icon class="arrow" name='arrow-right' size="18"></u-icon>
  59. </view>
  60. </picker>
  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. <!-- 增值服务标题(带折叠箭头) -->
  107. <view class="pickup-title" style="display: flex; justify-content: space-between; align-items: center;">
  108. <text>增值服务</text>
  109. <u-icon :name="showValueAdded ? 'arrow-up' : 'arrow-down'" size="18" color="#999" @click="showValueAdded = !showValueAdded"></u-icon>
  110. </view>
  111. <!-- 增值服务卡片(折叠内容) -->
  112. <view class="goods-card">
  113. <block v-if="showValueAdded">
  114. <!-- 展开状态:显示所有增值服务项 -->
  115. <view class="goods-item" v-if="product === '1'">
  116. <view class="item-label">包装服务</view>
  117. <view class="item-control">
  118. <switch color="#007AFF" :checked="valueServices.isPack" @change="onPackagingChange" />
  119. </view>
  120. </view>
  121. <view class="goods-item">
  122. <view class="item-label">保价</view>
  123. <view class="item-control">
  124. <switch color="#007AFF" :checked="insuranceAmountChecked" @change="onInsuranceChange" />
  125. </view>
  126. </view>
  127. <view v-if="insuranceAmountChecked" class="goods-item">
  128. <view class="item-label">保价金额(元)</view>
  129. <view class="item-control">
  130. <input class="input-field" placeholder="请输入保价金额" placeholder-class="placeholder"
  131. v-model="insuranceAmount" type="digit" maxlength="10" @input="validateInsuranceAmount" />
  132. </view>
  133. </view>
  134. <!-- 超长超重(顺丰/京东) -->
  135. <!-- <view class="goods-item" v-if="product === '2'">
  136. <view class="item-label">超长超重</view>
  137. <view class="item-control">
  138. <switch color="#007AFF" :checked="valueServices.isOverLongWeight" @change="onOverweightChange" />
  139. </view>
  140. </view> -->
  141. <!-- 签单返还 -->
  142. <view class="goods-item" v-if="product === '1'">
  143. <view class="item-label">签单返还</view>
  144. <view class="item-control">
  145. <switch color="#007AFF" :checked="valueServices.isReceiptCollect" @change="onSignReturnChange" />
  146. </view>
  147. </view>
  148. <!-- 顺丰:签单返还类型选择 -->
  149. <view v-if="product === '2' && valueServices.isReceiptCollect" class="goods-item">
  150. <view class="item-label">返还类型</view>
  151. <view class="item-control">
  152. <picker :range="receiptReturnTypes" :range-key="'label'" @change="onReceiptReturnTypeChange"
  153. :value="receiptReturnTypeIndex">
  154. <view class="picker-text">{{ receiptReturnTypes[receiptReturnTypeIndex]?.label || '请选择返还类型' }}
  155. </view>
  156. </picker>
  157. <u-icon class="arrow" name='arrow-right' size="18"></u-icon>
  158. </view>
  159. </view>
  160. <!-- 打木架(仅顺丰) -->
  161. <!-- <view v-if="product === '2'" class="goods-item">
  162. <view class="item-label">打木架</view>
  163. <view class="item-control">
  164. <switch color="#007AFF" :checked="valueServices.isWoodenCrate" @change="onWoodenFrameChange" />
  165. </view>
  166. </view> -->
  167. </block>
  168. <block v-else>
  169. <!-- 折叠状态:显示一个可点击的展开提示行 -->
  170. <view class="goods-item" @click="showValueAdded = true">
  171. <view class="item-label">增值服务</view>
  172. <view class="item-control">
  173. <text class="picker-text">点击展开</text>
  174. <u-icon name="arrow-down" size="18" color="#999"></u-icon>
  175. </view>
  176. </view>
  177. </block>
  178. </view>
  179. <!-- 协议同意 -->
  180. <view class="agreement-card">
  181. <!-- <view class="agreement-item">
  182. <checkbox color="#1B64F0" :checked="agreed" @click="toggleAgreement" style="transform:scale(0.8)" />
  183. <text class="agreement-text">我已理解并同意遵守《快件服务协议》</text>
  184. </view> -->
  185. <view class="price-notice">
  186. 实际费用以快递员核实为准
  187. </view>
  188. </view>
  189. <view class="add-btn-container">
  190. <!-- 下单按钮 -->
  191. <view class="submit-btn" :class="{ disabled: !agreed }" :disabled="!agreed" @click="submitOrder">
  192. 提交订单
  193. </view>
  194. </view>
  195. <!-- 安全区域占位 -->
  196. <view class="safe-area"></view>
  197. <!-- 时间选择弹窗 -->
  198. <TimePopup :visible="showTimePicker" @close="showTimePicker = false" @confirm="handleTimeConfirm" />
  199. </view>
  200. </template>
  201. <script setup>
  202. import {
  203. ref,
  204. computed,
  205. onMounted,
  206. onUnmounted,
  207. } from 'vue'
  208. import {
  209. onLoad
  210. } from '@dcloudio/uni-app'
  211. import TimePopup from './components/TimePopup.vue'
  212. import AddressInfo from '@/components/AddressInfo.vue'
  213. import {
  214. createOrder,
  215. dictList
  216. } from '../../api/order'
  217. import {
  218. getDefaultAddress
  219. } from '../../api/address'
  220. // ==================== 工具函数 ====================
  221. const pad = (n) => (n < 10 ? '0' + n : n)
  222. const formatDateTime = (date) => {
  223. const y = date.getFullYear()
  224. const m = String(date.getMonth() + 1).padStart(2, '0')
  225. const d = String(date.getDate()).padStart(2, '0')
  226. const hh = String(date.getHours()).padStart(2, '0')
  227. const mm = String(date.getMinutes()).padStart(2, '0')
  228. const ss = String(date.getSeconds()).padStart(2, '0')
  229. return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
  230. }
  231. // 生成默认时间段(当前时间+30分钟 ~ 当前时间+1小时30分钟)
  232. const getDefaultTimeRange = () => {
  233. const now = new Date()
  234. const start = new Date(now.getTime() + 1 * 60000) // 加30分钟
  235. const end = new Date(start.getTime() + 60 * 60000) // 加1小时
  236. return {
  237. startTime: formatDateTime(start),
  238. endTime: formatDateTime(end)
  239. }
  240. }
  241. // 时间合规性判断
  242. function isBeforeNineAM(date) {
  243. return date.getHours() < 9
  244. }
  245. function isAfterEightPM(date) {
  246. return date.getHours() > 20 || (date.getHours() === 20 && date.getMinutes() > 0)
  247. }
  248. // ==================== 数据存储 ====================
  249. const getAddressFromStorage = (key) => {
  250. const address = uni.getStorageSync(key)
  251. return address || {
  252. id: '',
  253. name: '',
  254. phone: '',
  255. address: '',
  256. provinceName: '',
  257. cityName: '',
  258. countyName: '',
  259. isDefault: false
  260. }
  261. }
  262. const saveAddressToStorage = (key, address) => {
  263. uni.setStorageSync(key, address)
  264. }
  265. // ==================== 页面参数 ====================
  266. const product = ref('1') // 1-京东,2-顺丰
  267. // ==================== 地址信息 ====================
  268. const addressSend = ref(getAddressFromStorage('senderAddress'))
  269. const addressReceive = ref(getAddressFromStorage('receiverAddress'))
  270. // ==================== 物品信息 ====================
  271. const goodsInfo = ref('')
  272. const weight = ref(0.5)
  273. const volume = ref(0.001)
  274. const quantity = ref(1)
  275. // ==================== 时间选择 ====================
  276. const showTimePicker = ref(false)
  277. const selectedTimeData = ref({
  278. startTime: undefined,
  279. endTime: undefined
  280. })
  281. // 动态生成显示标签(不依赖额外字段)
  282. const selectedTimeLabel = computed(() => {
  283. if (!selectedTimeData.value.startTime) return '一小时内'
  284. const start = new Date(selectedTimeData.value.startTime.replace(/-/g, '/'))
  285. const end = new Date(selectedTimeData.value.endTime.replace(/-/g, '/'))
  286. const now = new Date()
  287. const isToday = start.toDateString() === now.toDateString()
  288. const isTomorrow = start.toDateString() === new Date(now.getTime() + 86400000).toDateString()
  289. const dateLabel = isToday ? '今天' : (isTomorrow ? '明天' : `${start.getMonth() + 1}月${start.getDate()}日`)
  290. const timeLabel = `${pad(start.getHours())}:${pad(start.getMinutes())}-${pad(end.getHours())}:${pad(end.getMinutes())}`
  291. return `${dateLabel} ${timeLabel}`
  292. })
  293. // ==================== 快递类型字典 ====================
  294. const jdDictList = ref([])
  295. const sfDictList = ref([])
  296. const expressTypeIndex = ref(0)
  297. const expressTypes = ref([])
  298. const fetchDictData = async () => {
  299. try {
  300. uni.showLoading({ title: '加载字典...' })
  301. const [jdRes, sfRes] = await Promise.all([
  302. dictList('jd_logistics_product_code'),
  303. dictList('sf_logistics_product_code')
  304. ])
  305. if (jdRes.code === 200) jdDictList.value = jdRes.data || []
  306. if (sfRes.code === 200) sfDictList.value = sfRes.data || []
  307. updateExpressTypes()
  308. } catch (error) {
  309. console.error('获取快递类型字典失败', error)
  310. uni.showToast({ title: '字典加载失败', icon: 'none' })
  311. } finally {
  312. uni.hideLoading()
  313. }
  314. }
  315. const updateExpressTypes = () => {
  316. let sourceList = product.value === '1' ? jdDictList.value : sfDictList.value
  317. expressTypes.value = sourceList.map(item => ({
  318. label: item.dictLabel,
  319. value: item.dictValue
  320. }))
  321. expressTypeIndex.value = 0
  322. }
  323. // ==================== 增值服务 ====================
  324. const valueServices = ref({
  325. isPack: false,
  326. isOverLongWeight: false,
  327. isReceiptCollect: false,
  328. isWoodenCrate: false
  329. })
  330. const insuranceAmountChecked = ref(false)
  331. const insuranceAmount = ref('')
  332. const insuranceAmountError = ref('')
  333. const receiptReturnTypeIndex = ref(0)
  334. const receiptReturnTypes = ref([
  335. { label: '电子凭证', value: 'electronic' },
  336. { label: '纸质凭证', value: 'paper' }
  337. ])
  338. const showValueAdded = ref(false)
  339. const agreed = ref(true)
  340. // ==================== 表单操作 ====================
  341. const increaseWeight = () => {
  342. weight.value = parseFloat((weight.value + 0.1).toFixed(3))
  343. if (weight.value > 100) weight.value = 100
  344. }
  345. const decreaseWeight = () => {
  346. weight.value = parseFloat((weight.value - 0.1).toFixed(3))
  347. if (weight.value < 0.1) weight.value = 0.1
  348. }
  349. const handleWeightInput = (e) => {
  350. let value = e.detail.value.replace(/[^\d.]/g, '')
  351. const parts = value.split('.')
  352. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  353. if (value.includes('.')) {
  354. const decimal = value.split('.')[1]
  355. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  356. }
  357. const num = parseFloat(value) || 0.1
  358. weight.value = Math.max(0.1, Math.min(100, num))
  359. }
  360. const increaseVolume = () => {
  361. volume.value = parseFloat((volume.value + 0.001).toFixed(3))
  362. }
  363. const decreaseVolume = () => {
  364. volume.value = parseFloat((volume.value - 0.001).toFixed(3))
  365. if (volume.value < 0.001) volume.value = 0.001
  366. }
  367. const handleVolumeInput = (e) => {
  368. let value = e.detail.value.replace(/[^\d.]/g, '')
  369. const parts = value.split('.')
  370. if (parts.length > 2) value = parts[0] + '.' + parts.slice(1).join('')
  371. if (value.includes('.')) {
  372. const decimal = value.split('.')[1]
  373. if (decimal && decimal.length > 3) value = parseFloat(value).toFixed(3)
  374. }
  375. const num = parseFloat(value) || 0.001
  376. volume.value = Math.max(0.001, Math.min(10, num))
  377. }
  378. const increaseQuantity = () => {
  379. quantity.value += 1
  380. }
  381. const decreaseQuantity = () => {
  382. quantity.value -= 1
  383. if (quantity.value < 1) quantity.value = 1
  384. }
  385. const handleQuantityInput = (e) => {
  386. let value = e.detail.value.replace(/\D/g, '')
  387. const int = parseInt(value) || 1
  388. quantity.value = int
  389. }
  390. const onGoodsInfoInput = (e) => {
  391. goodsInfo.value = e.detail.value
  392. if (goodsInfo.value.length > 50) goodsInfo.value = goodsInfo.value.substring(0, 50)
  393. }
  394. const onExpressTypeChange = (e) => {
  395. expressTypeIndex.value = e.detail.value
  396. }
  397. const onPackagingChange = (e) => valueServices.value.isPack = e.detail.value
  398. const onOverweightChange = (e) => valueServices.value.isOverLongWeight = e.detail.value
  399. const onWoodenFrameChange = (e) => valueServices.value.isWoodenCrate = e.detail.value
  400. const onInsuranceChange = (e) => {
  401. insuranceAmountChecked.value = e.detail.value
  402. if (e.detail.value) insuranceAmount.value = '0'
  403. else {
  404. insuranceAmount.value = ''
  405. insuranceAmountError.value = ''
  406. }
  407. }
  408. const onSignReturnChange = (e) => {
  409. valueServices.value.isReceiptCollect = e.detail.value
  410. if (!e.detail.value) receiptReturnTypeIndex.value = 0
  411. }
  412. const onReceiptReturnTypeChange = (e) => receiptReturnTypeIndex.value = e.detail.value
  413. const validateInsuranceAmount = () => {
  414. if (!insuranceAmount.value) return true
  415. const amount = parseFloat(insuranceAmount.value)
  416. if (isNaN(amount) || amount < 0) {
  417. insuranceAmountError.value = '保价金额必须为数字且大于0'
  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. const handleTimeClick = () => showTimePicker.value = true
  429. const handleTimeConfirm = (time) => {
  430. // 只保留核心字段,避免依赖额外字段
  431. selectedTimeData.value = {
  432. startTime: time.startTime,
  433. endTime: time.endTime
  434. }
  435. showTimePicker.value = false
  436. }
  437. const openAddressBook = (type) => {
  438. uni.navigateTo({ url: `/pages/address/address_list?addType=${type}&from=order` })
  439. }
  440. const handleAddAddress = (type) => {
  441. uni.navigateTo({ url: `/pages/address/edit?addType=${type}&from=order` })
  442. }
  443. const toggleAgreement = () => agreed.value = !agreed.value
  444. // ==================== 表单验证(增强) ====================
  445. const validateForm = () => {
  446. if (!addressSend.value.addressId) {
  447. uni.showToast({ title: '请选择寄件人地址', icon: 'none' })
  448. return false
  449. }
  450. if (!addressReceive.value.addressId) {
  451. uni.showToast({ title: '请选择收件人地址', icon: 'none' })
  452. return false
  453. }
  454. if (!expressTypes.value.length) {
  455. uni.showToast({ title: '快递类型字典加载中,请稍后', icon: 'none' })
  456. return false
  457. }
  458. if (!expressTypes.value[expressTypeIndex.value]) {
  459. uni.showToast({ title: '请选择快递类型', icon: 'none' })
  460. return false
  461. }
  462. if (!goodsInfo.value.trim()) {
  463. uni.showToast({ title: '请输入物品信息', icon: 'none' })
  464. return false
  465. }
  466. if (goodsInfo.value.length > 50) {
  467. uni.showToast({ title: '物品信息不能超过50字', icon: 'none' })
  468. return false
  469. }
  470. if (weight.value < 0) {
  471. uni.showToast({ title: '请输入正确的重量', icon: 'none' })
  472. return false
  473. }
  474. if (volume.value < 0) {
  475. uni.showToast({ title: '请输入正确的体积', icon: 'none' })
  476. return false
  477. }
  478. if (quantity.value < 1) {
  479. uni.showToast({ title: '请输入正确的件数', icon: 'none' })
  480. return false
  481. }
  482. if (insuranceAmount.value !== '' && insuranceAmount.value !== null && !validateInsuranceAmount()) {
  483. uni.showToast({ title: insuranceAmountError.value || '保价金额无效', icon: 'none' })
  484. return false
  485. }
  486. if (product.value === '2' && valueServices.value.isReceiptCollect && !receiptReturnTypes.value[receiptReturnTypeIndex.value]) {
  487. uni.showToast({ title: '请选择签单返还类型', icon: 'none' })
  488. return false
  489. }
  490. if (!agreed.value) {
  491. uni.showToast({ title: '请同意服务协议', icon: 'none' })
  492. return false
  493. }
  494. // 时间处理:若无选择则使用默认时间段
  495. if (!selectedTimeData.value.startTime) {
  496. const defaultRange = getDefaultTimeRange()
  497. selectedTimeData.value = defaultRange
  498. }
  499. // 时间合规性检查(9:00-20:00)
  500. const startDate = new Date(selectedTimeData.value.startTime.replace(/-/g, '/'))
  501. const endDate = new Date(selectedTimeData.value.endTime.replace(/-/g, '/'))
  502. if (isBeforeNineAM(startDate) || isAfterEightPM(startDate)) {
  503. uni.showToast({ title: '请选择 9:00-20:00 之间的取件时间', icon: 'none' })
  504. selectedTimeData.value = { startTime: undefined, endTime: undefined }
  505. return false
  506. }
  507. if (isAfterEightPM(endDate)) {
  508. uni.showToast({ title: '取件结束时间不能晚于 20:00', icon: 'none' })
  509. selectedTimeData.value = { startTime: undefined, endTime: undefined }
  510. return false
  511. }
  512. return true
  513. }
  514. const isSubmit = ref(false)
  515. // ==================== 提交订单 ====================
  516. const submitOrder = async () => {
  517. if (!validateForm()) return
  518. if (isSubmit.value) return
  519. isSubmit.value = true
  520. const addedService = {
  521. isPack: valueServices.value.isPack || null,
  522. guaranteeMoney: insuranceAmount.value && insuranceAmount.value !== '' ? parseFloat(insuranceAmount.value) : null,
  523. isReceiptCollect: product.value === '2'
  524. ? (valueServices.value.isReceiptCollect ? receiptReturnTypes.value[receiptReturnTypeIndex.value]?.value : null)
  525. : (valueServices.value.isReceiptCollect ? true : null),
  526. isOverLongWeight: valueServices.value.isOverLongWeight || null,
  527. isWoodenCrate: valueServices.value.isWoodenCrate || null
  528. }
  529. Object.keys(addedService).forEach(key => {
  530. if (addedService[key] === null || addedService[key] === undefined) delete addedService[key]
  531. })
  532. const orderData = {
  533. orderType: product.value,
  534. senderName: addressSend.value.contactName,
  535. senderPhone: addressSend.value.contactPhone,
  536. senderProvince: addressSend.value.provinceName,
  537. senderCity: addressSend.value.cityName,
  538. senderCounty: addressSend.value.countyName,
  539. senderAddress: addressSend.value.detailedAddress,
  540. receiverName: addressReceive.value.contactName,
  541. receiverPhone: addressReceive.value.contactPhone,
  542. receiverProvince: addressReceive.value.provinceName,
  543. receiverCity: addressReceive.value.cityName,
  544. receiverCounty: addressReceive.value.countyName,
  545. receiverAddress: addressReceive.value.detailedAddress,
  546. goodsName: goodsInfo.value,
  547. goodsWeight: weight.value,
  548. goodsVolume: volume.value,
  549. goodsQty: quantity.value,
  550. sendStartTime: selectedTimeData.value.startTime,
  551. sendEndTime: selectedTimeData.value.endTime,
  552. productCode: expressTypes.value[expressTypeIndex.value]?.value,
  553. addedService: JSON.stringify(addedService)
  554. }
  555. uni.showLoading({ title: '提交中...', mask: true })
  556. try {
  557. const res = await createOrder(orderData)
  558. uni.hideLoading()
  559. if (res.code === 200) {
  560. uni.showToast({ title: '下单成功', icon: 'success', mask: true })
  561. uni.switchTab({ url: '/pages/order/index' })
  562. } else {
  563. isSubmit.value = false
  564. selectedTimeData.value = { startTime: undefined, endTime: undefined }
  565. uni.showToast({ title: res.msg || '下单失败', icon: 'none' })
  566. }
  567. } catch (error) {
  568. isSubmit.value = false
  569. uni.showToast({ title: error, icon: 'none' })
  570. console.error('订单提交失败', error)
  571. selectedTimeData.value = { startTime: undefined, endTime: undefined }
  572. }
  573. }
  574. // ==================== 获取默认寄件地址 ====================
  575. const fetchDefaultSenderAddress = async () => {
  576. try {
  577. const res = await getDefaultAddress()
  578. if (res.code === 200 && res.data) {
  579. addressSend.value = res.data
  580. addressSend.value.defaultFlag = res.data.defaultFlag === '1'
  581. saveAddressToStorage('senderAddress', res.data)
  582. }
  583. } catch (error) {
  584. console.error('获取默认地址失败', error)
  585. }
  586. }
  587. // ==================== 生命周期 ====================
  588. onLoad((option) => {
  589. if (option.product) product.value = option.product
  590. fetchDictData()
  591. })
  592. onMounted(() => {
  593. uni.$on('addressSelected', (data) => {
  594. if (data.type === 'sender') {
  595. addressSend.value = data.address
  596. saveAddressToStorage('senderAddress', data.address)
  597. } else if (data.type === 'receiver') {
  598. addressReceive.value = data.address
  599. saveAddressToStorage('receiverAddress', data.address)
  600. }
  601. })
  602. if (!addressSend.addressId) fetchDefaultSenderAddress()
  603. })
  604. onUnmounted(() => {
  605. uni.$off('addressSelected')
  606. })
  607. </script>
  608. <style lang="scss" scoped>
  609. /* 完全保持原有样式,未做任何修改 */
  610. .order-container {
  611. min-height: 100vh;
  612. background-color: #F5F7FA;
  613. padding-bottom: 180rpx;
  614. box-sizing: border-box;
  615. }
  616. .info-card {
  617. margin: 0rpx 20rpx;
  618. background: #fff;
  619. border-radius: 32rpx;
  620. padding: 20rpx;
  621. margin-bottom: 20rpx;
  622. .address-item {
  623. display: flex;
  624. align-items: center;
  625. &:first-child {
  626. padding-bottom: 20rpx;
  627. }
  628. &:last-child {
  629. padding-top: 20rpx;
  630. }
  631. }
  632. .user-info {
  633. flex: 1;
  634. margin-left: 26rpx;
  635. .line {
  636. width: 590rpx;
  637. height: 3rpx;
  638. background-color: #F1F3F8;
  639. }
  640. .create-btn {
  641. height: 88rpx;
  642. font-weight: 400;
  643. font-size: 40rpx;
  644. color: #999999;
  645. line-height: 88rpx;
  646. }
  647. }
  648. .right {
  649. margin-left: 20rpx;
  650. }
  651. .img-status-text {
  652. align-self: center;
  653. text-align: center;
  654. flex-shrink: 0;
  655. .img {
  656. width: 56rpx;
  657. height: 56rpx;
  658. }
  659. .img-change {
  660. width: 40rpx;
  661. height: 40rpx;
  662. }
  663. .address-image {
  664. width: 48rpx;
  665. height: 48rpx;
  666. }
  667. .status-text {
  668. font-size: 28rpx;
  669. color: #333;
  670. font-weight: 400;
  671. }
  672. }
  673. }
  674. .pickup-title {
  675. margin: 0rpx 20rpx;
  676. font-size: 32rpx;
  677. font-weight: 600;
  678. color: #333;
  679. margin-bottom: 20rpx;
  680. }
  681. .goods-card {
  682. margin: 0rpx 20rpx;
  683. background-color: #fff;
  684. border-radius: 32rpx;
  685. padding: 0rpx 20rpx;
  686. margin-bottom: 20rpx;
  687. }
  688. .goods-item {
  689. display: flex;
  690. justify-content: space-between;
  691. align-items: center;
  692. height: 100rpx;
  693. border-bottom: 1rpx solid #F1F3F8;
  694. &:last-child {
  695. border-bottom: none;
  696. margin-bottom: 0;
  697. }
  698. .item-label {
  699. font-size: 28rpx;
  700. color: #666;
  701. font-weight: 400;
  702. &.required::before {
  703. content: '*';
  704. color: #ff4444;
  705. margin-right: 8rpx;
  706. }
  707. }
  708. .item-control {
  709. display: flex;
  710. align-items: center;
  711. .picker-text {
  712. font-size: 28rpx;
  713. color: #333;
  714. text-align: right;
  715. margin-right: 8rpx;
  716. }
  717. &.btn {
  718. background: #fff;
  719. border: 1rpx solid #dcdfe6;
  720. border-radius: 5rpx;
  721. height: 59rpx;
  722. width: 266rpx;
  723. box-sizing: border-box;
  724. }
  725. .control-btn {
  726. width: 56rpx;
  727. height: 56rpx;
  728. background: #F5F7FA;
  729. border-radius: 4rpx;
  730. display: flex;
  731. align-items: center;
  732. justify-content: center;
  733. font-size: 36rpx;
  734. color: #666;
  735. &.minus {
  736. color: #999;
  737. border-right: 1rpx solid #dcdfe6;
  738. }
  739. &.plus {
  740. color: #999;
  741. border-left: 1rpx solid #dcdfe6;
  742. }
  743. }
  744. .control-value {
  745. width: 154rpx;
  746. text-align: center;
  747. font-size: 24rpx;
  748. color: #333;
  749. .input-field {
  750. width: 100%;
  751. height: 100rpx;
  752. line-height: 100rpx;
  753. padding: 0 24rpx;
  754. font-size: 28rpx;
  755. color: #333;
  756. text-align: center;
  757. &::placeholder {
  758. color: #999;
  759. }
  760. }
  761. }
  762. }
  763. .input-field {
  764. width: 100%;
  765. height: 100rpx;
  766. line-height: 100rpx;
  767. padding: 0 24rpx;
  768. font-size: 28rpx;
  769. color: #333;
  770. text-align: right;
  771. &::placeholder {
  772. color: #999;
  773. }
  774. }
  775. .time-value {
  776. display: flex;
  777. align-items: center;
  778. font-size: 28rpx;
  779. color: #333;
  780. line-height: 88rpx;
  781. height: 88rpx;
  782. .value {
  783. margin-right: 16rpx;
  784. }
  785. .arrow {
  786. margin-left: 32rpx;
  787. color: #999;
  788. font-size: 28rpx;
  789. font-weight: bold;
  790. }
  791. }
  792. .arrow {
  793. color: #999;
  794. }
  795. }
  796. .agreement-card {
  797. margin: 0rpx 20rpx;
  798. background-color: #fff;
  799. border-radius: 32rpx;
  800. padding: 30rpx 20rpx;
  801. margin-bottom: 20rpx;
  802. .agreement-item {
  803. display: flex;
  804. align-items: center;
  805. margin-bottom: 20rpx;
  806. .agreement-text {
  807. font-size: 28rpx;
  808. color: #333;
  809. margin-left: 10rpx;
  810. }
  811. }
  812. .price-notice {
  813. font-size: 24rpx;
  814. color: #ff7d00;
  815. text-align: center;
  816. }
  817. }
  818. .add-btn-container {
  819. width: 100%;
  820. position: fixed;
  821. bottom: 0rpx;
  822. padding: 32rpx;
  823. background-color: #fff;
  824. border-top: 1rpx solid #eee;
  825. box-sizing: border-box;
  826. /* 确定按钮 */
  827. .submit-btn {
  828. height: 88rpx;
  829. background: #1B64F0;
  830. border-radius: 44rpx;
  831. color: #fff;
  832. font-size: 32rpx;
  833. font-weight: 500;
  834. height: 88rpx;
  835. line-height: 88rpx;
  836. text-align: center
  837. }
  838. }
  839. .safe-area {
  840. height: 140rpx;
  841. }
  842. </style>