confirm.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <div>
  3. <!-- 头部地址选择【配送地址】【自提地址】 -->
  4. <AddressSelection v-if="!goodsType" v-model="addressState" class="addressBox" />
  5. <!-- 表头 -->
  6. <div class="d-flex px-6 mb-2">
  7. <div class="img">小图</div>
  8. <div class="headerTitleList">
  9. <div class="title">商品名称</div>
  10. <div class="skuString">商品属性</div>
  11. <div class="price">单价</div>
  12. <div class="buyNum">数量</div>
  13. <div class="subtotal">小计</div>
  14. </div>
  15. </div>
  16. <!-- 购买的商品信息 -->
  17. <v-card class="goodsListBox mb-3 pa-3">
  18. <s-goods-item
  19. v-for="(item, index) in state.orderInfo.items"
  20. :key="item.skuId"
  21. :img="item.picUrl"
  22. :title="item.spuName"
  23. :skuText="item.properties.map((property) => property.valueName).join(' ')"
  24. :price="item.price"
  25. :num="item.count"
  26. :style="{'marginTop': index ? '8px' : '0px'}"
  27. />
  28. </v-card>
  29. <!-- 价格信息 -->
  30. <div>
  31. <div>
  32. <div class="order-item d-flex">
  33. <div class="item-title mr-3 ">商品金额:</div>
  34. <div>¥{{ fen2yuan(state.orderInfo.price.totalPrice) }}</div>
  35. </div>
  36. <!-- 快递配置时,信息的展示 -->
  37. <div
  38. class="order-item d-flex"
  39. v-if="addressState.deliveryType === 1"
  40. >
  41. <div class="item-title mr-3">运{{ spaces() }}费:</div>
  42. <div>
  43. <span class="text-red" v-if="state.orderInfo.price.deliveryPrice > 0">
  44. +¥{{ fen2yuan(state.orderInfo.price.deliveryPrice) }}
  45. </span>
  46. <div class="item-value" v-else>免运费</div>
  47. </div>
  48. </div>
  49. <!-- 门店自提时,需要填写姓名和手机号 -->
  50. </div>
  51. <div v-if="goodsType" class="mt-5" style="width: 800px;">
  52. <TextUI v-model="email" :item="emailObj"></TextUI>
  53. </div>
  54. <div class="mt-5">
  55. <v-text-field
  56. v-model="state.orderPayload.remark"
  57. label="订单备注"
  58. placeholder="建议留言前先与商家沟通"
  59. variant="outlined"
  60. density="compact"
  61. color="primary"
  62. ></v-text-field>
  63. </div>
  64. <div class="total-box-footer d-flex flex-column align-end">
  65. <div class="d-flex">
  66. <div class="mr-3">
  67. <span class="total-num">共</span>
  68. <span class="mx-1" style="color: var(--v-primary-base);">{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}</span>
  69. <span class="total-num">件</span>
  70. </div>
  71. <div>合计:</div>
  72. <div class="total-num text-red"> ¥{{ fen2yuan(state.orderInfo.price.payPrice) }}</div>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. </template>
  78. <script setup>
  79. import { reactive, ref, watch } from 'vue'
  80. import AddressSelection from './addressSelection.vue'
  81. import sGoodsItem from '@/views/mall/components/s-goods-item'
  82. import { fen2yuan } from '@/hooks/web/useGoods'
  83. import { spaces } from '@/utils/index.js'
  84. import { getTradeConfig, createOrder, settlementOrder } from '@/api/mall/trade'
  85. import Snackbar from '@/plugins/snackbar'
  86. import { isNumber } from '@/utils/is'
  87. import { checkCompanyEmail } from '@/utils/validate'
  88. import TextUI from '@/components/FormUI/TextInput'
  89. const emit = defineEmits(['orderCreated'])
  90. const props = defineProps({
  91. data: {
  92. type: String,
  93. default: ''
  94. }
  95. })
  96. const goodsType = ref(0)
  97. const email = ref('') // 如果商品spu的type不是0(不是普通商品)则要填这个邮箱
  98. const state = reactive({
  99. orderPayload: {},
  100. orderInfo: {
  101. items: [], // 商品项列表
  102. price: {}, // 价格信息
  103. },
  104. showCoupon: false, // 是否展示优惠劵
  105. couponInfo: [], // 优惠劵列表
  106. showDiscount: false, // 是否展示营销活动
  107. // ========== 积分 ==========
  108. pointStatus: false, //是否使用积分
  109. });
  110. const emailObj = {
  111. type: 'text',
  112. key: 'email',
  113. value: '',
  114. label: '接收邮箱 *(虚拟商品会发送到填写的邮箱,填写错误的邮箱会导致接收失败,请注意检查!)',
  115. rules: [
  116. value => {
  117. if (value) return true
  118. return '请输入邮箱'
  119. },
  120. value => {
  121. if (checkCompanyEmail(value)) return true
  122. return '请输入正确的邮箱'
  123. }
  124. ],
  125. }
  126. // 检测支付环境
  127. // const payState = reactive({
  128. // orderType: 'goods', // 订单类型; goods - 商品订单, recharge - 充值订单
  129. // orderInfo: {}, // 支付单信息
  130. // payStatus: 0, // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
  131. // payMethods: [], // 可选的支付方式
  132. // payment: '', // 选中的支付方式
  133. // });
  134. const addressState = ref({
  135. addressInfo: {}, // 选择的收货地址
  136. deliveryType: undefined, // 收货方式:1-快递配送,2-门店自提
  137. isPickUp: true, // 门店自提是否开启
  138. pickUpInfo: {}, // 选择的自提门店信息
  139. receiverName: '', // 收件人名称
  140. receiverMobile: '', // 收件人手机
  141. });
  142. watch(
  143. () => props.data,
  144. async (newVal) => {
  145. if (newVal) {
  146. state.orderPayload = JSON.parse(newVal);
  147. // 是否是普通快递商品
  148. goodsType.value = state.orderPayload && isNumber(state.orderPayload?.goodsType-0) ? state.orderPayload.goodsType-0 : 0
  149. if (!goodsType.value) tradeConfig()
  150. else {
  151. // addressState.value.deliveryType = null
  152. getOrderInfo()
  153. }
  154. }
  155. },
  156. { immediate: true },
  157. // { deep: true }
  158. )
  159. async function tradeConfig () {
  160. // 获取交易配置
  161. const data = await getTradeConfig();
  162. addressState.value.isPickUp = data.deliveryPickUpEnabled;
  163. // 价格计算
  164. // 情况一:先自动选择“快递物流”
  165. addressState.value.deliveryType = 1;
  166. let orderCode = await getOrderInfo();
  167. if (orderCode === 0) {
  168. return;
  169. }
  170. // 情况二:失败,再自动选择“门店自提”
  171. if (addressState.value.isPickUp) {
  172. addressState.value.deliveryType = 2;
  173. let orderCode = await getOrderInfo();
  174. if (orderCode === 0) {
  175. return;
  176. }
  177. }
  178. // 情况三:都失败,则不选择
  179. addressState.value.deliveryType = undefined;
  180. await getOrderInfo()
  181. }
  182. // 提交订单
  183. function onConfirm() {
  184. if (addressState.value.deliveryType === 1 && !addressState.value.addressInfo.id) {
  185. Snackbar.warning('请选择收货地址')
  186. return;
  187. }
  188. if (goodsType.value && !email.value) {
  189. Snackbar.warning('请填写邮箱')
  190. return;
  191. }
  192. if (goodsType.value && !checkCompanyEmail(email.value)) {
  193. Snackbar.warning('请输入正确的邮箱')
  194. return;
  195. }
  196. submitOrder()
  197. }
  198. // 创建订单&跳转
  199. async function submitOrder() {
  200. let query = {
  201. items: state.orderPayload.items,
  202. couponId: state.orderPayload.couponId,
  203. remark: state.orderPayload.remark,
  204. deliveryType: addressState.value.deliveryType,
  205. addressId: addressState.value.addressInfo.id, // 收件地址编号
  206. pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
  207. receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
  208. receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
  209. pointStatus: state.pointStatus,
  210. combinationActivityId: state.orderPayload.combinationActivityId,
  211. combinationHeadId: state.orderPayload.combinationHeadId,
  212. seckillActivityId: state.orderPayload.seckillActivityId,
  213. pointActivityId: state.orderPayload.pointActivityId,
  214. }
  215. if (email.value) query = Object.assign(query, { receiverExtend: { email: email.value} })
  216. const data = await createOrder(query)
  217. if (!data.payOrderId && data.payOrderId > 0) return
  218. // 更新购物车列表,如果来自购物车
  219. emit('orderCreated', {
  220. payOrderId: data.payOrderId,
  221. orderId: data.id
  222. })
  223. // payImmediately(data.payOrderId)
  224. }
  225. // 检查库存 & 计算订单价格
  226. async function getOrderInfo() {
  227. // 计算价格
  228. const data = await settlementOrder({
  229. items: state.orderPayload.items,
  230. couponId: state.orderPayload.couponId,
  231. deliveryType: addressState.value.deliveryType,
  232. addressId: addressState.value.addressInfo.id, // 收件地址编号
  233. pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
  234. receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
  235. receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
  236. pointStatus: state.pointStatus,
  237. combinationActivityId: state.orderPayload.combinationActivityId,
  238. combinationHeadId: state.orderPayload.combinationHeadId,
  239. seckillActivityId: state.orderPayload.seckillActivityId,
  240. pointActivityId: state.orderPayload.pointActivityId,
  241. });
  242. state.orderInfo = data;
  243. state.couponInfo = data.coupons || [];
  244. // 设置收货地址
  245. if (state.orderInfo.address) {
  246. addressState.value.addressInfo = state.orderInfo.address;
  247. }
  248. return 0;
  249. }
  250. // 使用 watch 监听地址和配送方式的变化
  251. watch(addressState, async (newAddress, oldAddress) => {
  252. // 如果收货地址或配送方式有变化,则重新计算价格
  253. if (
  254. newAddress.addressInfo.id !== oldAddress.addressInfo.id ||
  255. newAddress.deliveryType !== oldAddress.deliveryType
  256. ) {
  257. await getOrderInfo();
  258. }
  259. });
  260. defineExpose({
  261. onConfirm
  262. })
  263. </script>
  264. <style lang="scss" scoped>
  265. .addressBox {
  266. margin-bottom: 20px;
  267. }
  268. .goodsListBox {
  269. // background: linear-gradient(to bottom, #e93323 0%, #e93323 100%);
  270. // background: linear-gradient(to bottom, var(--v-primary-base) 0%, var(--v-primary-base) 100%);
  271. border-radius: 4px;
  272. }
  273. .order-item {
  274. color: #7a7a7a;
  275. }
  276. .total-box-footer {
  277. .total-num {
  278. color: #7a7a7a;
  279. }
  280. }
  281. .img {
  282. width: 92px
  283. }
  284. .headerTitleList {
  285. width: 100%;
  286. display: flex;
  287. justify-content: space-between;
  288. .title {
  289. width: 30%;
  290. }
  291. .skuString {
  292. width: 25%;
  293. }
  294. .price {
  295. width: 15%;
  296. }
  297. .buyNum {
  298. width: 10%;
  299. }
  300. .subtotal {
  301. width: 15%;
  302. }
  303. }
  304. </style>