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']
|
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']
|
||||||
|
|||||||
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)
|
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 }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () => {
|
||||||
|
|||||||
@@ -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: () => {
|
||||||
|
|||||||
@@ -8,4 +8,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Management from '@/layouts/management.vue';
|
import Management from '@/layouts/management.vue';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,75 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="Default || 'div'">
|
<component :is="Default || 'div'">
|
||||||
<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
|
||||||
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px] ">
|
||||||
class="max-w-full h-auto object-contain" />
|
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||||
</div>
|
class="max-w-full h-auto object-contain" />
|
||||||
</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>
|
</div>
|
||||||
<div class="flex gap-2">
|
</div>
|
||||||
<span class="text-gray-500 dark:text-gray-400">Seat Height:</span>
|
<div class="flex-1 flex flex-col gap-4">
|
||||||
<p class="font-medium dark:text-white">{{ productData.seatHeight }}</p>
|
<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>
|
</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 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">
|
||||||
<div class="flex flex-col gap-3">
|
<span class="text-sm text-(--text-sky-light) dark:text-(--text-sky-dark) ">Colors:</span>
|
||||||
<span class="text-sm text-(--text-sky-light) dark:text-(--text-sky-dark) ">Colors:</span>
|
<div class="flex gap-2">
|
||||||
<div class="flex gap-2">
|
<button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color"
|
||||||
<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
|
||||||
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-(--accent-blue-light) ring-2 ring-blue-600 ring-offset-2'
|
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
|
||||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
|
:style="{ backgroundColor: color.hex }" :title="color.name" />
|
||||||
: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>
|
</div>
|
||||||
<div class="flex gap-5 items-end">
|
<ProductCustomization />
|
||||||
<UInputNumber v-model="value" />
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
<div class="mb-6 w-[100%] xl:w-[60%]">
|
||||||
Add to Cart
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-10 mb-8">
|
||||||
</UButton>
|
<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>
|
</div>
|
||||||
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
|
<ProductVariants />
|
||||||
</div>
|
</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>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
thead: 'hidden'
|
:ui="{
|
||||||
}" />
|
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 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||||
@click="open = !open" />
|
@click="open = !open" />
|
||||||
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle
|
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle
|
||||||
}}</span></p>
|
}}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:flex items-center gap-12">
|
<div class="hidden md:flex items-center gap-12">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -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 []
|
||||||
|
|||||||
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