fix: store customer-product
This commit is contained in:
1
bo/components.d.ts
vendored
1
bo/components.d.ts
vendored
@@ -20,6 +20,7 @@ declare module 'vue' {
|
||||
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
|
||||
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
|
||||
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
|
||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||
|
||||
12
bo/src/components/admin/FavoriteProducts.vue
Normal file
12
bo/src/components/admin/FavoriteProducts.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Default from '@/layouts/default.vue';
|
||||
|
||||
</script>
|
||||
@@ -141,7 +141,7 @@ async function fetchProductList() {
|
||||
if (route.params.category_id)
|
||||
params.append('category_id', String(route.params.category_id))
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?elems=${perPage.value}&${params.toString()}`
|
||||
const url = `/api/v1/restricted/product/list?elems=${perPage.value}&${params.toString()}`
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
@@ -161,7 +161,7 @@ function goToProduct(productId: number, linkRewrite: string) {
|
||||
}
|
||||
localStorage.setItem('back_from_product', JSON.stringify(path))
|
||||
router.push({
|
||||
name: 'customer-product-details',
|
||||
name: 'admin-product-details',
|
||||
params: { product_id: productId, link_rewrite: linkRewrite }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="flex flex-col md:flex-row gap-10">
|
||||
<CategoryMenu />
|
||||
<div class="w-full flex flex-col items-center gap-4">
|
||||
<UTable :data="usersList" :columns="columns" class="flex-1 w-full"
|
||||
:ui="{ root: 'max-w-100wv overflow-auto!' }" />
|
||||
@@ -194,7 +193,7 @@ const columns: TableColumn<Customer>[] = [
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.user_id
|
||||
return h(UButton, {
|
||||
color: 'primary',
|
||||
color: 'info',
|
||||
size: 'sm',
|
||||
variant: 'soft',
|
||||
onClick: () => {
|
||||
|
||||
@@ -222,7 +222,7 @@ const columns: TableColumn<Customer>[] = [
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.user_id
|
||||
return h(UButton, {
|
||||
color: 'primary',
|
||||
color: 'info',
|
||||
size: 'sm',
|
||||
variant: 'soft',
|
||||
onClick: () => {
|
||||
|
||||
@@ -1,75 +1,84 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="">
|
||||
<div class="flex md:flex-row flex-col justify-between gap-8 my-6">
|
||||
<div class="flex-1">
|
||||
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||
class="max-w-full h-auto object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ productData.name }}
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ productData.description }}
|
||||
</p>
|
||||
<div class="text-3xl font-bold text-(--text-sky-light) dark:text-(--text-sky-dark)">
|
||||
{{ productData.price }}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Dimensions:</span>
|
||||
<p class="font-medium dark:text-white">{{ productData.dimensions }}</p>
|
||||
<div class="">
|
||||
<div class="flex md:flex-row flex-col justify-between gap-8 my-6">
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||
class="max-w-full h-auto object-contain" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Seat Height:</span>
|
||||
<p class="font-medium dark:text-white">{{ productData.seatHeight }}</p>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ productData.name }}
|
||||
</h1>
|
||||
|
||||
<UIcon name="material-symbols:favorite"
|
||||
class="cursor-pointer text-2xl transition hover:scale-110"
|
||||
:class="productData.is_favorite ? 'text-red-500' : 'text-gray-400'"
|
||||
@click="toggleFavorite" />
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ productData.description }}
|
||||
</p>
|
||||
<div class="text-3xl font-bold text-(--text-sky-light) dark:text-(--text-sky-dark)">
|
||||
{{ productData.price }}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Dimensions:</span>
|
||||
<p class="font-medium dark:text-white">{{ productData.dimensions }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Seat Height:</span>
|
||||
<p class="font-medium dark:text-white">{{ productData.seatHeight }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<span class="text-sm text-(--text-sky-light) dark:text-(--text-sky-dark) ">Colors:</span>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color"
|
||||
class="w-10 h-10 border-2 transition-all" :class="selectedColor?.id === color.id
|
||||
? 'border-(--accent-blue-light) ring-2 ring-blue-600 ring-offset-2'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
|
||||
:style="{ backgroundColor: color.hex }" :title="color.name" />
|
||||
<div class="flex md:flex-row flex-col justify-between md:items-end items-start gap-5 md:gap-0 md:mb-8 mb-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<span class="text-sm text-(--text-sky-light) dark:text-(--text-sky-dark) ">Colors:</span>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color"
|
||||
class="w-10 h-10 border-2 transition-all" :class="selectedColor?.id === color.id
|
||||
? 'border-(--accent-blue-light) ring-2 ring-blue-600 ring-offset-2'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
|
||||
:style="{ backgroundColor: color.hex }" :title="color.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-5 items-end">
|
||||
<UInputNumber v-model="value" />
|
||||
<UButton color="primary"
|
||||
class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
||||
Add to Cart
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-5 items-end">
|
||||
<UInputNumber v-model="value" />
|
||||
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
||||
Add to Cart
|
||||
</UButton>
|
||||
<ProductCustomization />
|
||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!',
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
]" variant="ghost">
|
||||
{{ tab.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
<div class="py-5 px-3 bg-(--second-light) dark:bg-(--main-dark) rounded-md">
|
||||
<p class="dark:text-white whitespace-pre-line">
|
||||
{{ activeTabContent }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||
<ProductVariants />
|
||||
</div>
|
||||
<ProductCustomization />
|
||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||
'px-15 py-2 cursor-pointer sm:text-nowrap flex items-center! justify-center!',
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
]" variant="ghost">
|
||||
{{ tab.label }}
|
||||
</UButton>
|
||||
</div>
|
||||
<div class="py-5 px-3 bg-(--second-light) dark:bg-(--main-dark) rounded-md">
|
||||
<p class="dark:text-white whitespace-pre-line">
|
||||
{{ activeTabContent }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||
<ProductVariants />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -78,6 +87,8 @@ import { ref, computed } from 'vue'
|
||||
import ProductCustomization from './components/ProductCustomization.vue'
|
||||
import ProductVariants from './components/ProductVariants.vue'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { useRoute } from 'vue-router'
|
||||
interface Color {
|
||||
id: string
|
||||
name: string
|
||||
@@ -97,6 +108,7 @@ interface ProductData {
|
||||
howToUseText: string
|
||||
productDetailsText: string
|
||||
documentsText: string
|
||||
is_favorite: boolean
|
||||
}
|
||||
|
||||
const activeTab = ref('description')
|
||||
@@ -157,6 +169,24 @@ const activeTabContent = computed(() => {
|
||||
if (productData.colors.length > 0) {
|
||||
selectedColor.value = productData.colors[0] as Color
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
async function toggleFavorite() {
|
||||
const url = `/api/v1/restricted/product/favorite/${route.params.product_id}`
|
||||
|
||||
try {
|
||||
if (!productData.is_favorite) {
|
||||
await useFetchJson(url, { method: 'POST' })
|
||||
} else {
|
||||
await useFetchJson(url, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
productData.is_favorite = !productData.is_favorite
|
||||
} catch (e: unknown) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,27 +11,30 @@
|
||||
</template>
|
||||
</UNavigationMenu> -->
|
||||
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div v-if="customerProductStore.loading" class="text-center py-8">
|
||||
<span class="text-gray-600 dark:text-gray-400">Loading products...</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
<div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ customerProductStore.error }}
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="flex gap-2">
|
||||
<CategoryMenu />
|
||||
<UTable :data="productsList" :columns="columns" class="flex-1">
|
||||
<UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
|
||||
<template #expanded="{ row }">
|
||||
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{
|
||||
thead: 'hidden'
|
||||
}" />
|
||||
<UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild"
|
||||
:ui="{
|
||||
thead: 'hidden'
|
||||
}" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<UPagination v-model:page="page" :total="total" :page-size="perPage" />
|
||||
<UPagination v-model:page="page" :total="customerProductStore.total"
|
||||
:page-size="customerProductStore.perPage" />
|
||||
</div>
|
||||
<div v-if="productsList.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div v-if="customerProductStore.productsList.length === 0"
|
||||
class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No products found
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,20 +45,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, h, resolveComponent, computed } from 'vue'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import Default from '@/layouts/default.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import CategoryMenu from '../inner/CategoryMenu.vue'
|
||||
import { useCustomerProductStore } from '@/stores/customer/customer-product'
|
||||
import type { Product } from '@/stores/customer/customer-product'
|
||||
|
||||
interface Product {
|
||||
reference: number
|
||||
product_id: number
|
||||
name: string
|
||||
image_link: string
|
||||
link_rewrite: string
|
||||
}
|
||||
|
||||
const customerProductStore = useCustomerProductStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -101,18 +99,6 @@ const sortField = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const perPage = ref(15)
|
||||
const total = ref(0)
|
||||
|
||||
interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
const productsList = ref<Product[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const filters = computed<Record<string, string>>({
|
||||
get: () => {
|
||||
const q = { ...route.query }
|
||||
@@ -157,36 +143,16 @@ const updateFilter = debounce((columnId: string, val: string) => {
|
||||
filters.value = newFilters
|
||||
}, 400)
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/list/list-products?${params}`
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToProduct(productId: number) {
|
||||
router.push({
|
||||
name: 'product-detail',
|
||||
params: { id: productId }
|
||||
name: 'customer-product-details',
|
||||
params: { product_id: productId }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const selectedCount = ref({
|
||||
product_id: null,
|
||||
count: 0
|
||||
@@ -205,7 +171,7 @@ const UInput = resolveComponent('UInput')
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
|
||||
const columns: TableColumn<Payment>[] = [
|
||||
const columns: TableColumn<Product>[] = [
|
||||
{
|
||||
id: 'expand',
|
||||
cell: ({ row }) =>
|
||||
@@ -351,6 +317,35 @@ const columns: TableColumn<Payment>[] = [
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
class: 'cursor-pointer',
|
||||
color: 'info',
|
||||
variant: 'soft'
|
||||
}, () => 'Show product')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'counta',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UIcon, {
|
||||
onClick: () => customerProductStore.toggleFavorite(row.original),
|
||||
class: [
|
||||
'cursor-pointer text-[20px] transition-transform duration-200 hover:scale-125',
|
||||
row.original.is_favorite ? 'text-red-500' : 'text-blue-500'
|
||||
],
|
||||
name: 'material-symbols:favorite',
|
||||
variant: 'soft',
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -417,13 +412,27 @@ const columnsChild: TableColumn<Payment>[] = [
|
||||
variant: 'solid'
|
||||
}, 'Add to cart')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
onClick: () => {
|
||||
goToProduct(row.original.product_id)
|
||||
},
|
||||
class: 'cursor-pointer',
|
||||
color: 'info',
|
||||
variant: 'soft'
|
||||
}, () => 'Show product')
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
fetchProductList()
|
||||
customerProductStore.fetchProductList()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<div class="p-4">
|
||||
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<ULoader />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<UTree v-if="showTree" :items="treeItems" v-model:expanded="expandedFolders" :key="treeKey" @toggle="onToggle" :get-key="item => item.value">
|
||||
<template #item-wrapper="{ item }">
|
||||
<div class="flex items-start cursor-pointer">
|
||||
|
||||
<div class="flex items-start cursor-pointer" @click.stop="!item.isFolder">
|
||||
<div class="flex items-center gap-1">
|
||||
<UIcon :name="item.icon" :size="30" />
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-[15px] font-medium">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
|
||||
<UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral"
|
||||
variant="outline" icon="i-lucide-download"
|
||||
@click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</UTree>
|
||||
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||
@click="open = !open" />
|
||||
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle
|
||||
}}</span></p>
|
||||
}}</span></p>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-12">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -78,8 +78,6 @@ const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
await userStore.getUser()
|
||||
|
||||
const open = ref(true)
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -175,16 +173,17 @@ import LangSwitch from '@/components/inner/LangSwitch.vue'
|
||||
import ThemeSwitch from '@/components/inner/ThemeSwitch.vue'
|
||||
import type { LabelTrans, TopMenuItem } from '@/types'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { components } from 'reka-ui/constant'
|
||||
|
||||
import { watch } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const menu = ref<TopMenuItem[] | null>(null)
|
||||
|
||||
async function getTopMenu() {
|
||||
const Id =Number(route.params.user_id)
|
||||
|
||||
async function cmGetTopMenu() {
|
||||
try {
|
||||
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
|
||||
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
|
||||
|
||||
menu.value = items
|
||||
} catch (err) {
|
||||
@@ -192,9 +191,16 @@ async function getTopMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTopMenu()
|
||||
})
|
||||
console.log(route)
|
||||
watch(
|
||||
() => route.params.user_id,
|
||||
() => {
|
||||
if (route.params.user_id) {
|
||||
cmGetTopMenu()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const menuItems = computed(() => {
|
||||
if (!menu.value?.length) return []
|
||||
|
||||
86
bo/src/stores/customer/customer-product.ts
Normal file
86
bo/src/stores/customer/customer-product.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
|
||||
export interface Product {
|
||||
id: number
|
||||
image: string
|
||||
name: string
|
||||
productDetails?: string
|
||||
product_id: number
|
||||
is_favorite?: boolean
|
||||
}
|
||||
|
||||
export interface ProductResponse {
|
||||
items: Product[]
|
||||
items_count: number
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
message: string
|
||||
items: Product[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export const useCustomerProductStore = defineStore('customer-product', () => {
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const route = useRoute()
|
||||
const productsList = ref<Product[]>([])
|
||||
const total = ref(0)
|
||||
const perPage = ref(15)
|
||||
|
||||
async function fetchProductList() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(route.query).forEach(([key, value]) => {
|
||||
if (value) params.append(key, String(value))
|
||||
})
|
||||
|
||||
const url = `/api/v1/restricted/product/list?${params}`
|
||||
|
||||
|
||||
try {
|
||||
const response = await useFetchJson<ApiResponse>(url)
|
||||
productsList.value = response.items || []
|
||||
total.value = response.count || 0
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load products'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFavorite(product: Product) {
|
||||
const productId = product.product_id
|
||||
const isFavorite = product.is_favorite
|
||||
|
||||
const url = `/api/v1/restricted/product/favorite/${productId}`
|
||||
|
||||
try {
|
||||
if (!isFavorite) {
|
||||
await useFetchJson(url, { method: 'POST' })
|
||||
} else {
|
||||
await useFetchJson(url, { method: 'DELETE' })
|
||||
}
|
||||
product.is_favorite = !isFavorite
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to update favorite'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fetchProductList,
|
||||
toggleFavorite,
|
||||
productsList,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
perPage
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user