Files
tehran_taj/apps/user-app-v2/src/components/product/OrderItemCard.vue
T

239 lines
5.7 KiB
Vue

<!--
One line of an order's contents, restyled into the redesigned design
language (see components/home/* and HomeView). Ported originally from
legacy henza_pwa/src/components/product/orderItem.vue.
The product/variant title and photo come WITH the order (the server includes
each item's variant and product). They used to be looked up in the browser
against the whole catalogue, which downloaded ~16k products to label a few
rows and left the row blank for a product that had since left the storefront.
DROPPED the "توضیحات: -" row the previous version rendered: our OrderItem
schema has no note field (legacy's per-item note was a real column there;
ours isn't), so it could only ever print a dash. An always-empty row is
noise, not information.
-->
<template>
<article class="oi">
<button
v-if="lookup?.imageUrl"
type="button"
class="oi__art"
aria-label="بزرگ‌نمایی تصویر"
@click="previewOpen = true"
>
<img v-lazy="imageUrl(lookup.imageUrl, 200)" :alt="lookup?.productTitle ?? ''" />
</button>
<div v-else class="oi__art oi__art--empty" aria-hidden="true">
<Icon icon="vuesax-linear:diamonds" size="18" />
</div>
<div class="oi__main">
<div class="oi__head">
<h4 class="oi__title">{{ lookup?.productTitle ?? '—' }}</h4>
<span v-if="lookup?.variantTitle" class="oi__variant">{{ lookup.variantTitle }}</span>
</div>
<div class="oi__figures">
<span class="oi__figure">
{{ value.quantity }}
<i>عدد</i>
</span>
<span class="oi__figure">
{{ value.computedWeightGrams }}
<i>گرم</i>
</span>
<span class="oi__figure">
{{ value.wagePercentSnapshot }}
<i>٪ اجرت</i>
</span>
</div>
</div>
<Teleport to="body">
<Transition name="oi-preview">
<div
v-if="previewOpen && lookup?.imageUrl"
class="oi__preview"
role="dialog"
aria-modal="true"
aria-label="پیش‌نمایش تصویر"
@click.self="previewOpen = false"
>
<button type="button" class="oi__previewClose" aria-label="بستن" @click="previewOpen = false">
<Icon icon="vuesax-linear:close-circle" size="22" />
</button>
<img class="oi__previewImg" :src="imageUrl(lookup.imageUrl, 1600)" :alt="lookup.productTitle ?? ''" />
</div>
</Transition>
</Teleport>
</article>
</template>
<script setup lang="ts">
import { computed, ref, watch, onBeforeUnmount } from 'vue'
import Icon from '../icon/Icon.vue'
import { orderItemLookup } from '../../composables/useOrderItemProduct'
import type { OrderItemDetail } from '../../api/client'
import { imageUrl } from '../../utils/image'
const props = defineProps<{ value: OrderItemDetail }>()
// The server sends these with the order, so there is nothing to fetch and
// nothing to wait for — the row renders complete on its first frame instead of
// filling in after a catalogue download.
const lookup = computed(() => orderItemLookup(props.value))
const previewOpen = ref(false)
watch(previewOpen, (open) => {
document.body.style.overflow = open ? 'hidden' : ''
})
onBeforeUnmount(() => {
if (previewOpen.value) document.body.style.overflow = ''
})
</script>
<style scoped lang="less">
.oi {
display: flex;
gap: 11px;
padding: 11px;
background: @canvas;
border: 1px solid @hairline;
border-radius: 16px;
}
.oi__art {
flex: 0 0 62px;
width: 62px;
height: 62px;
padding: 0;
border: 0;
border-radius: 12px;
overflow: hidden;
background: @hairlineSoft;
cursor: zoom-in;
appearance: none;
}
.oi__art--empty {
display: grid;
place-items: center;
color: @inkFaint;
cursor: default;
}
.oi__art img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
// Dark mode: product photos are shot on white, so they sit on a light mat
// instead of showing as white boxes on a dark card (@photoMat — transparent,
// i.e. no change, in light mode).
html.theme-dark .oi__art img {
background: @photoMat;
border-radius: 12px;
}
.oi__main {
flex: 1;
min-width: 0;
}
.oi__head {
display: flex;
align-items: baseline;
gap: 7px;
min-width: 0;
}
.oi__title {
margin: 0;
font-size: 12.5px;
/* yekan_bakh has no 600 face — 600 silently maps to bold (700). */
font-weight: 500;
color: @ink;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oi__variant {
flex: 0 0 auto;
font-size: 10.5px;
color: @inkFaint;
}
.oi__figures {
display: flex;
flex-wrap: wrap;
gap: 14px;
padding-top: 9px;
}
.oi__figure {
font-size: 12px;
font-weight: 500;
color: @inkSoft;
}
.oi__figure i {
font-style: normal;
font-size: 10px;
font-weight: 400;
color: @inkFaint;
padding-inline-start: 2px;
}
</style>
<style lang="less">
/* Teleported to body — unscoped so the overlay styles apply. */
.oi__preview {
position: fixed;
inset: 0;
z-index: 2100;
display: grid;
place-items: center;
padding: 28px 18px;
background: rgba(8, 8, 10, 0.88);
backdrop-filter: blur(6px);
}
.oi__previewClose {
position: absolute;
top: 14px;
inset-inline-end: 14px;
width: 40px;
height: 40px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
color: #fff;
display: grid;
place-items: center;
cursor: pointer;
}
.oi__previewImg {
max-width: min(920px, 100%);
max-height: min(80vh, 100%);
object-fit: contain;
border-radius: 14px;
background: #fff;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);
}
.oi-preview-enter-active,
.oi-preview-leave-active {
transition: opacity 0.18s ease;
}
.oi-preview-enter-from,
.oi-preview-leave-to {
opacity: 0;
}
</style>