This commit is contained in:
2026-06-13 12:20:11 +03:30
parent 68be4fd5a1
commit f7ba6963ef
10 changed files with 236 additions and 267 deletions
-74
View File
@@ -1,74 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
BasicActionSheet: typeof import('./src/components/BasicPacks/BasicActionSheet.vue')['default']
BasicButton: typeof import('./src/components/BasicPacks/BasicButton.vue')['default']
BasicDraggableFileUpload: typeof import('./src/components/BasicPacks/BasicUploads/BasicDraggableFileUpload.vue')['default']
BasicIcon: typeof import('./src/components/BasicPacks/BasicIcon.vue')['default']
BasicInfoItem: typeof import('./src/components/BasicPacks/BasicInfoItem.vue')['default']
BasicInput: typeof import('./src/components/BasicPacks/BasicInput.vue')['default']
BasicModal: typeof import('./src/components/BasicPacks/BasicModal.vue')['default']
BasicPriceInput: typeof import('./src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue')['default']
BasicSelect: typeof import('./src/components/BasicPacks/BasicSelect.vue')['default']
BasicTags: typeof import('./src/components/BasicPacks/BasicTags/index.vue')['default']
BasicTextArea: typeof import('./src/components/BasicPacks/BasicTextArea.vue')['default']
BasicToggleTwoItem: typeof import('./src/components/BasicPacks/BasicToggleTwoItem.vue')['default']
Bg_main: typeof import('./src/components/bg_main.vue')['default']
Bordered: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/bordered.vue')['default']
DateRender: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/date-render.vue')['default']
Demo: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/index.vue')['default']
DialogVant: typeof import('./src/components/DialogVant.vue')['default']
DialogVantOneButton: typeof import('./src/components/DialogVantOneButton.vue')['default']
Disabled: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled.vue')['default']
DisabledDate: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled-date.vue')['default']
Empty: typeof import('./src/components/empty.vue')['default']
ExtraFooter: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/extra-footer.vue')['default']
Format: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/format.vue')['default']
GoldLoading: typeof import('./src/components/GoldLoading.vue')['default']
ListAnim: typeof import('./src/components/ListAnim.vue')['default']
Mode: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/mode.vue')['default']
PresettedRanges: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/presetted-ranges.vue')['default']
RangePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/range-picker.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectInRange: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/select-in-range.vue')['default']
Size: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/size.vue')['default']
StartEnd: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/start-end.vue')['default']
Suffix: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/suffix.vue')['default']
Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default']
Text: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/text.vue')['default']
Time: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/time.vue')['default']
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanDialog: typeof import('vant/es')['Dialog']
VanField: typeof import('vant/es')['Field']
}
}
+10
View File
@@ -0,0 +1,10 @@
import useAxios from '@/utils/useAxios'
export interface StoreCategory {
id: number
name: string
sort_order?: number
}
export const getStoreCategories = async (): Promise<StoreCategory[]> =>
await useAxios.get<StoreCategory[]>('/customer/categories')
+56
View File
@@ -0,0 +1,56 @@
<template>
<img
:src="currentSrc"
:class="[imgClass, dimmed && 'product-image--dimmed']"
:alt="alt"
@error="handleError"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import {
getProductFallbackImage,
resolveProductImageSrc,
type ProductImageSource
} from '@/utils/productImage'
const props = withDefaults(
defineProps<{
product?: ProductImageSource
imgClass?: string
alt?: string
dimmed?: boolean
}>(),
{
imgClass: 'max-h-full max-w-full object-contain',
alt: '',
dimmed: false
}
)
const currentSrc = ref(resolveProductImageSrc(props.product))
const usingFallback = ref(false)
watch(
() => props.product,
(product) => {
usingFallback.value = false
currentSrc.value = resolveProductImageSrc(product)
},
{ deep: true }
)
function handleError() {
if (usingFallback.value) return
usingFallback.value = true
currentSrc.value = getProductFallbackImage(props.product)
}
</script>
<style scoped>
.product-image--dimmed {
opacity: 0.45;
filter: grayscale(1);
}
</style>
+51
View File
@@ -0,0 +1,51 @@
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg'
export type ProductImageSource = {
file?: { src?: string | null } | null
group_type?: string | null
} | null | undefined
function isUsableImageSrc(src?: string | null): boolean {
if (!src) return false
const value = src.trim()
return value !== '' && value !== 'null' && value !== 'undefined'
}
export function getProductFallbackImage(product?: ProductImageSource): string {
const type = (product?.group_type ?? '').trim()
if (!type) return defaultImg
const exactMap: Record<string, string> = {
گوی: goyImg,
مکعب: mokaabImg,
سکه: sekkeImg,
'شمش ونوس': venusImg,
'سکه پارسیان': parsianImg,
شمش: defaultImg,
آبشده: defaultImg,
}
if (exactMap[type]) return exactMap[type]
if (type.includes('پارسیان')) return parsianImg
if (type.includes('ونوس')) return venusImg
if (type.includes('گوی')) return goyImg
if (type.includes('مکعب')) return mokaabImg
if (type.includes('سکه')) return sekkeImg
if (type.includes('شمش')) return defaultImg
return defaultImg
}
export function resolveProductImageSrc(product?: ProductImageSource): string {
const remoteSrc = product?.file?.src
if (isUsableImageSrc(remoteSrc)) {
return remoteSrc!.trim()
}
return getProductFallbackImage(product)
}
+4 -35
View File
@@ -39,10 +39,9 @@
<div class="flex gap-4">
<!-- Product Image -->
<div class="bg-gray-100 rounded-lg flex justify-center items-center p-1 min-w-[80px] h-[80px]">
<img
:src="getProductImage(item.product)"
class="max-w-full max-h-full object-contain"
alt=""
<ProductImage
:product="item.product"
img-class="max-w-full max-h-full object-contain"
/>
</div>
@@ -151,16 +150,10 @@ import {useFundUserStore} from '@/store/fundStore'
import type {Price} from '@/models/home'
import type {Order} from '@/models/order'
// Image imports
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg'
import {loading, getToken} from '@/utils'
import GoldLoading from '@/components/GoldLoading.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import ProductImage from '@/components/ProductImage.vue'
import {registerCodeDiscount} from "@/api/giftCard";
const cartStore = useCartStore()
@@ -208,30 +201,6 @@ async function submitDiscount() {
}
}
function getProductImage(product: any) {
// Check if product has file field from server with src property
if (product?.file?.src) {
return product.file.src
}
// Fallback to local images based on group_type if file is empty or doesn't exist
const type = product?.group_type
switch (type) {
case 'گوی':
return goyImg
case 'مکعب':
return mokaabImg
case 'سکه':
return sekkeImg
case 'شمش ونوس':
return venusImg
case 'سکه پارسیان':
return parsianImg
default:
return defaultImg
}
}
function getProductAmount(amount?: number) {
if (!amount) return ''
if (amount < 1) {
@@ -64,23 +64,27 @@
<template>
<div
class="flex bg-white border border-gray-200 rounded-xl p-3 transition-all duration-300
hover:shadow-lg hover:-translate-y-0.5 cursor-pointer"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
class="product-card relative flex cursor-pointer rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
>
<!-- Image -->
<div class="bg-gray-100 rounded-lg w-24 h-24 flex items-center justify-center shrink-0">
<img
:src="productImage"
class="max-h-30 object-contain"
alt=""
<span
v-if="isOutOfStock"
class="product-card__badge absolute top-2 right-2 z-10 rounded-md bg-gray-700/90 px-2 py-0.5 text-xs font-medium text-white shadow-sm"
>
ناموجود
</span>
<div class="flex h-24 w-24 shrink-0 items-center justify-center rounded-lg bg-gray-100">
<ProductImage
:product="props.ProductItem"
:dimmed="isOutOfStock"
img-class="max-h-20 object-contain"
/>
</div>
<!-- Content -->
<div class="mr-4 flex flex-col justify-between flex-1">
<div class="mr-4 flex flex-1 flex-col justify-between">
<div>
<div class="text-sm font-semibold line-clamp-2 mb-1">
<div class="mb-1 line-clamp-2 text-sm font-semibold">
{{ props.ProductItem.title }}
</div>
@@ -89,17 +93,8 @@
</div>
</div>
<div class="flex items-center justify-between mt-2">
<div class="!text-primary font-bold text-sm">
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
</div>
<span
v-if="!props.ProductItem?.inventory || props.ProductItem?.inventory==0"
class="text-xs bg-gray-300 text-gray-600 px-2 py-0.5 rounded-md"
>
ناموجود
</span>
<div class="mt-2 !text-primary text-sm font-bold">
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
</div>
</div>
</div>
@@ -109,13 +104,7 @@
import { computed } from 'vue'
import type { Product } from '@/models/products'
import { usePrice } from '@/utils/usePrice'
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg'
import ProductImage from '@/components/ProductImage.vue'
interface Props {
ProductItem: Product
@@ -123,6 +112,10 @@ interface Props {
const props = defineProps<Props>()
const isOutOfStock = computed(
() => !props.ProductItem?.inventory || props.ProductItem.inventory === 0
)
const amountProduct = computed(() => {
if (!props.ProductItem.amount) return ''
if (props.ProductItem.amount < 1) {
@@ -131,28 +124,6 @@ const amountProduct = computed(() => {
return `${props.ProductItem.amount} گرم`
})
const productImage = computed(() => {
// Check if product has file field from server with src property
if (props.ProductItem?.file?.src) {
return props.ProductItem.file.src
}
// Fallback to local images based on group_type if file is empty or doesn't exist
switch (props.ProductItem.group_type) {
case 'گوی':
return goyImg
case 'مکعب':
return mokaabImg
case 'سکه':
return sekkeImg
case 'شمش ونوس':
return venusImg
case 'سکه پارسیان':
return parsianImg
default:
return defaultImg
}
})
</script>
<style scoped>
@@ -1,40 +1,33 @@
<template>
<div
class="bg-white border border-gray-200 rounded-xl p-3 transition-all duration-300
hover:shadow-lg hover:-translate-y-1 cursor-pointer"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
class="product-card relative flex h-full flex-col rounded-xl border border-gray-200 bg-white p-3 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg cursor-pointer"
:class="!props.ProductItem.show && 'opacity-50 grayscale'"
>
<!-- Image -->
<div class="bg-gray-100 rounded-lg h-32 flex items-center justify-center mb-3">
<img
:src="productImage"
class="max-h-35 object-contain"
alt=""
<span
v-if="isOutOfStock"
class="product-card__badge absolute top-2 right-2 z-10 rounded-md bg-gray-700/90 px-2 py-0.5 text-xs font-medium text-white shadow-sm"
>
ناموجود
</span>
<div class="mb-3 flex h-32 items-center justify-center rounded-lg bg-gray-100">
<ProductImage
:product="props.ProductItem"
:dimmed="isOutOfStock"
img-class="max-h-28 w-full object-contain"
/>
</div>
<!-- Title -->
<div class="text-sm font-semibold line-clamp-2 mb-2 min-h-[40px]">
<div class="mb-2 min-h-[40px] line-clamp-2 text-sm font-semibold">
{{ props.ProductItem.title }}
</div>
<!-- Amount -->
<div class="text-xs text-gray-500 mb-2">
<div class="mb-2 text-xs text-gray-500">
{{ amountProduct }}
</div>
<!-- Price & Status -->
<div class="flex items-center justify-between">
<div class="!text-primary font-bold text-sm">
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
</div>
<span
v-if="!props.ProductItem?.inventory || props.ProductItem?.inventory==0"
class="text-xs bg-gray-300 text-gray-600 px-2 py-0.5 rounded-md"
>
ناموجود
</span>
<div class="mt-auto !text-primary text-sm font-bold">
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
</div>
</div>
</template>
@@ -43,13 +36,7 @@
import { computed } from 'vue'
import type { Product } from '@/models/products'
import { usePrice } from '@/utils/usePrice'
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg'
import ProductImage from '@/components/ProductImage.vue'
interface Props {
ProductItem: Product
@@ -57,6 +44,10 @@ interface Props {
const props = defineProps<Props>()
const isOutOfStock = computed(
() => !props.ProductItem?.inventory || props.ProductItem.inventory === 0
)
const amountProduct = computed(() => {
if (!props.ProductItem.amount) return ''
if (props.ProductItem.amount < 1) {
@@ -64,29 +55,6 @@ const amountProduct = computed(() => {
}
return `${props.ProductItem.amount} گرم`
})
const productImage = computed(() => {
// Check if product has file field from server with src property
if (props.ProductItem?.file?.src) {
return props.ProductItem.file.src
}
// Fallback to local images based on group_type if file is empty or doesn't exist
switch (props.ProductItem.group_type) {
case 'گوی':
return goyImg
case 'مکعب':
return mokaabImg
case 'سکه':
return sekkeImg
case 'شمش ونوس':
return venusImg
case 'سکه پارسیان':
return parsianImg
default:
return defaultImg
}
})
</script>
<style scoped>
+52 -13
View File
@@ -17,11 +17,25 @@
<BasicIcon v-else icon="vuesax-linear:element-3"></BasicIcon>
</div>
<div class="mx-5 pb-30 pt-2 grid grid-cols-2 gap-2" style="color: #1b2b07">
<div class="mt-3 gap-2 " :class="!viewVertical?'col-span-2':''" v-for="item in items" :key="item.id">
<ProductItemVertical v-if="viewVertical" :ProductItem="item" @click="clickProduct(item)"></ProductItemVertical>
<ProductItem v-else class="col-span-2" :ProductItem="item" @click="clickProduct(item)"></ProductItem>
</div>
<div class="mx-5 pb-30 pt-2 grid grid-cols-2 gap-3 items-stretch">
<template v-if="viewVertical">
<ProductItemVertical
v-for="item in items"
:key="item.id"
class="h-full"
:ProductItem="item"
@click="clickProduct(item)"
/>
</template>
<template v-else>
<ProductItem
v-for="item in items"
:key="item.id"
class="col-span-2 h-full"
:ProductItem="item"
@click="clickProduct(item)"
/>
</template>
</div>
</div>
@@ -44,6 +58,7 @@ import { onMounted, onUnmounted, ref, watch } from 'vue'
import router from '@/router'
import ProductItem from '@/views/base/products/components/ProductItem.vue'
import { getProducts } from '@/api/products'
import { getStoreCategories } from '@/api/categories'
import {
getLastPrices,
lastPrice,
@@ -66,7 +81,7 @@ import ProductItemVertical from "@/views/base/products/components/ProductItemVer
const activekey = ref('همه')
const showFilter = ref()
const selectedProduct = ref<Product>()
const viewVertical=ref<boolean>('true')
const viewVertical = ref(true)
function clickProduct(item: Product) {
if(item?.show){
@@ -81,7 +96,27 @@ function changeTab() {
val.group_type == activekey.value
)
} else items.value = firstItems.value
}
function sortProductsByInventory(products: Product[] | undefined) {
if (!products?.length) return products ?? []
return [...products].sort((a, b) => {
const aInStock = Number(a.inventory ?? 0) > 0 ? 0 : 1
const bInStock = Number(b.inventory ?? 0) > 0 ? 0 : 1
if (aInStock !== bInStock) return aInStock - bInStock
return Number(b.inventory ?? 0) - Number(a.inventory ?? 0)
})
}
async function loadCategoryTabs() {
try {
const categories = await getStoreCategories()
typeItems.value = categories.map((item) => item.name)
} catch (e) {
console.log(e)
typeItems.value = []
}
}
const firstItems = ref<Product[]>()
@@ -91,13 +126,17 @@ const typeItems = ref([])
onMounted(async () => {
try {
loading.value = true
items.value = await getProducts()
items.value?.forEach((val) => {
if (!typeItems.value.includes(val.group_type)) {
typeItems.value.push(val.group_type)
}
})
firstItems.value = items.value
const products = sortProductsByInventory(await getProducts())
items.value = products
firstItems.value = products
await loadCategoryTabs()
if (!typeItems.value.length) {
products.forEach((val) => {
if (val.group_type && !typeItems.value.includes(val.group_type)) {
typeItems.value.push(val.group_type)
}
})
}
await initPrice()
// await setIntervalGetLastPrice()
// console.log('run product')
+16 -37
View File
@@ -6,8 +6,12 @@
<div class="!h-full overflow-y-scroll">
<div class="mx-5 pb-30 pt-5" style="color: #1b2b07">
<!-- Product Image -->
<div class="bg-gray-100 rounded-lg w-full flex justify-center mb-4">
<img :src="getProductImage(product)" class="max-w-full h-auto"/>
<div class="bg-gray-100 rounded-lg w-full flex justify-center mb-4">
<ProductImage
:product="product"
:dimmed="isOutOfStock"
img-class="max-w-full h-auto"
/>
</div>
<!-- Product Title -->
@@ -68,9 +72,10 @@
>
<!-- Image -->
<div class="bg-gray-100 rounded-md flex justify-center p-4 mb-2">
<img
:src="getProductImage(item)"
class="h-20 object-contain"
<ProductImage
:product="item"
:dimmed="!item.inventory || item.inventory === 0"
img-class="h-20 object-contain"
/>
</div>
@@ -129,14 +134,7 @@ import {
} from '@/utils/price'
import {Swiper, SwiperSlide} from 'swiper/vue'
import 'swiper/swiper-bundle.css'
// Image imports
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import parsianImg from '@/assets/img/parsianImg.svg'
import venusImg from '@/assets/img/venousImg.svg'
import ProductImage from '@/components/ProductImage.vue'
const route = useRoute()
const router = useRouter()
@@ -147,6 +145,11 @@ const productId = computed(() => {
const id = route.params.id
return typeof id === 'string' ? parseInt(id) : id
})
const isOutOfStock = computed(
() => !product.value?.inventory || product.value.inventory === 0
)
watch(
() => productId.value,
(newId, oldId) => {
@@ -250,30 +253,6 @@ function goToProduct(id: number) {
router.push({name: 'productDetail', params: {id}})
}
function getProductImage(product: any) {
// Check if product has file field from server with src property
if (product?.file?.src) {
return product.file.src
}
// Fallback to local images based on group_type if file is empty or doesn't exist
const type = product?.group_type
switch (type) {
case 'گوی':
return goyImg
case 'مکعب':
return mokaabImg
case 'سکه':
return sekkeImg
case 'شمش ونوس':
return venusImg
case 'سکه پارسیان':
return parsianImg
default:
return defaultImg
}
}
</script>
<style scoped>
+1 -1
View File
@@ -89,7 +89,7 @@ export default defineConfig({
}
},
server: {
port: 3100,
port: 3200,
},
// base: `/single`,
// base: `/customer`,