| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import { computed, reactive, readonly, ref } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useApi } from '~/composables/useApi'
- import { skillApi } from '~/api/skill'
- import { useAuthStore } from '~/stores/auth'
- import { useFirstOrderDiscountStore } from '~/stores/firstOrderDiscount'
- export interface OrderPopupOptions {
- avatar: string
- name: string
- productType: string
- /** Original price per unit (e.g. coins per hour). First unit may be discounted; rest at this rate. */
- rate: number
- /** unit text, for example "/h" */
- unit?: string
- /** Skill owner userNo (for first-order discount: exclude own products) */
- ownerUserNo?: string
- /** minimum quantity allowed */
- minQuantity?: number
- /** maximum quantity allowed */
- maxQuantity?: number
- /** default quantity when opening the popup */
- defaultQuantity?: number
- /** If set, quantity is fixed and stepper is hidden */
- specificQuantity?: number
- /** 是否为二维码下单 */
- isQrCodeOrder?: boolean
- /** 二维码 code(二维码下单时必传) */
- qrCode?: string
- /** 技能 id(普通技能下单时必传) */
- skillId?: string
- /** 确认回调 */
- onConfirm?: (orderId: string, totalPrice: number) => void
- }
- export interface OrderPopupState {
- visible: boolean
- avatar: string
- name: string
- productType: string
- /** Original unit price */
- rate: number
- unit: string
- /** Skill owner userNo for discount eligibility */
- ownerUserNo: string | null
- quantity: number
- minQuantity: number
- maxQuantity?: number
- /** Fixed quantity when provided, disables manual changes */
- specificQuantity?: number
- /** 是否为二维码下单 */
- isQrCodeOrder: boolean
- /** 二维码 code */
- qrCode: string | null
- /** 技能 id */
- skillId: string | null
- }
- const DEFAULT_MIN_QUANTITY = 1
- const state = reactive<OrderPopupState>({
- visible: false,
- avatar: '',
- name: '',
- productType: '',
- rate: 0,
- unit: '/h',
- ownerUserNo: null,
- quantity: DEFAULT_MIN_QUANTITY,
- minQuantity: DEFAULT_MIN_QUANTITY,
- maxQuantity: undefined,
- specificQuantity: undefined,
- isQrCodeOrder: false,
- qrCode: null,
- skillId: null,
- })
- const confirmHandler = ref<((orderId: string, totalPrice: number) => void) | null>(null)
- // Lazy store access inside computed so Pinia is only used when composable runs in component context
- const firstUnitPrice = computed(() => {
- const firstOrderStore = useFirstOrderDiscountStore()
- const { userProfile } = storeToRefs(useAuthStore())
- return firstOrderStore.getDisplayPrice(state.rate, state.ownerUserNo ?? undefined, userProfile.value?.userNo)
- })
- const hasDiscount = computed(() => {
- const firstOrderStore = useFirstOrderDiscountStore()
- const { userProfile } = storeToRefs(useAuthStore())
- return firstOrderStore.isDiscountEligible(state.rate, state.ownerUserNo ?? undefined, userProfile.value?.userNo)
- })
- const discountAmount = computed(() =>
- hasDiscount.value && state.quantity >= 1 ? state.rate - firstUnitPrice.value : 0,
- )
- const totalPrice = computed(() =>
- state.quantity <= 0
- ? 0
- : firstUnitPrice.value + (state.quantity - 1) * state.rate,
- )
- const clampQuantity = (value: number): number => {
- if (state.specificQuantity != null) return state.specificQuantity
- const min = state.minQuantity || DEFAULT_MIN_QUANTITY
- const max = state.maxQuantity ?? Number.POSITIVE_INFINITY
- if (Number.isNaN(value)) return min
- return Math.min(Math.max(value, min), max)
- }
- const setQuantity = (value: number) => {
- state.quantity = clampQuantity(value)
- }
- const open = (options: OrderPopupOptions) => {
- state.avatar = options.avatar
- state.name = options.name
- state.productType = options.productType
- state.rate = options.rate
- state.unit = options.unit ?? '/h'
- state.ownerUserNo = options.ownerUserNo ?? null
- state.minQuantity = options.minQuantity ?? DEFAULT_MIN_QUANTITY
- state.maxQuantity = options.maxQuantity
- state.specificQuantity = options.specificQuantity
- state.isQrCodeOrder = options.isQrCodeOrder ?? false
- state.qrCode = options.qrCode ?? null
- state.skillId = options.skillId ?? null
- const initialQuantity
- = options.specificQuantity && options.specificQuantity > 0
- ? options.specificQuantity
- : options.defaultQuantity && options.defaultQuantity > 0
- ? options.defaultQuantity
- : state.minQuantity
- setQuantity(initialQuantity)
- confirmHandler.value = options.onConfirm ?? null
- state.visible = true
- }
- const close = () => {
- state.visible = false
- }
- const confirm = async () => {
- const { refreshUser } = useAuth()
- const handler = confirmHandler.value
- let orderId: string | null = null
- const { request } = useApi()
- // 根据是否为二维码下单调用不同的支付接口
- if (state.isQrCodeOrder && state.qrCode) {
- const paymentResult = await request(() =>
- skillApi.orderQrPayment({
- qrCode: state.qrCode as string,
- purchaseQty: state.quantity,
- }),
- )
- orderId = paymentResult?.orderNo ?? null
- }
- else if (state.skillId) {
- const paymentResult = await request(() =>
- skillApi.orderPayment({
- skillId: state.skillId as string,
- purchaseQty: state.quantity,
- }),
- )
- orderId = paymentResult?.orderNo ?? null
- }
- if (!orderId)
- return
- if (handler) {
- handler(orderId, totalPrice.value)
- }
- refreshUser()
- await useFirstOrderDiscountStore().fetchChance()
- // 支付成功后关闭弹窗
- state.visible = false
- }
- export const useOrderPopup = () => {
- return {
- state: readonly(state),
- totalPrice,
- firstUnitPrice,
- hasDiscount,
- discountAmount,
- discountRatePercent: computed(() => useFirstOrderDiscountStore().discountRatePercent),
- open,
- close,
- confirm,
- setQuantity,
- }
- }
|