fix(user-app): repair mojibake Persian copy and drop phone-column shell

UTF-8 UI strings in product detail/cart were Latin-1-resaved; also remove the centred desktop phone frame so fixed chrome spans full width again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
saeed
2026-10-28 17:46:47 +03:30
co-authored by Cursor
parent 6fa31d3399
commit 6181940a6a
17 changed files with 115 additions and 200 deletions
+2 -17
View File
@@ -34,23 +34,8 @@
</style>
<title>%BRAND_TITLE%</title>
</head>
<body>
<!--
Boot splash: same phone column as #app so desktop never flashes a
full-bleed white sheet. Removed in main.ts once Vue mounts.
-->
<div
id="app-splash"
style="
position: fixed;
inset-block: 0;
inset-inline: max(0px, calc(50vw - 215px));
width: min(100vw, 430px);
margin-inline: auto;
background-color: var(--c-page, #fff);
z-index: 0;
"
></div>
<body style="background-color: #fff">
<div class="w-screen h-screen fixed" style="background-color: var(--c-page, #fff)"></div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
-17
View File
@@ -81,23 +81,6 @@ body::-webkit-scrollbar {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
// Centred phone column on desktop (width only). Do NOT put transform /
// filter / perspective here — that would make position:fixed bars
// (tabbar, PageBar) stick to #app and scroll away with the page.
max-width: var(--app-max-width);
margin-inline: auto;
min-height: 100dvh;
min-height: 100svh;
background: @page;
position: relative;
}
@media (min-width: 431px) {
#app {
box-shadow:
0 0 0 1px @hairline,
0 28px 64px -28px rgba(20, 22, 26, 0.32);
}
}
.slideDown-enter-active,
@@ -343,36 +343,8 @@ p {
margin-bottom: 10px;
}
// Phone column width. On desktop the shell centres at this size so the app
// does not stretch into a loose website layout. Tab bar already sat near 420.
:root {
--app-max-width: 430px;
}
html, body {
overscroll-behavior-y: contain;
background: @page;
}
@media (min-width: 431px) {
html, body {
// Side gutters behind the centred phone column (#app max-width).
background: @canvasSoft !important;
}
}
// Fixed overlays teleported to <body> (outside #app) — pin them to the same
// column. Below --app-max-width, max(0, …) collapses to full bleed.
.app-frame-x {
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
width: auto;
}
// Ant bottom drawers (order detail, status sheet) — same phone width, centred.
.ant-drawer-bottom > .ant-drawer-content-wrapper {
max-width: var(--app-max-width);
margin-inline: auto;
}
.overflow-x-auto {
@@ -57,8 +57,8 @@ async function leave() {
.aab {
position: fixed;
top: 0;
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
left: 0;
right: 0;
z-index: 2000;
display: flex;
align-items: center;
@@ -483,9 +483,7 @@ onBeforeUnmount(() => {
position: fixed;
top: 0;
bottom: 0;
// Stay in the phone column on desktop (same as #app / .app-frame-x).
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
inset: 0;
width: auto;
// Fixed bars inside SingleProduct (action bar, …) stay in this column.
transform: translateZ(0);
@@ -1,19 +1,19 @@
<!--
Product detail — redesigned to match the new home-screen design language
Product detail — redesigned to match the new home-screen design language
(components/home/*): LESS colour tokens, BEM class names, ink surfaces with a
wine accent, hairline borders and soft long shadows.
WHAT IS SHOWN IS REAL. Every figure comes from data the API genuinely
returns today:
- title, productCode, description, category, brand
- the selected variant's rash weight (گرم) and making charge (اجرت ٪)
- the selected variant's rash weight (گرم) and making charge (اجرت ٪)
- stock, from ProductVariant.qty / isPreOrder
No rial price is printed: a finished piece's price is the live gold rate ×
No rial price is printed: a finished piece's price is the live gold rate ×
weight plus wage, computed at order time and not stored on the product, so
printing one here would be invented. Karat and clasp type are likewise absent
from the schema and therefore absent from the page.
FIXED — a real bug, not a restyle. The previous version ran, at module scope:
FIXED — a real bug, not a restyle. The previous version ran, at module scope:
history.pushState(null, location.href)
window.onpopstate = () => history.go(1)
Because ant-design-vue's Drawer mounts its slot content eagerly, this fired
@@ -25,18 +25,18 @@
drawer and the /product/:id route both already rely on.
-->
<template>
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" v-if="flag && productDetail.productRef.value">
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" v-if="flag && productDetail.productRef.value">
<article class="pd">
<!-- Gallery -->
<div class="pd__gallery">
<button type="button" class="pd__round pd__round--back" aria-label="بازگشت" @click="close">
<button type="button" class="pd__round pd__round--back" aria-label="بازگشت" @click="close">
<Icon icon="vuesax-linear:arrow-right-1" size="19" />
</button>
<button
type="button"
class="pd__round pd__round--fav"
:class="{ 'is-on': favorites.isFavorite(id) }"
aria-label="علاقه‌مندی"
aria-label="علاقه‌مندی"
@click="toggleFav"
>
<Icon :icon="favorites.isFavorite(id) ? 'vuesax-linear:heart-fill' : 'vuesax-linear:heart'" size="19" />
@@ -59,7 +59,7 @@
type="button"
class="pd__dot"
:class="{ 'is-on': i === index }"
:aria-label="`تصویر ${i + 1}`"
:aria-label="`تصویر ${i + 1}`"
@click.stop="go(i)"
/>
</div>
@@ -77,7 +77,7 @@
<h1 class="pd__title">{{ title }}</h1>
<div class="pd__sub">
<span v-if="productCode" class="pd__code">{{ productCode }}</span>
<span v-if="categoryTitle" class="pd__dotSep">·</span>
<span v-if="categoryTitle" class="pd__dotSep">·</span>
<span v-if="categoryTitle">{{ categoryTitle }}</span>
</div>
</div>
@@ -87,31 +87,31 @@
<!-- Headline figures -->
<div class="pd__figures">
<div class="pd__figure">
<span class="pd__figureLabel">وزن تقریبی</span>
<span class="pd__figureLabel">وزن تقریبی</span>
<span class="pd__figureValue">
{{ min_gram }}
<span class="pd__figureUnit">گرم</span>
<span class="pd__figureUnit">گرم</span>
</span>
</div>
<div v-if="wage != null" class="pd__figure">
<span class="pd__figureLabel">اجرت</span>
<span class="pd__figureLabel">اجرت</span>
<span class="pd__figureValue">
{{ wageLabel }}
<span class="pd__figureUnit">درصد</span>
<span class="pd__figureUnit">درصد</span>
</span>
</div>
</div>
<!--
Compact variant chips: weight/title as pills. A tiny thumb only when
the variant has its OWN photo (not the product fallback) — otherwise
the variant has its OWN photo (not the product fallback) — otherwise
every chip repeated the same large image and ate the fold.
-->
<section v-if="variants && variants.length" class="pd__block pd__block--variants">
<div class="pd__blockHead">
<h2 class="pd__blockTitle">انتخاب تنوع</h2>
<h2 class="pd__blockTitle">انتخاب تنوع</h2>
<span v-if="selectedVariant" class="pd__variantsHint">
{{ Number(selectedVariant.rashWeightGrams) }} گرم
{{ Number(selectedVariant.rashWeightGrams) }} گرم
</span>
</div>
<div class="pd__variants">
@@ -137,7 +137,7 @@
/>
<span class="pd__variantText">
<span class="pd__variantTitle">{{ item.title }}</span>
<span class="pd__variantWeight">{{ Number(item.rashWeightGrams) }} گرم</span>
<span class="pd__variantWeight">{{ Number(item.rashWeightGrams) }} گرم</span>
</span>
<span class="pd__variantStock" :class="stockChipClass(item)">
{{ stockChipShort(item) }}
@@ -150,32 +150,32 @@
class="pd__variantsMore"
@click="showAllVariants = !showAllVariants"
>
{{ showAllVariants ? 'نمایش کمتر' : `+${variants.length - VARIANT_PREVIEW_COUNT} تنوع دیگر` }}
{{ showAllVariants ? 'نمایش کمتر' : `+${variants.length - VARIANT_PREVIEW_COUNT} تنوع دیگر` }}
</button>
</section>
<!-- Specs -->
<section class="pd__block">
<h2 class="pd__blockTitle">مشخصات</h2>
<h2 class="pd__blockTitle">مشخصات</h2>
<dl class="pd__specs">
<div class="pd__spec">
<dt><Icon icon="vuesax-linear:weight" size="15" /> وزن</dt>
<dd>{{ min_gram }} گرم</dd>
<dt><Icon icon="vuesax-linear:weight" size="15" /> وزن</dt>
<dd>{{ min_gram }} گرم</dd>
</div>
<div v-if="wage != null" class="pd__spec">
<dt><Icon icon="vuesax-linear:percentage-square" size="15" /> اجرت</dt>
<dd>{{ wageLabel }}Ùª</dd>
<dt><Icon icon="vuesax-linear:percentage-square" size="15" /> اجرت</dt>
<dd>{{ wageLabel }}٪</dd>
</div>
<div v-if="selectedVariant" class="pd__spec">
<dt><Icon icon="vuesax-linear:diamonds" size="15" /> تنوع</dt>
<dt><Icon icon="vuesax-linear:diamonds" size="15" /> تنوع</dt>
<dd>{{ selectedVariant.title }}</dd>
</div>
<div v-if="categoryTitle" class="pd__spec">
<dt><Icon icon="vuesax-linear:category" size="15" /> دسته‌بندی</dt>
<dt><Icon icon="vuesax-linear:category" size="15" /> دسته‌بندی</dt>
<dd>{{ categoryTitle }}</dd>
</div>
<div v-if="brandTitle" class="pd__spec">
<dt><Icon icon="vuesax-linear:shop" size="15" /> برند</dt>
<dt><Icon icon="vuesax-linear:shop" size="15" /> برند</dt>
<dd>{{ brandTitle }}</dd>
</div>
</dl>
@@ -183,17 +183,17 @@
<!-- Description -->
<section v-if="body" class="pd__block">
<h2 class="pd__blockTitle">توضیحات</h2>
<h2 class="pd__blockTitle">توضیحات</h2>
<p class="pd__body" :class="{ 'is-clamped': !bodyOpen }">{{ body }}</p>
<button v-if="bodyIsLong" type="button" class="pd__more" @click="bodyOpen = !bodyOpen">
{{ bodyOpen ? 'بستن' : 'ادامه مطلب' }}
{{ bodyOpen ? 'بستن' : 'ادامه مطلب' }}
</button>
</section>
<!-- Trust -->
<div class="pd__trust">
<span><Icon icon="vuesax-linear:shield-tick" size="16" /> خرید امن</span>
<span><Icon icon="vuesax-linear:verify" size="16" /> اصالت کالا</span>
<span><Icon icon="vuesax-linear:shield-tick" size="16" /> خرید امن</span>
<span><Icon icon="vuesax-linear:verify" size="16" /> اصالت کالا</span>
</div>
</div>
@@ -203,7 +203,7 @@
<button
type="button"
class="pd__qtyBtn"
aria-label="افزایش"
aria-label="افزایش"
:disabled="!canIncrement"
@click="increment"
>
@@ -213,7 +213,7 @@
<button
type="button"
class="pd__qtyBtn"
:aria-label="cartQty === 1 ? 'حذف' : 'کاهش'"
:aria-label="cartQty === 1 ? 'حذف' : 'کاهش'"
@click="decrement"
>
<Icon :icon="cartQty === 1 ? 'vuesax-linear:trash' : 'vuesax-linear:minus-square'" size="16" />
@@ -227,11 +227,11 @@
:disabled="!is_exist"
@click="isUserLoggedIn() ? addToCart() : (loginNotify = true)"
>
{{ is_exist ? 'افزودن به سفارش' : 'ناموجود' }}
{{ is_exist ? 'افزودن به سفارش' : 'ناموجود' }}
<Icon v-if="is_exist" icon="vuesax-linear:arrow-left-2" size="17" />
</button>
<router-link v-else class="pd__cta pd__cta--ghost" :to="{ name: 'cart' }">
مشاهده سبد خرید
مشاهده سبد خرید
<Icon icon="vuesax-linear:arrow-left-2" size="17" />
</router-link>
</div>
@@ -243,7 +243,7 @@
<Teleport to="body">
<Transition name="pd-preview">
<div v-if="previewOpen" class="pd__preview" @click.self="closePreview">
<button type="button" class="pd__previewClose" aria-label="بستن" @click="closePreview">
<button type="button" class="pd__previewClose" aria-label="بستن" @click="closePreview">
<Icon icon="vuesax-linear:add" size="20" />
</button>
@@ -264,7 +264,7 @@
type="button"
class="pd__previewThumb"
:class="{ 'is-on': i === imageIndex }"
:aria-label="`تصویر ${i + 1}`"
:aria-label="`تصویر ${i + 1}`"
@click="imageIndex = i"
>
<img :src="s" :alt="`${title} ${i + 1}`" draggable="false" />
@@ -280,12 +280,12 @@
</template>
<template v-slot:title>
<div class="p-8 text-justify">
برای دسترسی به تمامی امکانات نرم افزار {{ brand.name }}، ابتدا گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.
برای دسترسی به تمامی امکانات نرم افزار {{ brand.name }}، ابتدا گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.
</div>
</template>
<template v-slot:button>
<div class="text-center font-bold" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center" @click="loginNotify = false">بیخیال</div>
<div class="text-center font-bold" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center" @click="loginNotify = false">بیخیال</div>
</template>
</DialogVant>
</Spinner>
@@ -309,7 +309,7 @@ import { isUserLoggedIn } from '../../utils/checkAccess'
import loginDialogImage from '../../assets/img/empty/dialogLogin.svg'
// `initial`: the card's own ProductSummary when opened from a list (see
// ProductMorph.vue) — lets the page render whole on its first frame.
// ProductMorph.vue) — lets the page render whole on its first frame.
const props = defineProps<{ id: string; initial?: ProductSummary | null }>()
const emit = defineEmits<{ (e: 'closeDrawer'): void }>()
@@ -362,7 +362,7 @@ const qty = computed(() => productAdapter.value.qty.value)
const isPreOrder = computed(() => productAdapter.value.isPreOrder.value)
const is_exist = computed(() => productAdapter.value.is_exist.value)
// ── Variant chips ───────────────────────────────────────────────
// ── Variant chips ───────────────────────────────────────────────
// Compact pills; extras behind a short toggle so weight lists don't dominate.
const VARIANT_PREVIEW_COUNT = 8
const showAllVariants = ref(false)
@@ -373,13 +373,13 @@ const visibleVariants = computed(() =>
const isOrderable = (v: ProductVariantSummary) => isVariantOrderable(v)
const stockChipLabel = (v: ProductVariantSummary) => {
if ((v.qty ?? 0) > 0) return 'موجود'
return v.isPreOrder ? 'پیش‌سفارش' : 'ناموجود'
if ((v.qty ?? 0) > 0) return 'موجود'
return v.isPreOrder ? 'پیش‌سفارش' : 'ناموجود'
}
/** One short glyph/word so the chip stays one line. */
const stockChipShort = (v: ProductVariantSummary) => {
if ((v.qty ?? 0) > 0) return 'موجود'
return v.isPreOrder ? 'پیش' : '—'
if ((v.qty ?? 0) > 0) return 'موجود'
return v.isPreOrder ? 'پیش' : '—'
}
const stockChipClass = (v: ProductVariantSummary) => {
if ((v.qty ?? 0) > 0) return 'is-in'
@@ -408,8 +408,8 @@ watch(selectedVariant, (v) => {
if (i > -1) imageIndex.value = i
})
// ── Full-screen preview ─────────────────────────────────────────
// Tapping the inline gallery (a plain tap, not a swipe — GallerySlider tells
// ── Full-screen preview ─────────────────────────────────────────
// Tapping the inline gallery (a plain tap, not a swipe — GallerySlider tells
// the two apart itself and only emits `tap` for the former) opens a
// full-screen viewer teleported to <body>, with its own GallerySlider kept in
// lockstep via the shared `imageIndex` v-model.
@@ -442,8 +442,8 @@ const wageLabel = computed(() => {
})
const stockLabel = computed(() => {
if (qty.value > 0) return `${qty.value} عدد موجود`
return isPreOrder.value ? 'پیش‌سفارش' : 'ناموجود'
if (qty.value > 0) return `${qty.value} عدد موجود`
return isPreOrder.value ? 'پیش‌سفارش' : 'ناموجود'
})
const stockClass = computed(() => {
if (qty.value > 0) return 'is-in'
@@ -465,7 +465,7 @@ const cartIndex = computed(() => cart.find_index(storeProduct.value))
const cartQty = computed(() => (cartIndex.value > -1 ? cart.items[cartIndex.value].qty : 0))
// Stock ceiling: a pre-order variant may exceed what is on hand, anything else
// stops at its qty. The store clamps too — this just keeps the button honest.
// stops at its qty. The store clamps too — this just keeps the button honest.
const cartMaxQty = computed(() => cart.maxQty(storeProduct.value))
const canIncrement = computed(() => cartMaxQty.value === null || cartQty.value < cartMaxQty.value)
const increment = () => {
@@ -485,7 +485,7 @@ const addToCart = () => {
// Previously the "added to cart" toast fired either way, telling the customer
// something had been added when nothing had.
if (!cart.addToCart(storeProduct.value)) return
toast.success('محصول با موفقیت به سبد خرید افزوده شد', { title: 'ثبت سفارش' })
toast.success('محصول با موفقیت به سبد خرید افزوده شد', { title: 'ثبت سفارش' })
}
const toggleFav = (e: Event) => {
@@ -497,8 +497,8 @@ onBeforeMount(async () => {
await productDetail.load()
})
// If the preview is left open when this instance goes away — the drawer is
// closed, or the routed page is navigated away from — the body scroll lock it
// If the preview is left open when this instance goes away — the drawer is
// closed, or the routed page is navigated away from — the body scroll lock it
// set would otherwise outlive it and leave the rest of the app unscrollable.
onBeforeUnmount(() => {
if (lockedBody) document.body.style.overflow = ''
@@ -515,12 +515,12 @@ onBeforeUnmount(() => {
min-height: 100vh;
min-height: 100dvh;
background: @canvas;
/* Room for the fixed action bar — keep in sync with .pd__bar height. */
/* Room for the fixed action bar — keep in sync with .pd__bar height. */
padding-bottom: 104px;
box-sizing: border-box;
}
/* ── Gallery ─────────────────────────────────────────────── */
/* ── Gallery ─────────────────────────────────────────────── */
.pd__gallery {
position: relative;
z-index: 0;
@@ -614,7 +614,7 @@ onBeforeUnmount(() => {
background: #fff;
}
/* ── Sheet ───────────────────────────────────────────────── */
/* ── Sheet ───────────────────────────────────────────────── */
.pd__sheet {
position: relative;
z-index: 1;
@@ -681,7 +681,7 @@ onBeforeUnmount(() => {
background: @hairlineSoft;
}
/* ── Figures ─────────────────────────────────────────────── */
/* ── Figures ─────────────────────────────────────────────── */
.pd__figures {
display: flex;
gap: 10px;
@@ -714,7 +714,7 @@ onBeforeUnmount(() => {
padding-inline-start: 3px;
}
/* ── Blocks ──────────────────────────────────────────────── */
/* ── Blocks ──────────────────────────────────────────────── */
.pd__block {
margin-top: 22px;
}
@@ -741,7 +741,7 @@ onBeforeUnmount(() => {
white-space: nowrap;
}
/* Compact wrapping pills — weight lists stay on-screen without card tiles. */
/* Compact wrapping pills — weight lists stay on-screen without card tiles. */
.pd__variants {
display: flex;
flex-wrap: wrap;
@@ -916,12 +916,11 @@ onBeforeUnmount(() => {
color: @inkFaint;
}
/* ── Action bar ──────────────────────────────────────────── */
/* ── Action bar ──────────────────────────────────────────── */
.pd__bar {
position: fixed;
bottom: 0;
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
inset-inline: 0;
z-index: 20;
display: flex;
align-items: center;
@@ -991,8 +990,8 @@ onBeforeUnmount(() => {
border: 1px solid @primary;
}
/* ── Full-screen preview ─────────────────────────────────── */
/* Teleported to <body>, so it sits outside .pd entirely — see the template
/* ── Full-screen preview ─────────────────────────────────── */
/* Teleported to <body>, so it sits outside .pd entirely — see the template
comment on why. z-index cleared well above ant-design-vue's own Drawer
layer (@zindex-modal) so the preview is unconditionally topmost regardless
of which Teleport happens to mount last. */
@@ -157,11 +157,7 @@ const ringFillStyle = computed(() => {
.app-ptr__overlay {
position: fixed;
// Stay in the phone column on desktop (same as #app / .app-frame-x).
top: 0;
bottom: 0;
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
inset: 0;
z-index: 1900;
display: grid;
place-items: center;
+2 -13
View File
@@ -1,7 +1,6 @@
<!-- Ported from legacy henza_pwa/src/components/ui/container.vue.
Width follows the phone column (#app / --app-max-width), not screen-md. -->
<!-- Ported from legacy henza_pwa/src/components/ui/container.vue -->
<template>
<main class="app-container relative no-scrollbar">
<main class="px-[16px] max-w-screen-md m-auto relative no-scrollbar">
<slot />
</main>
</template>
@@ -12,13 +11,3 @@ interface Props {
}
withDefaults(defineProps<Props>(), {})
</script>
<style scoped>
.app-container {
width: 100%;
max-width: var(--app-max-width);
margin-inline: auto;
padding-inline: 16px;
position: relative;
}
</style>
@@ -9,7 +9,7 @@
so it needs no measurement of the container width.
-->
<template>
<nav class="tabbar app-frame-x" v-show="nav.showNav">
<nav class="tabbar" v-show="nav.showNav">
<div class="tabbar__inner" :style="{ '--count': nav.visibleNavBarRoutes.length }">
<div
class="tabbar__indicator"
@@ -47,8 +47,8 @@ const indicatorOffset = computed(() => -(nav.activeLink - 1) * 100)
<style scoped lang="less">
.tabbar {
position: fixed;
// left/right from global .app-frame-x — stays viewport-fixed vertically,
// phone-column width on desktop.
left: 0;
right: 0;
bottom: 0;
z-index: 10;
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
+3 -2
View File
@@ -8,7 +8,7 @@
the bar's height, blur, hairline and icon buttons stay consistent app-wide.
-->
<template>
<header class="bar app-frame-x">
<header class="bar">
<div class="bar__inner">
<button v-if="back" type="button" class="bar__btn" aria-label="بازگشت" @click="onBack">
<Icon icon="vuesax-linear:arrow-right-1" size="20" />
@@ -80,7 +80,8 @@ function onBack() {
.bar {
position: fixed;
top: var(--aab-h, 0px);
// left/right from global .app-frame-x
left: 0;
right: 0;
z-index: 20;
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);
@@ -10,7 +10,7 @@
-->
<template>
<main
class="app-scroll-fetch flex flex-col flex-1 overflow-auto no-scrollbar"
class="flex flex-col flex-1 overflow-auto no-scrollbar w-[100vw] max-w-screen-md m-auto"
:style="{ paddingTop: padding || transition.headerHeight + 'px' }"
ref="scrollArea"
>
@@ -48,11 +48,3 @@ useInfiniteScroll(
{ distance: 50 },
)
</script>
<style scoped>
.app-scroll-fetch {
width: 100%;
max-width: var(--app-max-width);
margin-inline: auto;
}
</style>
@@ -24,9 +24,8 @@ const { toasts, dismiss } = useToast()
.toast-host {
position: fixed;
top: var(--aab-h, 0px);
left: max(0px, calc(50vw - (var(--app-max-width) / 2)));
right: max(0px, calc(50vw - (var(--app-max-width) / 2)));
width: auto;
left: 0;
right: 0;
z-index: 3000;
display: flex;
flex-direction: column;
+1 -1
View File
@@ -21,7 +21,7 @@
</transition>
</router-view>
<Navigation />
<div class="bg-primary top-[-2rem] fixed h-[2rem] z-10 app-frame-x" />
<div class="bg-primary top-[-2rem] fixed h-[2rem] w-full z-10" />
</section>
</template>
-2
View File
@@ -63,5 +63,3 @@ app.use(VueLazyLoad, {
});
app.component('container', Container);
app.mount('#app');
// Boot splash (index.html) matched the phone column; drop it once the app paints.
document.getElementById('app-splash')?.remove();
+28 -27
View File
@@ -2,8 +2,8 @@
Ported from legacy henza_pwa/src/views/cart.vue, restyled into the
redesigned design language (see components/home/* and HomeView).
EVERY REAL BEHAVIOUR IS CARRIED OVER UNCHANGED — only the chrome changes:
- "ثبت نهایی سفارش" still posts to the real POST /orders endpoint. Legacy's
EVERY REAL BEHAVIOUR IS CARRIED OVER UNCHANGED — only the chrome changes:
- "ثبت نهایی سفارش" still posts to the real POST /orders endpoint. Legacy's
own `/set-order` payload had two hardcoded placeholder numbers with the
original author's comment "how to get?!"; the real Orders API computes
gold weight server-side from productVariantId + wagePercent, so
@@ -19,7 +19,7 @@
computed at order time. The honest total the cart can show is total weight,
which is what it shows.
DROPPED: the old unscoped `.rtl { direction: ltr }` rule, which was dead —
DROPPED: the old unscoped `.rtl { direction: ltr }` rule, which was dead —
the global `.rtl` in style.less carries !important, so a non-!important rule
could never override it. It was previously kept "for fidelity" while the
file was a faithful port; now that the file is deliberately redesigned,
@@ -27,9 +27,9 @@
-->
<template>
<div class="cart">
<PageBar title="سبد خرید" back-to="home" />
<PageBar title="سبد خرید" back-to="home" />
<Spinner :spinning="cartData.loadingPage" tip="درحال دریافت اطلاعات">
<Spinner :spinning="cartData.loadingPage" tip="درحال دریافت اطلاعات">
<container class="cart__body">
<!-- Step 1: the basket -->
<AppPullRefresh v-if="cartData.step === 1" v-model="refreshing" @refresh="onRefresh">
@@ -53,9 +53,9 @@
<section v-else class="cart__empty">
<Icon icon="vuesax-linear:bag-2" size="26" />
<p>سبد خرید شما خالی است.</p>
<p>سبد خرید شما خالی است.</p>
<button type="button" class="cart__emptyCta" @click="router.push({ name: 'home' })">
دیدن محصولات
دیدن محصولات
</button>
</section>
</AppPullRefresh>
@@ -65,32 +65,32 @@
<div class="cart__doneIcon" aria-hidden="true">
<Icon icon="vuesax-linear:tick-circle" size="30" />
</div>
<h2 class="cart__doneTitle">سفارش شما ثبت شد</h2>
<h2 class="cart__doneTitle">سفارش شما ثبت شد</h2>
<p class="cart__doneSub">
شماره سفارش
شماره سفارش
<b>{{ cartData.orderID }}</b>
</p>
<p class="cart__doneHint">برای پیگیری وضعیت، به فهرست سفارش‌ها مراجعه کنید.</p>
<button type="button" class="cart__doneCta" @click="goOrders">فهرست سفارش‌ها</button>
<p class="cart__doneHint">برای پیگیری وضعیت، به فهرست سفارش‌ها مراجعه کنید.</p>
<button type="button" class="cart__doneCta" @click="goOrders">فهرست سفارش‌ها</button>
</section>
</container>
</Spinner>
<!-- Sticky summary + submit -->
<div v-if="cart.items.length > 0 && cartData.step === 1" class="cart__bar app-frame-x">
<div v-if="cart.items.length > 0 && cartData.step === 1" class="cart__bar">
<div class="cart__barInner">
<div class="cart__total">
<span class="cart__totalLabel">جمع وزن</span>
<span class="cart__totalLabel">جمع وزن</span>
<span class="cart__totalValue">
{{ cart.total() }}
<i>گرم</i>
<i>گرم</i>
</span>
</div>
<button type="button" class="cart__submit" @click="cartData.notify = true">ثبت نهایی سفارش</button>
<button type="button" class="cart__submit" @click="cartData.notify = true">ثبت نهایی سفارش</button>
</div>
</div>
<!-- Per-item note — floating island, same language as the tab bar -->
<!-- Per-item note — floating island, same language as the tab bar -->
<Drawer
v-model:visible="cartData.showPopUp"
placement="bottom"
@@ -104,17 +104,17 @@
>
<div class="sheet">
<div class="sheet__handle" aria-hidden="true" />
<h3 class="sheet__title">توضیح برای {{ cartData.tempRow.product.title }}</h3>
<h3 class="sheet__title">توضیح برای {{ cartData.tempRow.product.title }}</h3>
<Form :model="formModel" name="noteForm" layout="vertical" autocomplete="off" @finish="setNote">
<FormItem has-feedback name="note" class="sheet__field">
<Textarea
v-model:value="formModel.note"
:rows="3"
placeholder="مثلاً سایز یا جزئیات سفارش"
placeholder="مثلاً سایز یا جزئیات سفارش"
class="sheet__input"
/>
</FormItem>
<button type="submit" class="sheet__cta">ثبت توضیحات</button>
<button type="submit" class="sheet__cta">ثبت توضیحات</button>
</Form>
</div>
</Drawer>
@@ -123,10 +123,10 @@
<template v-slot:img>
<img :src="dialogEmpty4" alt="" class="opacity-70" />
</template>
<template v-slot:title>آیا مطمئن به ثبت سفارش هستید ؟</template>
<template v-slot:title>آیا مطمئن به ثبت سفارش هستید ؟</template>
<template v-slot:button>
<div class="text-center text-[color:var(--c-primary)]" @click="submitOrder">بله</div>
<div class="text-center text-[color:var(--c-inkMuted)]" @click="cartData.notify = false">بی خیال</div>
<div class="text-center text-[color:var(--c-primary)]" @click="submitOrder">بله</div>
<div class="text-center text-[color:var(--c-inkMuted)]" @click="cartData.notify = false">بی خیال</div>
</template>
</DialogVant>
@@ -135,11 +135,11 @@
<img :src="loginDialogImage" alt="" />
</template>
<template v-slot:title>
<div class="p-8 text-justify">برای ثبت سفارش ابتدا باید وارد حساب کاربری خود شوید.</div>
<div class="p-8 text-justify">برای ثبت سفارش ابتدا باید وارد حساب کاربری خود شوید.</div>
</template>
<template v-slot:button>
<div class="text-center text-[color:var(--c-primary)]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
<div class="text-center text-[color:var(--c-primary)]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
</template>
</DialogVant>
</div>
@@ -355,7 +355,8 @@ onMounted(async () => {
.cart__bar {
position: fixed;
bottom: 0;
// left/right from global .app-frame-x
left: 0;
right: 0;
z-index: 5;
background: alpha(@canvas, 92%);
backdrop-filter: blur(14px);
@@ -463,7 +464,7 @@ onMounted(async () => {
}
</style>
<!-- Drawer teleports to body — island chrome must live outside scoped CSS. -->
<!-- Drawer teleports to body — island chrome must live outside scoped CSS. -->
<style lang="less">
.cart-note-island.ant-drawer-bottom .ant-drawer-content-wrapper {
left: 14px !important;
+3 -2
View File
@@ -19,7 +19,7 @@
-->
<template>
<div class="home">
<header class="home__bar app-frame-x">
<header class="home__bar">
<div class="home__barInner">
<router-link :to="{ name: 'cart' }" class="home__iconBtn" aria-label="سبد خرید">
<Icon icon="vuesax-linear:bag-2" size="21" />
@@ -312,7 +312,8 @@ onActivated(async () => {
.home__bar {
position: fixed;
top: var(--aab-h, 0px);
// left/right from global .app-frame-x
left: 0;
right: 0;
z-index: 20;
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);
@@ -31,7 +31,7 @@
-->
<template>
<div class="dash">
<header class="dash__bar app-frame-x">
<header class="dash__bar">
<div class="dash__barInner">
<span class="dash__barSpacer" aria-hidden="true" />
<h1 class="dash__barTitle">میز کار</h1>
@@ -400,7 +400,8 @@ onMounted(async () => {
.dash__bar {
position: fixed;
top: var(--aab-h, 0px);
// left/right from global .app-frame-x
left: 0;
right: 0;
z-index: 20;
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);