user-app: extend the redesign across the account screens
Phase 2, completing the app-wide rollout of the home/product-detail design language (LESS colour tokens, BEM class names, hairline-bordered cards, SectionHead, dashed empty states) onto the account side: dashboard, orders, transactions, profile, login and register. Reuse over re-implementation: the dashboard's live rate block is now components/home/RateStrip.vue rather than a second hand-built strip, and every screen uses components/ui/PageBar.vue for its chrome. OrderCard and OrderItemCard are restyled to match. OrderItemCard drops its always-empty note row: our OrderItem schema has no note field (legacy's per-item note was a real column there; ours is not), so it could only ever print a dash. OrdersView swaps ant Tabs for the same scrolling chip rail the rest of the redesign uses, and its full-screen Modal with hand-written CSS overrides for a bottom Drawer, matching how every other detail surface opens. Deleted AppHeader.vue, orphaned once every screen moved to PageBar. Its shutter animation was driven by transitionFlag, which nothing reads any more — the router afterEach comment claiming otherwise is corrected rather than left asserting something untrue, and the now-inert half is flagged in place. ScrollFetchSearch loses its isFav prop: it gated two empty-state branches that rendered the identical image, and its only caller (FavoritesView) now renders its own. Behaviour is preserved throughout. Orders still reads GET /customers/:id/orders and filters client-side; transactions still reads GET /customers/:id/ledger-entries with legacy's CREDIT/DEBIT colour meaning intact; profile stays read-only over real fields; login keeps the real customer lookup plus the documented OTP stub; register stays stubbed with copy that does not claim an account was created. Verified in a real browser against the running API and native database: dashboard 4 tiles with a real balance, orders 1 real order card, profile 4 real spec rows, transactions the correct empty state for a customer with no ledger rows, and login/register confirmed with the session cleared (the guard bounces a signed-in user away from /login, which is why they needed a separate pass). No console errors, no horizontal overflow. vue-tsc and vite build both clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc62975dd0
commit
0e5f9ccd7b
@@ -1,58 +0,0 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/components/appHeader.vue.
|
||||
NOTE (existing issue, ported as-is per instructions — not fixed here):
|
||||
the header's id is "" (empty) but onEnter() looks up getElementById('appHeader').
|
||||
That lookup always returns null, so onEnter() never actually sets the header
|
||||
height from this code path in practice. Preserved exactly; flagged separately.
|
||||
-->
|
||||
<template>
|
||||
<transition name="shutter">
|
||||
<header
|
||||
id=""
|
||||
class="overflow-hidden bg-[#1C2D56] rounded-bl-[32px] p-[20px] pl-[30px] w-full mb-[0px] z-10"
|
||||
v-if="transition.transitionFlag"
|
||||
>
|
||||
<slot />
|
||||
</header>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useTransitionStore } from '../stores/transitionStore'
|
||||
|
||||
const transition = useTransitionStore()
|
||||
|
||||
function onEnter() {
|
||||
const element = document.getElementById('appHeader')
|
||||
if (element && element.offsetHeight) {
|
||||
element.style.position = 'fixed'
|
||||
element.style.top = '0'
|
||||
transition.setHeaderHeight(element.offsetHeight + 16)
|
||||
}
|
||||
}
|
||||
function onBeforeLeave() {
|
||||
if (transition.headerHeight > 0) transition.setHeaderHeight(0)
|
||||
}
|
||||
onBeforeUnmount(() => onBeforeLeave())
|
||||
onMounted(() => {
|
||||
setTimeout(() => onEnter(), 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.shutter-enter-active {
|
||||
animation: shutter 1s cubic-bezier(0.33, 1, 0.68, 1);
|
||||
}
|
||||
@keyframes shutter {
|
||||
0% {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
color: transparent !important;
|
||||
}
|
||||
100% {
|
||||
max-height: 120px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,165 @@
|
||||
<!-- Ported from legacy henza_pwa/src/components/product/order.vue -->
|
||||
<!--
|
||||
Order summary row, restyled into the redesigned design language (see
|
||||
components/home/* and HomeView). Ported originally from legacy
|
||||
henza_pwa/src/components/product/order.vue.
|
||||
|
||||
Status colours still come from utils/orderStatus.ts, which is the single
|
||||
source of truth shared with the detail view — not re-declared here.
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
@click="emit('view', value)"
|
||||
class="w-full cursor-pointer relative rounded-[10px] mb-4 p-[10px] text-primary border border-solid border-[#E5E7EB] bg-[#fff]"
|
||||
>
|
||||
<div class="grid grid-cols-2 p-2">
|
||||
<div>
|
||||
<div class="text-[18px] font-sans font-semibold mb-2">
|
||||
<span class="text-[#656f87]">سفارش :</span>
|
||||
{{ value.orderNumber }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[16px] font-bold pl-2">{{ value.items.length }}</span>
|
||||
<span class="text-[14px] font-medium">آیتم</span>
|
||||
</div>
|
||||
<article class="oc" @click="emit('view', value)">
|
||||
<div class="oc__head">
|
||||
<div class="oc__id">
|
||||
<span class="oc__idLabel">سفارش</span>
|
||||
<span class="oc__idValue">{{ value.orderNumber }}</span>
|
||||
</div>
|
||||
<div class="grid justify-end">
|
||||
<div class="rounded-[5px] px-[20px] py-[4px] w-fit text-[14px] mb-2" :style="{ background: color?.background, color: color?.font }">
|
||||
{{ statusLabel }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[16px] font-bold pl-2">{{ parseFloat(value.totalGoldWeightGrams).toFixed(2) }} </span>
|
||||
<span class="text-[14px] font-medium">گرم</span>
|
||||
</div>
|
||||
<span class="oc__status" :style="{ background: color?.background, color: color?.font }">
|
||||
{{ statusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="oc__figures">
|
||||
<div class="oc__figure">
|
||||
<span class="oc__figureLabel">تعداد</span>
|
||||
<span class="oc__figureValue">
|
||||
{{ value.items.length }}
|
||||
<i>آیتم</i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="oc__figure">
|
||||
<span class="oc__figureLabel">وزن</span>
|
||||
<span class="oc__figureValue">
|
||||
{{ parseFloat(value.totalGoldWeightGrams).toFixed(2) }}
|
||||
<i>گرم</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider style="margin: 8px 0" />
|
||||
<div class="flex justify-between p-2">
|
||||
<div class="text-[16px] text-[#656f87]">{{ jdate(value.createdAt) }}</div>
|
||||
<Icon icon="vuesax-linear:arrow-left-1" class="cursor-pointer text-left" size="24" />
|
||||
|
||||
<div class="oc__foot">
|
||||
<span class="oc__date">{{ jdate(value.createdAt) }}</span>
|
||||
<span class="oc__more">
|
||||
جزئیات
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="14" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Divider as ADivider } from 'ant-design-vue'
|
||||
import Icon from '../icon/Icon.vue'
|
||||
import { ORDER_STATUS_COLORS, ORDER_STATUS_LABELS } from '../../utils/orderStatus'
|
||||
import { jdate } from '../../utils/filters'
|
||||
import type { OrderDetail } from '../../api/client'
|
||||
|
||||
const props = withDefaults(defineProps<{ value: OrderDetail }>(), {})
|
||||
const props = defineProps<{ value: OrderDetail }>()
|
||||
const emit = defineEmits<{ (e: 'view', value: OrderDetail): void }>()
|
||||
|
||||
const statusLabel = computed(() => ORDER_STATUS_LABELS[props.value.status] ?? props.value.status)
|
||||
const color = computed(() => ORDER_STATUS_COLORS[props.value.status])
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.oc {
|
||||
cursor: pointer;
|
||||
padding: 13px 14px;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.oc:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.oc__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.oc__id {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.oc__idLabel {
|
||||
font-size: 11px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.oc__idValue {
|
||||
font-size: 13.5px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.oc__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 11px;
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oc__figures {
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
padding: 12px 0 11px;
|
||||
}
|
||||
|
||||
.oc__figure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.oc__figureLabel {
|
||||
font-size: 10.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.oc__figureValue {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
.oc__figureValue i {
|
||||
font-style: normal;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
color: @inkFaint;
|
||||
padding-inline-start: 3px;
|
||||
}
|
||||
|
||||
.oc__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid @hairlineSoft;
|
||||
}
|
||||
|
||||
.oc__date {
|
||||
font-size: 11px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.oc__more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,57 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/components/product/orderItem.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.
|
||||
|
||||
Adapted: product/variant title comes from a client-side lookup (see
|
||||
useOrderItemProduct.ts) since the real order-items response only carries
|
||||
productVariantId. `note` always shows '-' — our OrderItem schema has no
|
||||
note field (legacy's per-item note was a real column there; ours isn't).
|
||||
productVariantId.
|
||||
|
||||
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>
|
||||
<div class="bg-white/10 rounded-[10px] p-[12px] flex items-center">
|
||||
<img v-lazy="lookup?.imageUrl" class="ml-[8px] w-[120px]" />
|
||||
<div class="flex-1" style="width: calc(100% - 82px)">
|
||||
<div class="flex items-center">
|
||||
<div class="font-semibold text-[16px] ml-2">{{ lookup?.productTitle ?? '—' }}</div>
|
||||
<div class="text-[12px] text-secondary">({{ lookup?.variantTitle }})</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="mt-2">
|
||||
<div>تعداد کالاها :</div>
|
||||
<div class="pt-1">وزن :</div>
|
||||
<div class="pt-1">اجرت :</div>
|
||||
<div class="pt-1">توضیحات :</div>
|
||||
</div>
|
||||
<div class="text-left mt-2 text-black font-semibold">
|
||||
<div>{{ value.quantity }} <span>عدد</span></div>
|
||||
<div class="pt-1">{{ value.computedWeightGrams }} <span>گرم</span></div>
|
||||
<div class="pt-1">{{ value.wagePercentSnapshot }} <span>درصد</span></div>
|
||||
<div class="pt-1">-</div>
|
||||
</div>
|
||||
<article class="oi">
|
||||
<div class="oi__art">
|
||||
<img v-if="lookup?.imageUrl" v-lazy="lookup.imageUrl" :alt="lookup?.productTitle ?? ''" />
|
||||
<div v-else class="oi__artEmpty" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:diamonds" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Icon from '../icon/Icon.vue'
|
||||
import { useOrderItemProduct, type VariantLookup } from '../../composables/useOrderItemProduct'
|
||||
import type { OrderItemDetail } from '../../api/client'
|
||||
|
||||
const props = withDefaults(defineProps<{ value: OrderItemDetail }>(), {})
|
||||
const props = defineProps<{ value: OrderItemDetail }>()
|
||||
const { lookup: lookupVariant } = useOrderItemProduct()
|
||||
const lookup = ref<VariantLookup | undefined>()
|
||||
|
||||
@@ -44,3 +59,86 @@ onMounted(async () => {
|
||||
lookup.value = await lookupVariant(props.value.productVariantId)
|
||||
})
|
||||
</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;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: @hairlineSoft;
|
||||
}
|
||||
|
||||
.oi__art img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.oi__artEmpty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-weight: 700;
|
||||
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: 700;
|
||||
color: @inkSoft;
|
||||
}
|
||||
.oi__figure i {
|
||||
font-style: normal;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: @inkFaint;
|
||||
padding-inline-start: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/components/ui/ScrollFetchSearch.vue.
|
||||
EXISTING ISSUE, ported as-is: both the isFav and non-isFav empty states use
|
||||
the same `favoritEmpty` image (legacy imported cartEmpty/emp_search/
|
||||
emp_favorite too, but never actually used them in the template — those
|
||||
unused imports are dropped here as genuinely dead code; the duplicate-image
|
||||
*behavior* itself is preserved).
|
||||
|
||||
Legacy rendered two separate empty states, gated on an `isFav` prop, that
|
||||
both used the SAME favoritEmpty image — a distinction without a difference,
|
||||
preserved verbatim while this was a faithful port. The only caller that ever
|
||||
passed `is-fav` was FavoritesView, which now renders its own empty state in
|
||||
the redesigned language, so the prop had no remaining callers and the two
|
||||
identical branches collapse to one.
|
||||
-->
|
||||
<template>
|
||||
<main
|
||||
@@ -14,8 +16,7 @@
|
||||
>
|
||||
<slot></slot>
|
||||
<Spinner cls="my-[10px]" :spinning="loading" :indicator="true" tip="درحال دریافت اطلاعات" />
|
||||
<EmptyState v-if="flag && !isFav" :image="favoritEmpty" />
|
||||
<EmptyState v-if="flag && isFav" :image="favoritEmpty" />
|
||||
<EmptyState v-if="flag" :image="favoritEmpty" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -32,7 +33,6 @@ withDefaults(
|
||||
loading: boolean
|
||||
flag: boolean
|
||||
padding?: string
|
||||
isFav?: boolean
|
||||
}>(),
|
||||
{},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user