1 Commits
Author SHA1 Message Date
MojtabaSoumi a9922b33ef 1405/03/21-fix bug in ghatreh 2026-06-11 11:02:50 +03:30
6 changed files with 202 additions and 110 deletions
+1 -4
View File
@@ -7,6 +7,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
@@ -15,16 +16,13 @@ declare module 'vue' {
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider'] ASlider: typeof import('ant-design-vue/es')['Slider']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATag: typeof import('ant-design-vue/es')['Tag'] ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default'] BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default'] Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
@@ -68,6 +66,5 @@ declare module 'vue' {
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default'] TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet'] VanActionSheet: typeof import('vant/es')['ActionSheet']
VanDialog: typeof import('vant/es')['Dialog'] VanDialog: typeof import('vant/es')['Dialog']
VanField: typeof import('vant/es')['Field']
} }
} }
+10 -10
View File
@@ -20,16 +20,16 @@ const router = createRouter({
redirect: {name: 'buyProduct'}, redirect: {name: 'buyProduct'},
component: () => import('../views/base/index.vue'), component: () => import('../views/base/index.vue'),
children: [ children: [
{ // {
path: 'home', // path: 'home',
name: 'home', // name: 'home',
component: () => import('../views/base/home/index.vue') // component: () => import('../views/base/home/index.vue')
}, // },
{ // {
path: 'gold', // path: 'gold',
name: 'gold', // name: 'gold',
component: () => import('@/views/base/gold/index.vue') // component: () => import('@/views/base/gold/index.vue')
}, // },
{ {
path: 'buyProduct', path: 'buyProduct',
name: 'buyProduct', name: 'buyProduct',
+58 -1
View File
@@ -2,6 +2,8 @@ import { computed, ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { CartItem } from '@/models/cart' import type { CartItem } from '@/models/cart'
import type { Product } from '@/models/products' import type { Product } from '@/models/products'
import type { Price } from '@/models/home'
import { getProducts } from '@/api/products'
const CART_STORAGE_KEY = 'ghatre_cart' const CART_STORAGE_KEY = 'ghatre_cart'
@@ -93,6 +95,58 @@ export const useCartStore = defineStore('cart', () => {
return cartItems.value.length return cartItems.value.length
}) })
function syncProductsFromServer(serverProducts: Product[]): number {
const productMap = new Map(serverProducts.map(p => [p.id, p]))
const beforeCount = cartItems.value.length
cartItems.value = cartItems.value
.filter(item => productMap.has(item.product.id))
.map(item => ({
...item,
product: { ...productMap.get(item.product.id)! }
}))
if (cartItems.value.length > 0 || beforeCount > 0) {
saveCartToStorage()
}
return beforeCount - cartItems.value.length
}
function recalculateCartPrices(lastPrice: Price) {
if (!lastPrice || cartItems.value.length === 0) return
cartItems.value.forEach((item) => {
const product = item.product
let newPrice = 0
if (product.group_type == 'آبشده') {
newPrice = lastPrice.price_last ?? 0
} else {
newPrice = product.amount * (lastPrice.price_last ?? 0)
}
product.price = newPrice
product.sellPrice = product.amount * (lastPrice.sell ?? 0)
item.price = newPrice + (product.cash_fee_buy ?? 0)
item.totalPrice = parseInt(String(item.price * item.quantity))
})
saveCartToStorage()
}
async function refreshCartFromServer(): Promise<number> {
if (cartItems.value.length === 0) return 0
try {
const serverProducts: Product[] = await getProducts()
return syncProductsFromServer(serverProducts)
} catch (e) {
console.error('Error refreshing cart from server:', e)
return 0
}
}
return { return {
cartItems, cartItems,
addToCart, addToCart,
@@ -102,7 +156,10 @@ export const useCartStore = defineStore('cart', () => {
totalPrice, totalPrice,
itemCount, itemCount,
loadCartFromStorage, loadCartFromStorage,
saveCartToStorage saveCartToStorage,
syncProductsFromServer,
recalculateCartPrices,
refreshCartFromServer
} }
}) })
+64 -78
View File
@@ -15,7 +15,11 @@
</div> </div>
</div> </div>
<GoldLoading v-model:show="infoVisible" title=""/> <GoldLoading v-model:show="infoVisible" title=""/>
<div class="px-5 pt-26 pb-30"> <div class="px-5 pt-26 pb-30" :class="loading ? 'h-[60vh]' : ''">
<template v-if="loading">
<TransactionSkeleton v-for="i in 3" :key="i" class="mt-4" />
</template>
<template v-else>
<!-- Empty Cart Message --> <!-- Empty Cart Message -->
<div v-if="cartStore.itemCount === 0" class="flex flex-col items-center justify-center h-[60vh]"> <div v-if="cartStore.itemCount === 0" class="flex flex-col items-center justify-center h-[60vh]">
<div class="text-gray-500 text-lg font-semibold mb-4">سبد خرید شما خالی است</div> <div class="text-gray-500 text-lg font-semibold mb-4">سبد خرید شما خالی است</div>
@@ -75,38 +79,42 @@
</div> </div>
<!-- Total and Checkout --> <!-- Total and Checkout -->
<div class="fixed bottom-20 left-0 right-0 bg-white border-t border-gray-200 p-4 shadow-lg z-1"> <div class=" flex justify-center fixed bottom-20 left-0 right-0 z-1">
<div class="flex justify-between items-center mb-4"> <div class="md:w-[426px] w-full bg-white border-t border-gray-200 p-4 shadow-lg">
<div class="text-lg font-semibold">مجموع:</div> <div class="flex justify-between items-center mb-4">
<div class="flex flex-col gap-2 "> <div class="text-lg font-semibold">مجموع:</div>
<div class="text-primary font-bold text-xl"> <div class="flex flex-col gap-2 ">
{{ usePrice(displaytotalPrice) }} <div class="text-primary font-bold text-xl">
{{ usePrice(displaytotalPrice) }}
</div>
</div>
<div class="!text-primary font-bold text-xs line-through cursor-pointer "
v-if="discountInfo?.amount>0"
@click="showDiscountModal=true">
{{ usePrice(totalPrice) }}
</div>
<div v-else
class="!text-primary font-bold text-xs underline-solid underline cursor-pointer"
@click="showDiscountModal=true">
کد تخفیف دارم
</div> </div>
</div> </div>
<BasicButton
<div class="!text-primary font-bold text-xs line-through cursor-pointer " class="w-full py-3"
v-if="discountInfo?.amount>0" type="primary"
@click="showDiscountModal=true"> size="large"
{{ usePrice(totalPrice) }} @click="handleCheckout"
</div> >
<div v-else تسویه حساب
class="!text-primary font-bold text-xs underline-solid underline cursor-pointer" </BasicButton>
@click="showDiscountModal=true">
کد تخفیف دارم
</div>
</div> </div>
<BasicButton
class="w-full py-3"
type="primary"
size="large"
@click="handleCheckout"
>
تسویه حساب
</BasicButton>
</div> </div>
</div> </div>
</template>
</div> </div>
</div> </div>
<AModal v-model:visible="showDiscountModal" ok-text="اعمال کد تخفیف" cancel-text="انصراف" @ok="submitDiscount" <AModal v-model:visible="showDiscountModal" ok-text="اعمال کد تخفیف" cancel-text="انصراف" @ok="submitDiscount"
@@ -152,6 +160,7 @@ import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg' import venusImg from '@/assets/img/venousImg.svg'
import {loading, getToken} from '@/utils' import {loading, getToken} from '@/utils'
import GoldLoading from '@/components/GoldLoading.vue' import GoldLoading from '@/components/GoldLoading.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import {registerCodeDiscount} from "@/api/giftCard"; import {registerCodeDiscount} from "@/api/giftCard";
const cartStore = useCartStore() const cartStore = useCartStore()
@@ -167,28 +176,8 @@ const price = computed((): Price => <Price>lastPrice.value ?? 0)
// Watch for price updates and update cart items // Watch for price updates and update cart items
watch(lastPrice, () => { watch(lastPrice, () => {
if (lastPrice.value && cartStore.cartItems.length > 0) { if (lastPrice.value) {
cartStore.cartItems.forEach((item) => { cartStore.recalculateCartPrices(lastPrice.value)
const product = item.product
let newPrice = 0
if (product.group_type == 'آبشده') {
newPrice = lastPrice.value?.price_last ?? 0
} else {
newPrice = product.amount * (lastPrice.value?.price_last ?? 0)
}
// Update product price
product.price = newPrice
product.sellPrice = product.amount * (lastPrice.value?.sell ?? 0)
// Update item price and total price
item.price = newPrice + (product.cash_fee_buy ?? 0)
item.totalPrice = parseInt(item.price * item.quantity)
})
// Save updated prices to storage
cartStore.saveCartToStorage()
} }
}, {deep: true, immediate: false}) }, {deep: true, immediate: false})
@@ -313,8 +302,8 @@ async function handleCheckout() {
if (cashItemsTotal > 0) { if (cashItemsTotal > 0) {
// Ensure wallet info is loaded // Ensure wallet info is loaded
if (discountInfo.value.amount>0){ if (discountInfo.value?.amount && discountInfo.value?.amount>0){
cashItemsTotal=cashItemsTotal-cashItemsTotal*discountInfo.value.amount cashItemsTotal=cashItemsTotal-cashItemsTotal*discountInfo.value?.amount
} }
if (!walletStore.walletInfo) { if (!walletStore.walletInfo) {
await walletStore.getMyWallet() await walletStore.getMyWallet()
@@ -370,7 +359,7 @@ async function handleCheckout() {
product_id: product.id, product_id: product.id,
method: item.method, method: item.method,
number: item.quantity, number: item.quantity,
discount_code:discountInfo.value.code discount_code:discountInfo.value?.code
} }
}) })
@@ -413,40 +402,37 @@ async function initPrice() {
try { try {
await getLastPrices() await getLastPrices()
await setIntervalGetLastPrice() await setIntervalGetLastPrice()
if (lastPrice.value) {
// Update prices for all cart items cartStore.recalculateCartPrices(lastPrice.value)
if (lastPrice.value && cartStore.cartItems.length > 0) {
cartStore.cartItems.forEach((item) => {
const product = item.product
let newPrice = 0
if (product.group_type == 'آبشده') {
newPrice = lastPrice.value?.price_last ?? 0
} else {
newPrice = product.amount * (lastPrice.value?.price_last ?? 0)
}
// Update product price
product.price = newPrice
product.sellPrice = product.amount * (lastPrice.value?.sell ?? 0)
// Update item price and total price
item.price = newPrice + (product.cash_fee_buy ?? 0)
item.totalPrice = parseInt(item.price * item.quantity)
})
// Save updated prices to storage
cartStore.saveCartToStorage()
} }
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
} }
} }
async function refreshCart() {
const removedCount = await cartStore.refreshCartFromServer()
if (removedCount > 0) {
message.warning(`${removedCount} محصول از سبد خرید حذف شد (دیگر در دسترس نیست)`)
}
if (lastPrice.value) {
cartStore.recalculateCartPrices(lastPrice.value)
}
}
onMounted(async () => { onMounted(async () => {
// Load cart from storage if needed
cartStore.loadCartFromStorage() cartStore.loadCartFromStorage()
await initPrice() loading.value = true
try {
await initPrice()
await refreshCart()
const refreshCartInfo=setInterval(async () => {
await refreshCart()
// await getUpPrice()
}, 10000)
} finally {
loading.value = false
}
}) })
onUnmounted(() => { onUnmounted(() => {
+10 -3
View File
@@ -144,7 +144,9 @@
<!-- <div class="text-xs mt-1 font-bold">دریافت فیزیکی</div>--> <!-- <div class="text-xs mt-1 font-bold">دریافت فیزیکی</div>-->
<!-- </div>--> <!-- </div>-->
<div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')"> <div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')">
<Icon icon="vuesax-linear:shopping-cart" /> <a-badge :count="cartStore.itemCount">
<Icon icon="vuesax-linear:shopping-cart" />
</a-badge>
<div class="text-xs mt-1 font-bold">سبد خرید</div> <div class="text-xs mt-1 font-bold">سبد خرید</div>
</div> </div>
</div> </div>
@@ -180,7 +182,9 @@
<div class="text-xs mt-1 font-bold">خرید محصول</div> <div class="text-xs mt-1 font-bold">خرید محصول</div>
</div>--> </div>-->
<div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')"> <div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')">
<Icon icon="vuesax-linear:shopping-cart" /> <a-badge :count="cartStore.itemCount">
<Icon icon="vuesax-linear:shopping-cart" />
</a-badge>
<div class="text-xs mt-1 font-bold">سبد خرید</div> <div class="text-xs mt-1 font-bold">سبد خرید</div>
</div> </div>
<div :class="setNavColor('login')" class="tab" @click="setRoute('login')"> <div :class="setNavColor('login')" class="tab" @click="setRoute('login')">
@@ -205,11 +209,14 @@ import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import type { Transaction } from '@/models/transaction' import type { Transaction } from '@/models/transaction'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue' import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import {isToken} from "@/utils"; import {isToken} from "@/utils";
import { useCartStore } from '@/store/cartStore'
const currentRoute = computed(() => router.currentRoute.value.name) const cartStore = useCartStore()
const popVisible = ref(false) const popVisible = ref(false)
const currentRoute = computed(() => router.currentRoute.value.name)
function setNavColor(val: string) { function setNavColor(val: string) {
if (currentRoute.value === val) { if (currentRoute.value === val) {
return 'text-primary' return 'text-primary'
+59 -14
View File
@@ -16,7 +16,7 @@
class="text-gray-500 text-xs number-ltr">{{ item.gold }} class="text-gray-500 text-xs number-ltr">{{ item.gold }}
<div>{{item.unit}}</div> <div>{{item.unit}}</div>
</div> </div>
<div class="text-gray-500 flex gap-1 text-xs items-center number-ltr"> <div v-if="!isShopProduct" class="text-gray-500 flex gap-1 text-xs items-center number-ltr">
<div :class="item.cash > 0 ? 'text-green-600':'text-red-600'" class="text-base"> <div :class="item.cash > 0 ? 'text-green-600':'text-red-600'" class="text-base">
{{ usePriceWithoutType(item.cash?? 0) }} {{ usePriceWithoutType(item.cash?? 0) }}
</div> </div>
@@ -25,7 +25,27 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="isGold" class="flex w-full justify-between mt-2"> <template v-if="isShopProduct">
<div class="flex w-full justify-between mt-2">
<div class="text-gray-500">تعداد:</div>
<div class="text-gray-500 text-xs number-ltr">{{ item?.gold ?? 0 }}</div>
</div>
<div class="flex w-full justify-between mt-2">
<div class="text-gray-500">قیمت کل:</div>
<div class="text-gray-500 flex gap-1 text-xs items-center number-ltr">
<div class="text-base">{{ usePriceWithoutType(item.order?.total_price ?? 0) }}</div>
ریال
</div>
</div>
<div class="flex w-full justify-between mt-2">
<div class="text-gray-500">قیمت واحد:</div>
<div class="text-gray-500 flex gap-1 text-xs items-center number-ltr">
<div class="text-base">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
ریال
</div>
</div>
</template>
<div v-else-if="isGold" class="flex w-full justify-between mt-2">
<div class="text-gray-500">قیمت معامله:</div> <div class="text-gray-500">قیمت معامله:</div>
<div class="text-gray-500 flex gap-1 text-xs items-center"> <div class="text-gray-500 flex gap-1 text-xs items-center">
<!-- <div class="text-base">{{ typeof parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0)) }}</div>--> <!-- <div class="text-base">{{ typeof parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0)) }}</div>-->
@@ -60,20 +80,44 @@
ریال ریال
</div> </div>
</div> </div>
<div v-if="item.order" class="flex justify-between mb-3"> <template v-if="isShopProduct">
<div class="text-primary">قیمت هر گرم طلا حین معامله:</div> <div class="flex justify-between mb-3">
<div class="flex text-xs items-center number-ltr"> <div class="text-primary">تعداد:</div>
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price?? 0) }}</div> <div class="flex text-xs items-center number-ltr">
ریال <div class="text-base ml-1">{{ item.order?.number ?? 0 }}</div>
</div>
</div> </div>
</div> <div class="flex justify-between mb-3">
<div class="flex justify-between mb-3"> <div class="text-primary">قیمت کل:</div>
<div class="text-primary">مقدار {{item.wallet?.product? item.wallet.product.title: 'آبشده' }} مبادله شده:</div> <div class="flex text-xs items-center number-ltr">
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr"> <div class="text-base ml-1">{{ usePriceWithoutType(item.order?.total_price ?? 0) }}</div>
<div class="text-base ml-1">{{ item.gold }}</div> ریال
{{item.unit}} </div>
</div> </div>
</div> <div class="flex justify-between mb-3">
<div class="text-primary">قیمت واحد:</div>
<div class="flex text-xs items-center number-ltr">
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
ریال
</div>
</div>
</template>
<template v-else>
<div v-if="item.order" class="flex justify-between mb-3">
<div class="text-primary">قیمت هر گرم طلا حین معامله:</div>
<div class="flex text-xs items-center number-ltr">
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price?? 0) }}</div>
ریال
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-primary">مقدار {{item.wallet?.product? item.wallet.product.title: 'آبشده' }} مبادله شده:</div>
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr">
<div class="text-base ml-1">{{ item.gold }}</div>
{{item.unit}}
</div>
</div>
</template>
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="text-primary">موجودی کیف پول:</div> <div class="text-primary">موجودی کیف پول:</div>
<div :class="item.cash > 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr"> <div :class="item.cash > 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr">
@@ -156,6 +200,7 @@ interface Prop {
const props = withDefaults(defineProps<Prop>(), {}) const props = withDefaults(defineProps<Prop>(), {})
const isUp = computed(() => props.item.type === 3 || props.item.type === 1 || props.item.type === 8 || props.item.type === 9) const isUp = computed(() => props.item.type === 3 || props.item.type === 1 || props.item.type === 8 || props.item.type === 9)
const isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10) const isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10)
const isShopProduct = computed(() => !!props.item?.product_id)
/*const title = computed(() => { /*const title = computed(() => {
if (isUp.value) { if (isUp.value) {
if (isGold.value) { if (isGold.value) {