useOrderPopup.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { computed, reactive, readonly, ref } from 'vue'
  2. import { storeToRefs } from 'pinia'
  3. import { useApi } from '~/composables/useApi'
  4. import { skillApi } from '~/api/skill'
  5. import { useAuthStore } from '~/stores/auth'
  6. import { useFirstOrderDiscountStore } from '~/stores/firstOrderDiscount'
  7. export interface OrderPopupOptions {
  8. avatar: string
  9. name: string
  10. productType: string
  11. /** Original price per unit (e.g. coins per hour). First unit may be discounted; rest at this rate. */
  12. rate: number
  13. /** unit text, for example "/h" */
  14. unit?: string
  15. /** Skill owner userNo (for first-order discount: exclude own products) */
  16. ownerUserNo?: string
  17. /** minimum quantity allowed */
  18. minQuantity?: number
  19. /** maximum quantity allowed */
  20. maxQuantity?: number
  21. /** default quantity when opening the popup */
  22. defaultQuantity?: number
  23. /** If set, quantity is fixed and stepper is hidden */
  24. specificQuantity?: number
  25. /** 是否为二维码下单 */
  26. isQrCodeOrder?: boolean
  27. /** 二维码 code(二维码下单时必传) */
  28. qrCode?: string
  29. /** 技能 id(普通技能下单时必传) */
  30. skillId?: string
  31. /** 确认回调 */
  32. onConfirm?: (orderId: string, totalPrice: number) => void
  33. }
  34. export interface OrderPopupState {
  35. visible: boolean
  36. avatar: string
  37. name: string
  38. productType: string
  39. /** Original unit price */
  40. rate: number
  41. unit: string
  42. /** Skill owner userNo for discount eligibility */
  43. ownerUserNo: string | null
  44. quantity: number
  45. minQuantity: number
  46. maxQuantity?: number
  47. /** Fixed quantity when provided, disables manual changes */
  48. specificQuantity?: number
  49. /** 是否为二维码下单 */
  50. isQrCodeOrder: boolean
  51. /** 二维码 code */
  52. qrCode: string | null
  53. /** 技能 id */
  54. skillId: string | null
  55. }
  56. const DEFAULT_MIN_QUANTITY = 1
  57. const state = reactive<OrderPopupState>({
  58. visible: false,
  59. avatar: '',
  60. name: '',
  61. productType: '',
  62. rate: 0,
  63. unit: '/h',
  64. ownerUserNo: null,
  65. quantity: DEFAULT_MIN_QUANTITY,
  66. minQuantity: DEFAULT_MIN_QUANTITY,
  67. maxQuantity: undefined,
  68. specificQuantity: undefined,
  69. isQrCodeOrder: false,
  70. qrCode: null,
  71. skillId: null,
  72. })
  73. const confirmHandler = ref<((orderId: string, totalPrice: number) => void) | null>(null)
  74. // Lazy store access inside computed so Pinia is only used when composable runs in component context
  75. const firstUnitPrice = computed(() => {
  76. const firstOrderStore = useFirstOrderDiscountStore()
  77. const { userProfile } = storeToRefs(useAuthStore())
  78. return firstOrderStore.getDisplayPrice(state.rate, state.ownerUserNo ?? undefined, userProfile.value?.userNo)
  79. })
  80. const hasDiscount = computed(() => {
  81. const firstOrderStore = useFirstOrderDiscountStore()
  82. const { userProfile } = storeToRefs(useAuthStore())
  83. return firstOrderStore.isDiscountEligible(state.rate, state.ownerUserNo ?? undefined, userProfile.value?.userNo)
  84. })
  85. const discountAmount = computed(() =>
  86. hasDiscount.value && state.quantity >= 1 ? state.rate - firstUnitPrice.value : 0,
  87. )
  88. const totalPrice = computed(() =>
  89. state.quantity <= 0
  90. ? 0
  91. : firstUnitPrice.value + (state.quantity - 1) * state.rate,
  92. )
  93. const clampQuantity = (value: number): number => {
  94. if (state.specificQuantity != null) return state.specificQuantity
  95. const min = state.minQuantity || DEFAULT_MIN_QUANTITY
  96. const max = state.maxQuantity ?? Number.POSITIVE_INFINITY
  97. if (Number.isNaN(value)) return min
  98. return Math.min(Math.max(value, min), max)
  99. }
  100. const setQuantity = (value: number) => {
  101. state.quantity = clampQuantity(value)
  102. }
  103. const open = (options: OrderPopupOptions) => {
  104. state.avatar = options.avatar
  105. state.name = options.name
  106. state.productType = options.productType
  107. state.rate = options.rate
  108. state.unit = options.unit ?? '/h'
  109. state.ownerUserNo = options.ownerUserNo ?? null
  110. state.minQuantity = options.minQuantity ?? DEFAULT_MIN_QUANTITY
  111. state.maxQuantity = options.maxQuantity
  112. state.specificQuantity = options.specificQuantity
  113. state.isQrCodeOrder = options.isQrCodeOrder ?? false
  114. state.qrCode = options.qrCode ?? null
  115. state.skillId = options.skillId ?? null
  116. const initialQuantity
  117. = options.specificQuantity && options.specificQuantity > 0
  118. ? options.specificQuantity
  119. : options.defaultQuantity && options.defaultQuantity > 0
  120. ? options.defaultQuantity
  121. : state.minQuantity
  122. setQuantity(initialQuantity)
  123. confirmHandler.value = options.onConfirm ?? null
  124. state.visible = true
  125. }
  126. const close = () => {
  127. state.visible = false
  128. }
  129. const confirm = async () => {
  130. const { refreshUser } = useAuth()
  131. const handler = confirmHandler.value
  132. let orderId: string | null = null
  133. const { request } = useApi()
  134. // 根据是否为二维码下单调用不同的支付接口
  135. if (state.isQrCodeOrder && state.qrCode) {
  136. const paymentResult = await request(() =>
  137. skillApi.orderQrPayment({
  138. qrCode: state.qrCode as string,
  139. purchaseQty: state.quantity,
  140. }),
  141. )
  142. orderId = paymentResult?.orderNo ?? null
  143. }
  144. else if (state.skillId) {
  145. const paymentResult = await request(() =>
  146. skillApi.orderPayment({
  147. skillId: state.skillId as string,
  148. purchaseQty: state.quantity,
  149. }),
  150. )
  151. orderId = paymentResult?.orderNo ?? null
  152. }
  153. if (!orderId)
  154. return
  155. if (handler) {
  156. handler(orderId, totalPrice.value)
  157. }
  158. refreshUser()
  159. await useFirstOrderDiscountStore().fetchChance()
  160. // 支付成功后关闭弹窗
  161. state.visible = false
  162. }
  163. export const useOrderPopup = () => {
  164. return {
  165. state: readonly(state),
  166. totalPrice,
  167. firstUnitPrice,
  168. hasDiscount,
  169. discountAmount,
  170. discountRatePercent: computed(() => useFirstOrderDiscountStore().discountRatePercent),
  171. open,
  172. close,
  173. confirm,
  174. setQuantity,
  175. }
  176. }