fix: store customer-product

This commit is contained in:
2026-04-14 08:55:53 +02:00
parent f1a2f4c0b2
commit b54645830f
11 changed files with 276 additions and 141 deletions

1
bo/components.d.ts vendored
View File

@@ -20,6 +20,7 @@ declare module 'vue' {
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.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'] LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default'] PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default'] PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']

View 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>

View File

@@ -141,7 +141,7 @@ async function fetchProductList() {
if (route.params.category_id) if (route.params.category_id)
params.append('category_id', String(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 { try {
const response = await useFetchJson<ApiResponse>(url) const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || [] productsList.value = response.items || []
@@ -161,7 +161,7 @@ function goToProduct(productId: number, linkRewrite: string) {
} }
localStorage.setItem('back_from_product', JSON.stringify(path)) localStorage.setItem('back_from_product', JSON.stringify(path))
router.push({ router.push({
name: 'customer-product-details', name: 'admin-product-details',
params: { product_id: productId, link_rewrite: linkRewrite } params: { product_id: productId, link_rewrite: linkRewrite }
}) })
} }

View File

@@ -1,7 +1,6 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="flex flex-col md:flex-row gap-10"> <div class="flex flex-col md:flex-row gap-10">
<CategoryMenu />
<div class="w-full flex flex-col items-center gap-4"> <div class="w-full flex flex-col items-center gap-4">
<UTable :data="usersList" :columns="columns" class="flex-1 w-full" <UTable :data="usersList" :columns="columns" class="flex-1 w-full"
:ui="{ root: 'max-w-100wv overflow-auto!' }" /> :ui="{ root: 'max-w-100wv overflow-auto!' }" />
@@ -194,7 +193,7 @@ const columns: TableColumn<Customer>[] = [
cell: ({ row }) => { cell: ({ row }) => {
const userId = row.original.user_id const userId = row.original.user_id
return h(UButton, { return h(UButton, {
color: 'primary', color: 'info',
size: 'sm', size: 'sm',
variant: 'soft', variant: 'soft',
onClick: () => { onClick: () => {

View File

@@ -222,7 +222,7 @@ const columns: TableColumn<Customer>[] = [
cell: ({ row }) => { cell: ({ row }) => {
const userId = row.original.user_id const userId = row.original.user_id
return h(UButton, { return h(UButton, {
color: 'primary', color: 'info',
size: 'sm', size: 'sm',
variant: 'soft', variant: 'soft',
onClick: () => { onClick: () => {

View File

@@ -3,15 +3,23 @@
<div class=""> <div class="">
<div class="flex md:flex-row flex-col justify-between gap-8 my-6"> <div class="flex md:flex-row flex-col justify-between gap-8 my-6">
<div class="flex-1"> <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] "> <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" <img :src="selectedColor?.image || productData.image" :alt="productData.name"
class="max-w-full h-auto object-contain" /> class="max-w-full h-auto object-contain" />
</div> </div>
</div> </div>
<div class="flex-1 flex flex-col gap-4"> <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"> <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ productData.name }} {{ productData.name }}
</h1> </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"> <p class="text-gray-600 dark:text-gray-300">
{{ productData.description }} {{ productData.description }}
</p> </p>
@@ -43,7 +51,8 @@
</div> </div>
<div class="flex gap-5 items-end"> <div class="flex gap-5 items-end">
<UInputNumber v-model="value" /> <UInputNumber v-model="value" />
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white"> <UButton color="primary"
class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
Add to Cart Add to Cart
</UButton> </UButton>
</div> </div>
@@ -78,6 +87,8 @@ import { ref, computed } from 'vue'
import ProductCustomization from './components/ProductCustomization.vue' import ProductCustomization from './components/ProductCustomization.vue'
import ProductVariants from './components/ProductVariants.vue' import ProductVariants from './components/ProductVariants.vue'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useFetchJson } from '@/composable/useFetchJson'
import { useRoute } from 'vue-router'
interface Color { interface Color {
id: string id: string
name: string name: string
@@ -97,6 +108,7 @@ interface ProductData {
howToUseText: string howToUseText: string
productDetailsText: string productDetailsText: string
documentsText: string documentsText: string
is_favorite: boolean
} }
const activeTab = ref('description') const activeTab = ref('description')
@@ -157,6 +169,24 @@ const activeTabContent = computed(() => {
if (productData.colors.length > 0) { if (productData.colors.length > 0) {
selectedColor.value = productData.colors[0] as Color 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> </script>
<style scoped> <style scoped>

View File

@@ -11,27 +11,30 @@
</template> </template>
</UNavigationMenu> --> </UNavigationMenu> -->
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1> <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> <span class="text-gray-600 dark:text-gray-400">Loading products...</span>
</div> </div>
<div v-else-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded"> <div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ error }} {{ customerProductStore.error }}
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<div class="flex gap-2"> <div class="flex gap-2">
<CategoryMenu /> <CategoryMenu />
<UTable :data="productsList" :columns="columns" class="flex-1"> <UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }"> <template #expanded="{ row }">
<UTable :data="productsList.slice(0, 3)" :columns="columnsChild" :ui="{ <UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild"
:ui="{
thead: 'hidden' thead: 'hidden'
}" /> }" />
</template> </template>
</UTable> </UTable>
</div> </div>
<div class="flex justify-center items-center py-8"> <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>
<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 No products found
</div> </div>
</div> </div>
@@ -42,20 +45,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue' import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/CategoryMenu.vue' 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 router = useRouter()
const route = useRoute() 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>>({ const filters = computed<Record<string, string>>({
get: () => { get: () => {
const q = { ...route.query } const q = { ...route.query }
@@ -157,36 +143,16 @@ const updateFilter = debounce((columnId: string, val: string) => {
filters.value = newFilters filters.value = newFilters
}, 400) }, 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) { function goToProduct(productId: number) {
router.push({ router.push({
name: 'product-detail', name: 'customer-product-details',
params: { id: productId } params: { product_id: productId }
}) })
} }
const selectedCount = ref({ const selectedCount = ref({
product_id: null, product_id: null,
count: 0 count: 0
@@ -205,7 +171,7 @@ const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
const columns: TableColumn<Payment>[] = [ const columns: TableColumn<Product>[] = [
{ {
id: 'expand', id: 'expand',
cell: ({ row }) => cell: ({ row }) =>
@@ -351,6 +317,35 @@ const columns: TableColumn<Payment>[] = [
variant: 'solid' variant: 'solid'
}, 'Add to cart') }, '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' variant: 'solid'
}, 'Add to cart') }, '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( watch(
() => route.query, () => route.query,
() => { () => {
fetchProductList() customerProductStore.fetchProductList()
}, },
{ immediate: true } { immediate: true }
) )

View File

@@ -1,37 +1,29 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="p-4"> <div class="p-4">
<div v-if="loading" class="flex justify-center py-8"> <div v-if="loading" class="flex justify-center py-8">
<ULoader /> <ULoader />
</div> </div>
<div v-else-if="error" class="text-red-500"> <div v-else-if="error" class="text-red-500">
{{ error }} {{ error }}
</div> </div>
<UTree v-if="showTree" :items="treeItems" v-model:expanded="expandedFolders" :key="treeKey" @toggle="onToggle" :get-key="item => item.value"> <UTree v-if="showTree" :items="treeItems" v-model:expanded="expandedFolders" :key="treeKey" @toggle="onToggle" :get-key="item => item.value">
<template #item-wrapper="{ item }"> <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"> <div class="flex items-center gap-1">
<UIcon :name="item.icon" :size="30" /> <UIcon :name="item.icon" :size="30" />
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<span class="text-[15px] font-medium"> <span class="text-[15px] font-medium">
{{ item.label }} {{ item.label }}
</span> </span>
<UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral" <UButton v-if="!item.isFolder && item.fileName" size="xxs" color="neutral"
variant="outline" icon="i-lucide-download" variant="outline" icon="i-lucide-download"
@click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" /> @click.stop="downloadFile(item.path, item.fileName)" :ui="{ base: 'ring-0!' }" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</UTree> </UTree>
</div> </div>
</component> </component>
</template> </template>

View File

@@ -78,8 +78,6 @@ const pageTitle = computed(() => route.meta.name ?? 'Default Page')
const authStore = useAuthStore() const authStore = useAuthStore()
const userStore = useUserStore() const userStore = useUserStore()
await userStore.getUser()
const open = ref(true) const open = ref(true)
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -175,16 +173,17 @@ import LangSwitch from '@/components/inner/LangSwitch.vue'
import ThemeSwitch from '@/components/inner/ThemeSwitch.vue' import ThemeSwitch from '@/components/inner/ThemeSwitch.vue'
import type { LabelTrans, TopMenuItem } from '@/types' import type { LabelTrans, TopMenuItem } from '@/types'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { components } from 'reka-ui/constant' import { watch } from 'vue'
const router = useRouter() const router = useRouter()
const menu = ref<TopMenuItem[] | null>(null) const menu = ref<TopMenuItem[] | null>(null)
async function getTopMenu() { const Id =Number(route.params.user_id)
async function cmGetTopMenu() {
try { 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 menu.value = items
} catch (err) { } catch (err) {
@@ -192,9 +191,16 @@ async function getTopMenu() {
} }
} }
onMounted(() => { console.log(route)
getTopMenu() watch(
}) () => route.params.user_id,
() => {
if (route.params.user_id) {
cmGetTopMenu()
}
},
{ immediate: true }
)
const menuItems = computed(() => { const menuItems = computed(() => {
if (!menu.value?.length) return [] if (!menu.value?.length) return []

View 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
}
})