Files
hedena_gold/apps/user-app/src/components/product/ProductRow.vue
T
saeedandCursor feb5727c0e feat: order revision, rial wage from variant, product tags; drop user-app-v2
Admin order edit with category chips and timeline; customer checkout charges variant rialWage; product tags; retire the unused user-app-v2 tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 14:29:12 +03:30

231 lines
4.9 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Full-width horizontal product row for home «تازه‌ها».
Same fields and morph open as ProductCard — only the composition changes.
-->
<template>
<article ref="root" class="pr" data-product-origin @click="open">
<div class="pr__art">
<img v-if="image" v-lazy="image" :alt="title" />
<div v-else class="pr__artEmpty" aria-hidden="true">
<Icon icon="vuesax-linear:diamonds" size="22" />
</div>
<button
type="button"
class="pr__fav"
:class="{ 'is-on': favorites.isFavorite(id) }"
:aria-label="favorites.isFavorite(id) ? 'حذف از علاقه‌مندی' : 'افزودن به علاقه‌مندی'"
@click.stop="favorites.toggle(id)"
>
<Icon :icon="favorites.isFavorite(id) ? 'vuesax-linear:heart-fill' : 'vuesax-linear:heart'" size="15" />
</button>
<span v-if="!is_exist" class="pr__flag">ناموجود</span>
<span v-else-if="isPreOrder && qty === 0" class="pr__flag pr__flag--soft">پیش‌سفارش</span>
</div>
<div class="pr__body">
<h3 class="pr__title">{{ title }}</h3>
<div v-if="tags.length" class="pr__tags">
<span v-for="tag in tags" :key="tag" class="pr__tag">{{ tag }}</span>
</div>
<div class="pr__meta">
<span class="pr__weight">{{ min_gram }} گرم</span>
<span v-if="wage != null" class="pr__wage">اجرت {{ wageLabel }}٪</span>
</div>
<span class="pr__cta">مشاهده</span>
</div>
<Icon icon="vuesax-linear:arrow-left-1" size="18" class="pr__chev" aria-hidden="true" />
</article>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Icon from '../icon/Icon.vue'
import { useProduct } from '../../composables/useProduct'
import { openProduct } from '../../composables/useProductMorph'
import { useFavoritesStore } from '../../stores/favoritesStore'
import type { ProductSummary } from '../../api/client'
const props = defineProps<{ value: ProductSummary }>()
const favorites = useFavoritesStore()
const { image, id, title, min_gram, wage, is_exist, qty, isPreOrder } = useProduct(props.value)
const tags = computed(() => (props.value.tags ?? []).filter(Boolean))
const root = ref<HTMLElement | null>(null)
function open() {
openProduct(props.value, root.value)
}
const wageLabel = computed(() => {
const w = wage.value
if (w == null) return ''
return w % 1 === 0 ? String(w) : w.toFixed(2)
})
</script>
<style scoped lang="less">
.pr {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 8px 12px 8px 10px;
cursor: pointer;
background: @canvas;
border: 1px solid @hairline;
border-radius: 18px;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.pr:active {
transform: scale(0.99);
}
.pr__art {
position: relative;
flex: 0 0 88px;
width: 88px;
height: 88px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
overflow: hidden;
background: #f7f7f7;
}
.pr__art img {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
padding: 4px;
box-sizing: border-box;
object-fit: contain;
object-position: center;
display: block;
}
html.theme-dark .pr__art img {
background: @photoMat;
border-radius: 10px;
}
.pr__artEmpty {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: @inkFaint;
}
.pr__fav {
position: absolute;
top: 6px;
left: 6px;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 0;
cursor: pointer;
border-radius: 50%;
color: @inkMuted;
background: alpha(@canvas, 90%);
backdrop-filter: blur(6px);
}
.pr__fav.is-on {
color: @wine;
}
.pr__flag {
position: absolute;
top: 6px;
right: 6px;
padding: 3px 7px;
border-radius: 999px;
font-size: 9.5px;
font-weight: 700;
color: #fff;
background: @primary;
}
.pr__flag--soft {
background: @wineSoft;
}
.pr__body {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: 5px;
}
.pr__title {
margin: 0;
font-size: 13.5px;
font-weight: 700;
color: @ink;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pr__tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.pr__tag {
padding: 2px 7px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
color: @wine;
background: alpha(@wine, 10%);
line-height: 1.4;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pr__meta {
display: flex;
align-items: center;
gap: 7px;
}
.pr__weight {
font-size: 12px;
font-weight: 700;
color: @inkSoft;
}
.pr__wage {
font-size: 11px;
color: @inkFaint;
padding-inline-start: 7px;
border-inline-start: 1px solid @hairline;
}
.pr__cta {
margin-top: 2px;
font-size: 11.5px;
font-weight: 700;
color: @wine;
}
.pr__chev {
flex: 0 0 auto;
color: @inkFaint;
}
</style>