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
|
||||
}>(),
|
||||
{},
|
||||
)
|
||||
|
||||
@@ -64,12 +64,18 @@ router.beforeEach((to) => {
|
||||
else nav.setPgaTransition(undefined);
|
||||
});
|
||||
|
||||
// Ported from legacy router/index.ts:273-286. This hook is what actually
|
||||
// drives two animations, and its absence was why neither ran:
|
||||
// 1. cycling transitionFlag false->true replays AppHeader's "shutter" enter
|
||||
// animation on every navigation;
|
||||
// 2. setActiveLink moves the bottom-nav GSAP notch + bubble to the current
|
||||
// tab (it is the ONLY caller of setActiveLink in the whole app).
|
||||
// Ported from legacy router/index.ts:273-286.
|
||||
//
|
||||
// setActiveLink moves the bottom-nav GSAP notch + bubble to the current tab,
|
||||
// and this hook is its ONLY caller in the whole app — that part is live.
|
||||
//
|
||||
// The transitionFlag cycling below used to replay AppHeader's "shutter" enter
|
||||
// animation. AppHeader was deleted when every screen moved to the redesigned
|
||||
// components/ui/PageBar.vue, and nothing reads transitionFlag any more, so
|
||||
// that half is currently inert. It is left in place rather than ripped out
|
||||
// because transitionStore is still the shared home for this kind of
|
||||
// page-transition state; flagged here so the next reader doesn't assume it
|
||||
// still drives a visible animation.
|
||||
router.afterEach((to) => {
|
||||
const nav = useNavStore();
|
||||
const transition = useTransitionStore();
|
||||
|
||||
@@ -1,110 +1,122 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/Dashboard.vue —
|
||||
the most heavily adapted view in this port so far. Legacy's original (820
|
||||
lines) is a multi-role workbench mixing plain-customer, sales-agent
|
||||
("reagent"), and "banker" (type=1, sub-account switching) views. Kept: the
|
||||
real balance card, the nav grid to Orders/Documents/Profile/Favorites, and
|
||||
the logout/login-nudge dialogs — all backed by real data.
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/Dashboard.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView). The live rate block now reuses components/home/RateStrip.vue
|
||||
rather than re-implementing the same strip a second time.
|
||||
|
||||
Legacy's original (820 lines) is a multi-role workbench mixing
|
||||
plain-customer, sales-agent ("reagent") and "banker" (type=1, sub-account
|
||||
switching) views. Kept: the real balance card, the nav grid to
|
||||
Orders/Documents/Profile/Favorites, and the logout/login-nudge dialogs —
|
||||
all backed by real data.
|
||||
|
||||
DROPPED as architecturally incompatible, not merely deferred: everything
|
||||
tied to legacy's `Customer.type`/multi-account model — "انتخاب مشتری"
|
||||
(toggleUser/hasActiveChild), "سفارش مشتریان" (ChildOrders), and
|
||||
customerChoice. In the redesigned schema (see the original migration
|
||||
audit), sales agents are AdminUser records with no customer-facing session
|
||||
at all — there is no "type" field on our Customer model, so there is
|
||||
nothing to adapt here; the underlying data model this UI assumed no longer
|
||||
exists.
|
||||
(toggleUser/hasActiveChild), "سفارش مشتریان" (ChildOrders) and
|
||||
customerChoice. Sales agents are AdminUser records with no customer-facing
|
||||
session at all, and there is no `type` field on our Customer model, so the
|
||||
data model this UI assumed no longer exists.
|
||||
|
||||
DROPPED as no backend exists: "سفارش ساخت" (custom/pre-order manufacturing)
|
||||
— PreOrder has a DB table but no controller anywhere in apps/api. Shown as
|
||||
a disabled tile rather than a broken link.
|
||||
— PreOrder has a DB table but no controller anywhere in apps/api.
|
||||
|
||||
REPLACED, not dropped: legacy's "گزارش حساب" van-action-sheet called out to
|
||||
the external Tahesab accounting service (hesabMe()) that the very first
|
||||
migration audit explicitly decided to retire in favor of this system's own
|
||||
internal ledger — this dashboard's balance card already shows that real,
|
||||
internal balance directly, so there's no separate popup needed.
|
||||
REPLACED, not dropped: legacy's "گزارش حساب" called out to the external
|
||||
Tahesab accounting service, which the first migration audit retired in
|
||||
favour of this system's own internal ledger — the balance card below shows
|
||||
that real internal position directly.
|
||||
|
||||
DROPPED: the multi-row world gold/currency price ticker (legacy's GET
|
||||
/price, ~14 rows). The new API has one single-karat quote endpoint
|
||||
(GET /pricing/latest) — shown as one line instead of fabricating rows that
|
||||
don't exist.
|
||||
DROPPED: the multi-row world gold/currency ticker (legacy's GET /price, ~14
|
||||
rows). The new API has one single-karat quote endpoint, shown as one strip
|
||||
rather than fabricating rows that don't exist.
|
||||
-->
|
||||
<template>
|
||||
<div class="fixed top-0 z-10 w-full">
|
||||
<AppHeader>
|
||||
<div class="flex justify-between items-center">
|
||||
<div></div>
|
||||
<h3 class="text-white bold mb-0 text-center w-full">میز کار</h3>
|
||||
<Icon v-if="session.customerId" icon="vuesax-linear:logout" @click="data.notify = true" class="cursor-pointer text-white absolute left-5" size="25" />
|
||||
<div class="dash">
|
||||
<header class="dash__bar">
|
||||
<div class="dash__barInner">
|
||||
<span class="dash__barSpacer" aria-hidden="true" />
|
||||
<h1 class="dash__barTitle">میز کار</h1>
|
||||
<button
|
||||
v-if="session.customerId"
|
||||
type="button"
|
||||
class="dash__barBtn"
|
||||
aria-label="خروج"
|
||||
@click="data.notify = true"
|
||||
>
|
||||
<Icon icon="vuesax-linear:logout" size="19" />
|
||||
</button>
|
||||
<span v-else class="dash__barSpacer" aria-hidden="true" />
|
||||
</div>
|
||||
</AppHeader>
|
||||
</div>
|
||||
<div class="h-16"></div>
|
||||
</header>
|
||||
|
||||
<container class="overflow-y-scroll pt-4">
|
||||
<div v-if="session.customerId">
|
||||
<div class="rounded-[10px] bg-[#F9F9F9] p-[16px] w-full mb-[16px] border-[#E5E7EB] border border-solid">
|
||||
<div class="grid grid-cols-5 items-center pb-2 mb-2 border-b-2 border-solid border-[#E5E7EB]">
|
||||
<h5 class="font-bold col-span-2 mb-0">{{ customer?.fullName }}</h5>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div>شماره تلفن همراه</div>
|
||||
<div>{{ session.mobile }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="rounded-[10px] bg-[#F9F9F9] p-[16px] w-full mb-[16px] border-[#E5E7EB] border border-solid">
|
||||
<div class="text-center font-bold">برای استفاده از امکانات نرم افزار ابتدا وارد شوید</div>
|
||||
<div class="flex w-full justify-center mt-2" @click="router.push({ name: 'login' })">
|
||||
<Button type="primary" class="w-[17rem]">ورود/ثبت نام</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[10px] bg-light p-[16px] w-full mb-[16px] bg-[#F9F9F9]" v-if="customer">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<div class="text-[12px] mb-2">{{ remainText }}</div>
|
||||
<div class="font-bold">{{ Math.abs(parseFloat(customer.goldDebtGrams)) }} گرم</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[12px] mb-2">بدهی ریالی</div>
|
||||
<div class="font-bold">{{ priceUnit(customer.fiatDebtRial) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-6 my-4">
|
||||
<div v-for="item in list" :key="item.id" class="grid col-span-1 cursor-pointer" @click="goToDetails(item.id)">
|
||||
<div class="border border-solid border-[#E5E7EB] bg-[#F9F9F9] rounded-[5px] flex justify-center items-center py-4" :class="{ 'opacity-40': item.disabled }">
|
||||
<Icon :icon="item.icon" />
|
||||
</div>
|
||||
<div class="text-center text-[14px] font-medium pt-2">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[10px] text-secondary col-span-6 mb-24" v-if="goldPrice">
|
||||
<div class="flex items-center justify-between bg-[#1C2D56] text-[#ffffff] p-3 rounded">
|
||||
<div class="font-bold text-[16px]">نرخ طلای ۱۸ عیار</div>
|
||||
<div>{{ jdate(goldPrice.quotedAt) }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 mt-2">
|
||||
<div class="bg-[#F9F9F9] rounded flex justify-between items-center p-2">
|
||||
<div class="flex items-center">
|
||||
<div class="bg-white rounded w-9 h-9 flex justify-center items-center">
|
||||
<Icon class="text-primary" icon="vuesax-linear:number-18" />
|
||||
<container class="dash__body">
|
||||
<!-- Signed in: real position from GET /customers/:id -->
|
||||
<template v-if="customer">
|
||||
<section class="dash__balance">
|
||||
<div class="dash__balanceHead">
|
||||
<div>
|
||||
<div class="dash__eyebrow">{{ remainText }}</div>
|
||||
<div class="dash__figure">
|
||||
{{ grams }}
|
||||
<span class="dash__unit">گرم</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[#1C2D56] text-[15px] font-semibold mr-3">هر گرم طلای ۱۸ عیار</div>
|
||||
<span class="dash__seal" :class="isDebtor ? 'is-debtor' : 'is-creditor'">
|
||||
{{ isDebtor ? 'بدهکار' : 'بستانکار' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[#1C2D56] text-[18px] font-semibold flex items-center">
|
||||
{{ price(goldPrice.pricePerGramRial) }}
|
||||
<div class="text-xs text-[#8D6A46] pr-0.5 font-medium">ریال</div>
|
||||
|
||||
<div class="dash__balanceRow">
|
||||
<span class="dash__rowLabel">بدهی ریالی</span>
|
||||
<span class="dash__rowValue">{{ price(customer.fiatDebtRial) }} <i>ریال</i></span>
|
||||
</div>
|
||||
|
||||
<div class="dash__identity">
|
||||
<span class="dash__name">{{ customer.fullName }}</span>
|
||||
<span class="dash__mobile">{{ session.mobile }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- Signed out: a quiet invitation, no fabricated numbers -->
|
||||
<section v-else class="dash__invite">
|
||||
<div class="dash__inviteIcon" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:security-user" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dash__inviteCopy">
|
||||
<h3>حساب گرمی شما</h3>
|
||||
<p>برای دیدن موجودی، سفارشها و اسناد خود وارد شوید.</p>
|
||||
</div>
|
||||
<button type="button" class="dash__inviteCta" @click="router.push({ name: 'login' })">
|
||||
ورود / ثبتنام
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Nav grid -->
|
||||
<section class="dash__section">
|
||||
<SectionHead title="دسترسیها" subtitle="بخشهای حساب شما" />
|
||||
<div class="dash__grid">
|
||||
<button
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="dash__tile"
|
||||
:class="{ 'is-disabled': item.disabled }"
|
||||
@click="goToDetails(item.id)"
|
||||
>
|
||||
<span class="dash__tileIcon" aria-hidden="true">
|
||||
<Icon :icon="item.icon" size="20" />
|
||||
</span>
|
||||
<span class="dash__tileTitle">{{ item.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live 18k rate — same strip the home screen uses -->
|
||||
<section class="dash__section">
|
||||
<SectionHead title="نرخ روز" subtitle="آخرین نرخ اعلامشده" />
|
||||
<RateStrip :quote="goldPrice" />
|
||||
</section>
|
||||
</container>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.notify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
@@ -116,32 +128,35 @@
|
||||
<div class="text-center text-[#6b7280]" @click="data.notify = false">بی خیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.loginNotify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="loginDialogImage" alt="" class="opacity-80" />
|
||||
</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-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
|
||||
<div class="text-center text-danger" @click="data.loginNotify = false">بیخیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
</container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import RateStrip from '../../components/home/RateStrip.vue'
|
||||
import DialogVant from '../../components/DialogVant.vue'
|
||||
import exitImage from '../../assets/img/empty/dialogEmpty5.svg'
|
||||
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
|
||||
import { price, priceUnit } from '../../utils/filters'
|
||||
import { jdate } from '../../utils/filters'
|
||||
import { price } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
|
||||
|
||||
@@ -151,7 +166,14 @@ const session = useSessionStore()
|
||||
const customer = ref<CustomerDetail | null>(null)
|
||||
const goldPrice = ref<GoldPriceQuote | null>(null)
|
||||
|
||||
const remainText = computed(() => (customer.value && parseFloat(customer.value.goldDebtGrams) > 0 ? 'بدهکار گرمی' : 'بستانکار گرمی'))
|
||||
// The ledger stores the gram position as a signed figure — positive means the
|
||||
// customer owes gold. Labelled the same way AccountStrip does on the home
|
||||
// screen so the two never disagree.
|
||||
const isDebtor = computed(() => (customer.value ? parseFloat(customer.value.goldDebtGrams) > 0 : false))
|
||||
const remainText = computed(() => (isDebtor.value ? 'بدهکار گرمی' : 'بستانکار گرمی'))
|
||||
const grams = computed(() =>
|
||||
customer.value ? Math.abs(parseFloat(customer.value.goldDebtGrams)) : 0,
|
||||
)
|
||||
|
||||
const data = reactive({ notify: false, loginNotify: false })
|
||||
|
||||
@@ -190,3 +212,262 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dash {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dash__bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid @hairline;
|
||||
}
|
||||
|
||||
.dash__barInner {
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
padding: 11px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dash__barTitle {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.dash__barBtn {
|
||||
flex: 0 0 auto;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.dash__barBtn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.dash__barSpacer {
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.dash__body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.dash__balance {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
background: @ink;
|
||||
color: #fff;
|
||||
box-shadow: 0 14px 30px -22px rgba(20, 22, 26, 0.95);
|
||||
}
|
||||
|
||||
.dash__balanceHead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dash__eyebrow {
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.dash__figure {
|
||||
padding-top: 5px;
|
||||
font-size: 25px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.dash__unit {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
|
||||
.dash__seal {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dash__seal.is-debtor {
|
||||
color: #fff;
|
||||
background: @wineSoft;
|
||||
}
|
||||
|
||||
.dash__seal.is-creditor {
|
||||
color: @ink;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dash__balanceRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding-top: 13px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
.dash__rowLabel {
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.dash__rowValue {
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dash__rowValue i {
|
||||
font-style: normal;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
padding-inline-start: 3px;
|
||||
}
|
||||
|
||||
.dash__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 13px;
|
||||
padding-top: 13px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
.dash__name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dash__mobile {
|
||||
font-size: 11.5px;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.dash__invite {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.dash__inviteIcon {
|
||||
flex: 0 0 42px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.dash__inviteCopy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.dash__inviteCopy h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
.dash__inviteCopy p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.dash__inviteCta {
|
||||
flex: 0 0 auto;
|
||||
padding: 9px 14px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
font-family: inherit;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
}
|
||||
|
||||
.dash__section {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.dash__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.dash__tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 6px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
background: @canvas;
|
||||
font-family: inherit;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.dash__tile:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.dash__tile.is-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.dash__tileIcon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @inkSoft;
|
||||
background: @hairlineSoft;
|
||||
}
|
||||
|
||||
.dash__tileTitle {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: @inkSoft;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,94 +1,93 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Login.vue.
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Login.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
Per your decision: OTP-based flow ported structurally, but the SMS-send step
|
||||
is stubbed — no SMS provider exists. What's real vs. stubbed:
|
||||
Per the earlier decision: OTP-based flow ported structurally, but the
|
||||
SMS-send step is stubbed — no SMS provider exists. What's real vs stubbed,
|
||||
all carried over unchanged from the previous version:
|
||||
- Step 1 (mobile number) is checked against the REAL, already-existing
|
||||
GET /api/customers endpoint (client-side match on `mobile`) — no fake
|
||||
endpoint invented. If it matches a real seeded customer, that customer's
|
||||
exact legacy-notify message path is preserved: a non-match shows
|
||||
legacy's own "اطلاعات کاربری در سیستم وجود ندارد" text (legacy's actual
|
||||
404 handling), not a new message.
|
||||
endpoint invented. A non-match shows legacy's own
|
||||
"اطلاعات کاربری در سیستم وجود ندارد" text, not a new message.
|
||||
- Step 2 (OTP code) is NOT verified — any 4-digit code is accepted, since
|
||||
there is no real code to check against. On "verify," the session store
|
||||
logs in as the matched real customer (session.login), so downstream
|
||||
features (balance, ledger, favorites-as-logged-in) work against real
|
||||
seeded data. This is the "session wiring" — apps/api itself is untouched.
|
||||
- `token.fireBaseToken` (Firebase push token) doesn't exist in this port
|
||||
(Firebase was already deferred) — omitted from the stub verify call.
|
||||
- `useOtpToken()` (WebOTP browser autofill) was already dead in legacy
|
||||
itself (its call was commented out in App.vue, and even active, nothing
|
||||
ever read its output into the OTP input) — not ported.
|
||||
there is no real code to check against. On "verify", the session store
|
||||
logs in as the matched real customer, so downstream features (balance,
|
||||
ledger, favourites-as-logged-in) work against real seeded data.
|
||||
- `token.fireBaseToken` doesn't exist in this port (Firebase deferred).
|
||||
- `useOtpToken()` (WebOTP autofill) was already dead in legacy itself.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="auth">
|
||||
<NotifyVant :timeOut="2000" v-model="notify.show" :text="notify.text" :color="'background-color: #DC2626'" />
|
||||
<div class="fixed w-full">
|
||||
<img class="w-full absolute left-0 right-0" :src="pattern1" alt="" />
|
||||
<img class="absolute w-full left-0 right-0" :src="pattern2" alt="" />
|
||||
</div>
|
||||
<div class="fixed top-0 z-10 w-full">
|
||||
<AppHeader class="bg-white shadowHeader">
|
||||
<div class="flex justify-between items-center">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="router.push({ name: 'home' })" />
|
||||
<h3 class="t bold mb-0">ورود</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
</AppHeader>
|
||||
|
||||
<div class="flex justify-center mb-8 mt-5">
|
||||
<div class="h-[150px] w-[150px] bg-[#F8F7F6] rounded-full flex justify-center items-center bg-opacity-50">
|
||||
<div class="bg-[#F9F5F0] h-[120px] w-[120px] flex justify-center items-center rounded-full">
|
||||
<div>
|
||||
<div class="rounded-full w-[90px] h-[90px] flex justify-center bg-[#F1E8DE]">
|
||||
<img :src="editImg" class="p-6" alt="henza" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageBar title="ورود" back-to="home" />
|
||||
|
||||
<container class="auth__body">
|
||||
<section class="auth__intro">
|
||||
<div class="auth__mark" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:security-user" size="28" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full text-center font-bold text-black text-lg">ورود به اپلیکیشن هنزا گلد</div>
|
||||
<div class="px-8 custom-form">
|
||||
<OtpInput
|
||||
class="w-full mb-[10px] text-black mt-5"
|
||||
v-show="body.step == 2 && timer > 0"
|
||||
input-classes="otp-input"
|
||||
inputType="tel"
|
||||
:num-inputs="4"
|
||||
:should-auto-focus="true"
|
||||
:defaultFocus="body.defaultFocus"
|
||||
@on-complete="sendToken"
|
||||
/>
|
||||
<div v-show="body.step == 2 && timer === 0" class="text-center mb-[10px] mt-5">برای دریافت رمز عبور مجدد تلاش کنید</div>
|
||||
<div v-show="body.step == 2 && timer !== 0" class="text-center text-xl mb-[10px] mt-5">
|
||||
امکان دریافت مجدد رمز در {{ ' ' + counterZeroShow(timer) + ' ' }} ثانیه دیگر
|
||||
</div>
|
||||
<Form :model="formModel" layout="vertical" @finish="sendPhone" class="w-full">
|
||||
<FormItem name="username" label="تلفن همراه" v-show="body.step == 1" :rules="[required('تلفن همراه'), mobileRule('تلفن همراه')]">
|
||||
<h2 class="auth__title">ورود به هنزاگلد</h2>
|
||||
<p class="auth__sub">
|
||||
{{ body.step === 1 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<!-- Step 2: OTP -->
|
||||
<template v-if="body.step === 2">
|
||||
<OtpInput
|
||||
class="auth__otp"
|
||||
input-classes="otp-input"
|
||||
inputType="tel"
|
||||
:num-inputs="4"
|
||||
:should-auto-focus="true"
|
||||
:defaultFocus="body.defaultFocus"
|
||||
@on-complete="sendToken"
|
||||
/>
|
||||
|
||||
<p v-if="timer > 0" class="auth__timer">
|
||||
امکان دریافت مجدد رمز در {{ counterZeroShow(timer) }} ثانیه دیگر
|
||||
</p>
|
||||
<p v-else class="auth__timer">برای دریافت رمز عبور مجدد تلاش کنید</p>
|
||||
</template>
|
||||
|
||||
<Form :model="formModel" layout="vertical" @finish="sendPhone" class="auth__form">
|
||||
<FormItem
|
||||
v-show="body.step === 1"
|
||||
name="username"
|
||||
label="تلفن همراه"
|
||||
:rules="[required('تلفن همراه'), mobileRule('تلفن همراه')]"
|
||||
>
|
||||
<Input type="number" v-model:value="formModel.username" />
|
||||
</FormItem>
|
||||
<Button class="btn-px-double" type="primary" html-type="submit" size="large" block :loading="body.loading" :disabled="timer > 0 && body.step === 2">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="auth__cta"
|
||||
:class="{ 'is-loading': body.loading }"
|
||||
:disabled="body.loading || (timer > 0 && body.step === 2)"
|
||||
>
|
||||
{{ btnText }}
|
||||
</Button>
|
||||
</button>
|
||||
</Form>
|
||||
<div class="flex justify-center mt-5">
|
||||
<div>اگر ثبت نام نکرده اید از این قسمت</div>
|
||||
<div class="pr-1 text-primary font-bold" @click="emit('register', true)">ثبت نام کنید.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر ثبت نام نکردهاید
|
||||
<button type="button" @click="emit('register', true)">ثبت نام کنید</button>
|
||||
</p>
|
||||
</container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Form, FormItem, Input } from 'ant-design-vue'
|
||||
import pattern1 from '../../assets/img/pattern2.svg'
|
||||
import pattern2 from '../../assets/img/pattern3.svg'
|
||||
import editImg from '../../assets/img/henza.svg'
|
||||
import { Form, FormItem, Input } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import NotifyVant from '../../components/NotifyVant.vue'
|
||||
import OtpInput from '../../components/input/OtpInput.vue'
|
||||
import { required, mobile as mobileRule } from '../../utils/validator'
|
||||
@@ -166,32 +165,147 @@ const counterZeroShow = (value: number) => (value < 10 ? '0' + value : value)
|
||||
const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور' : 'دریافت مجدد رمز عبور'))
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.shadowHeader {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.custom-form {
|
||||
.otp-input {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
background: transparent;
|
||||
border: 0 solid @black;
|
||||
border-bottom-width: 2px;
|
||||
text-align: center;
|
||||
.auth__body {
|
||||
padding-top: 26px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: 1px solid @danger !important;
|
||||
}
|
||||
}
|
||||
.auth__intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.otp-input::-webkit-inner-spin-button,
|
||||
.otp-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
direction: ltr;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.auth__timer {
|
||||
margin: 12px 0 16px;
|
||||
text-align: center;
|
||||
font-size: 11.5px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
width: 100%;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.auth__cta:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.auth__cta:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__switch button {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* OtpInput renders its inputs with this class from a prop, so the rule cannot
|
||||
be scoped to this component. */
|
||||
.otp-input {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 13px;
|
||||
text-align: center;
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.otp-input:focus {
|
||||
outline: none;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
.otp-input.error {
|
||||
border-color: @danger !important;
|
||||
}
|
||||
|
||||
.otp-input::-webkit-inner-spin-button,
|
||||
.otp-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,126 +1,166 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/Orders.vue.
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/Orders.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
Adapted:
|
||||
- Status filter tabs use our real OrderStatus enum values directly (see
|
||||
Adapted (unchanged from the previous version):
|
||||
- Status filter chips use our real OrderStatus enum values directly (see
|
||||
utils/orderStatus.ts) rather than legacy's own inconsistent tab labels.
|
||||
They are chips now instead of ant Tabs, which lets them scroll
|
||||
horizontally like every other filter rail in the redesign.
|
||||
- Fetches real order history via the already-existing GET
|
||||
/customers/:id/orders endpoint, filtered client-side by tab (that
|
||||
/customers/:id/orders endpoint, filtered client-side by chip (that
|
||||
endpoint has no server-side status filter — small enough dataset that
|
||||
client-side filtering is reasonable, and apps/api stays untouched).
|
||||
- Search matches against `orderNumber` (our real user-facing identifier)
|
||||
instead of legacy's numeric `id`.
|
||||
|
||||
DROPPED: "دریافت فاکتور" (get invoice) button — legacy's handler opened a
|
||||
hardcoded link to the real legacy production domain
|
||||
(https://henzagold.com/oapi/...). Not just unimplemented — an actual
|
||||
DROPPED, unchanged: "دریافت فاکتور" — legacy's handler opened a hardcoded
|
||||
link to the real legacy production domain. Not merely unimplemented; an
|
||||
external production URL that must never be carried into this port. No
|
||||
invoice-generation endpoint exists in the new API either.
|
||||
|
||||
The detail view is a bottom Drawer rather than a full-screen ant Modal with
|
||||
hand-written overrides, matching how every other detail surface in the
|
||||
redesign opens (product detail, cart note, ledger entry).
|
||||
-->
|
||||
<template>
|
||||
<div class="fixed top-0 z-10 w-full bg-white">
|
||||
<AppHeader>
|
||||
<div class="flex justify-between items-center">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer text-white" size="25" @click="router.push({ name: 'home' })" />
|
||||
<h3 class="text-white bold mb-0">سفارشات</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
</AppHeader>
|
||||
<div class="pt-3 px-2 pb-3 mb-3 border-b-solid border-[#E5E7EB] w-full">
|
||||
<SearchInput @search="readSearch" place-holder="جستجو" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-36"></div>
|
||||
<div class="ord">
|
||||
<PageBar title="سفارشات" back-to="home">
|
||||
<template #below>
|
||||
<div class="ord__barExtra">
|
||||
<SearchInput @search="readSearch" place-holder="جستجوی شماره سفارش" />
|
||||
|
||||
<container class="bg-[#F9FAFB] px-2 orders">
|
||||
<Tabs v-model:activeKey="activeTab" style="padding-right: 0.2rem !important; font-weight: bold">
|
||||
<TabPane v-for="item in tabs" :key="item.key" :tab="item.title" force-render>
|
||||
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" cls="overflow-y-scroll no-scrollbar">
|
||||
<OrderCard @view="openModal" v-for="item in filteredOrders" :key="item.id" :value="item" />
|
||||
<EmptyState v-if="filteredOrders.length === 0" :image="ordersEmpty" />
|
||||
</Spinner>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Modal v-model:visible="viewData.model" :mask="false" :footer="false" :closable="false" class="henza-custom-modal" v-if="viewData.item">
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="viewData.model = false" />
|
||||
<h3 class="font-semibold mb-0">جزئیات سفارش</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
<container>
|
||||
<div class="overflow-y-auto h-screen">
|
||||
<div class="flex justify-between items-center pt-4">
|
||||
<span class="font-semibold text-[14px]">اطلاعات سفارش</span>
|
||||
</div>
|
||||
<div class="responsive-steps">
|
||||
<div class="steps-container">
|
||||
<div v-for="(step, index) in ORDER_STEPS" :key="index" class="step" :class="{ active: currentStep === index }">
|
||||
<div class="step-circle">{{ index + 1 }}</div>
|
||||
<div class="step-title">{{ step }}</div>
|
||||
<div v-if="index !== 0" class="step-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 border-[1.5px] border-[#E5E7EB] border-solid mt-4 rounded-[6px]">
|
||||
<div class="grid col-span-1 p-4 text-[#9CA3AF] text-[12px] font-medium">
|
||||
<div>شماره سفارش :</div>
|
||||
<div class="pt-4">وضعیت سفارش :</div>
|
||||
<div class="pt-4">تعداد کالاها :</div>
|
||||
<div class="pt-4">وزن سفارش :</div>
|
||||
<div class="pt-4">تاریخ سفارش :</div>
|
||||
</div>
|
||||
<div class="grid col-span-1 text-left p-4">
|
||||
<div class="font-bold text-[18px] text-[#23335B]">{{ viewData.item.orderNumber }}</div>
|
||||
<div class="pt-4 text-center flex justify-end">
|
||||
<div class="rounded-[5px] py-[4px] px-[12px] text-[14px] font-semibold w-fit" :style="{ background: modalColor?.background, color: modalColor?.font }">
|
||||
{{ modalStatusLabel }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<span class="text-[16px] font-bold pl-2 text-black">{{ viewData.item.items.length }}</span>
|
||||
<span class="text-[14px] font-medium">آیتم</span>
|
||||
</div>
|
||||
<div class="pt-4 font-medium">
|
||||
<span class="text-[16px] font-bold pl-1 text-black">{{ parseFloat(viewData.item.totalGoldWeightGrams).toFixed(2) }}</span>
|
||||
<span class="pr-2">گرم</span>
|
||||
</div>
|
||||
<div class="pt-4 font-semibold text-[#1E2229]">{{ jdate(viewData.item.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold mt-4">آیتم های سفارش</div>
|
||||
<div class="w-full max-w-screen-md m-auto h-full flex flex-col">
|
||||
<div class="flex-1 py-[10px] pb-[70px]">
|
||||
<OrderItemCard v-for="item in viewData.item.items" :key="item.id" :value="item" class="border-[#E5E7EB] border-[1.5px] border-solid text-primary bg-[#F9F9F9] rounded-[6px] mb-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ord__chips no-scrollbar">
|
||||
<button
|
||||
v-for="item in tabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="ord__chip"
|
||||
:class="{ 'is-on': activeTab === item.key }"
|
||||
@click="activeTab = item.key"
|
||||
>
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageBar>
|
||||
|
||||
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات">
|
||||
<container class="ord__body">
|
||||
<template v-if="filteredOrders.length">
|
||||
<SectionHead title="سفارشهای شما" :subtitle="`${filteredOrders.length} سفارش`" />
|
||||
<div class="ord__list">
|
||||
<OrderCard v-for="item in filteredOrders" :key="item.id" :value="item" @view="openDetail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-else-if="!loading" class="ord__empty">
|
||||
<Icon icon="vuesax-linear:box" size="26" />
|
||||
<p>سفارشی با این مشخصات پیدا نشد.</p>
|
||||
</section>
|
||||
</container>
|
||||
</Modal>
|
||||
</container>
|
||||
</Spinner>
|
||||
|
||||
<Drawer
|
||||
v-model:visible="viewData.model"
|
||||
placement="bottom"
|
||||
v-if="viewData.item"
|
||||
:closable="false"
|
||||
height="100vh"
|
||||
class="rtl"
|
||||
:bodyStyle="{ padding: 0, position: 'relative' }"
|
||||
>
|
||||
<div class="detail">
|
||||
<header class="detail__bar">
|
||||
<button type="button" class="detail__close" aria-label="بستن" @click="viewData.model = false">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" size="20" />
|
||||
</button>
|
||||
<h2 class="detail__title">جزئیات سفارش</h2>
|
||||
<span class="detail__spacer" aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<div class="detail__scroll">
|
||||
<!-- Progress -->
|
||||
<section class="detail__block">
|
||||
<div class="detail__steps">
|
||||
<div
|
||||
v-for="(step, index) in ORDER_STEPS"
|
||||
:key="step"
|
||||
class="detail__step"
|
||||
:class="{ 'is-done': index <= currentStep, 'is-active': index === currentStep }"
|
||||
>
|
||||
<span class="detail__stepDot">{{ index + 1 }}</span>
|
||||
<span class="detail__stepTitle">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Headline facts -->
|
||||
<section class="detail__block">
|
||||
<div class="detail__headline">
|
||||
<div>
|
||||
<div class="detail__eyebrow">شماره سفارش</div>
|
||||
<div class="detail__number">{{ viewData.item.orderNumber }}</div>
|
||||
</div>
|
||||
<span
|
||||
class="detail__status"
|
||||
:style="{ background: modalColor?.background, color: modalColor?.font }"
|
||||
>
|
||||
{{ modalStatusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl class="detail__specs">
|
||||
<div class="detail__spec">
|
||||
<dt>تعداد کالاها</dt>
|
||||
<dd>{{ viewData.item.items.length }} آیتم</dd>
|
||||
</div>
|
||||
<div class="detail__spec">
|
||||
<dt>وزن سفارش</dt>
|
||||
<dd>{{ parseFloat(viewData.item.totalGoldWeightGrams).toFixed(2) }} گرم</dd>
|
||||
</div>
|
||||
<div class="detail__spec">
|
||||
<dt>تاریخ سفارش</dt>
|
||||
<dd>{{ jdate(viewData.item.createdAt) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- Items -->
|
||||
<section class="detail__block">
|
||||
<h3 class="detail__blockTitle">آیتمهای سفارش</h3>
|
||||
<div class="detail__items">
|
||||
<OrderItemCard v-for="item in viewData.item.items" :key="item.id" :value="item" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Modal, TabPane, Tabs } from 'ant-design-vue'
|
||||
import ordersEmpty from '../../assets/img/empty/ordersEmpty.svg'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import SearchInput from '../../components/search/SearchInput.vue'
|
||||
import OrderCard from '../../components/product/OrderCard.vue'
|
||||
import OrderItemCard from '../../components/product/OrderItemCard.vue'
|
||||
import { ORDER_STATUS_COLORS, ORDER_STATUS_LABELS, ORDER_STEPS, orderStepIndex } from '../../utils/orderStatus'
|
||||
import {
|
||||
ORDER_STATUS_COLORS,
|
||||
ORDER_STATUS_LABELS,
|
||||
ORDER_STEPS,
|
||||
orderStepIndex,
|
||||
} from '../../utils/orderStatus'
|
||||
import { jdate } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type OrderDetail } from '../../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
|
||||
const tabs = [
|
||||
@@ -162,11 +202,13 @@ async function loadOrders() {
|
||||
}
|
||||
|
||||
const viewData = reactive<{ model: boolean; item: OrderDetail | null }>({ model: false, item: null })
|
||||
const modalStatusLabel = computed(() => (viewData.item ? ORDER_STATUS_LABELS[viewData.item.status] ?? viewData.item.status : ''))
|
||||
const modalStatusLabel = computed(() =>
|
||||
viewData.item ? ORDER_STATUS_LABELS[viewData.item.status] ?? viewData.item.status : '',
|
||||
)
|
||||
const modalColor = computed(() => (viewData.item ? ORDER_STATUS_COLORS[viewData.item.status] : undefined))
|
||||
const currentStep = computed(() => (viewData.item ? orderStepIndex(viewData.item.status) : 0))
|
||||
|
||||
function openModal(item: OrderDetail) {
|
||||
function openDetail(item: OrderDetail) {
|
||||
viewData.item = item
|
||||
viewData.model = true
|
||||
}
|
||||
@@ -174,109 +216,262 @@ function openModal(item: OrderDetail) {
|
||||
onMounted(loadOrders)
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.henza-custom-modal,
|
||||
.henza-custom-modal .ant-modal-body,
|
||||
.henza-custom-modal .ant-modal-content {
|
||||
border-radius: 0;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
direction: rtl;
|
||||
<style scoped lang="less">
|
||||
.ord {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.orders {
|
||||
min-height: calc(100vh - 150px);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.orders .ant-tabs > .ant-tabs-nav,
|
||||
.ant-tabs > div > .ant-tabs-nav {
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
.ord__barExtra {
|
||||
max-width: 768px;
|
||||
margin: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #e5e7eb;
|
||||
border-bottom-width: 1px;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px 11px;
|
||||
}
|
||||
|
||||
.responsive-steps {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
.steps-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
.step-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
color: #9ca3af;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&.active .step-circle {
|
||||
background-color: @primary-color;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.ord__chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 11px -16px 0;
|
||||
padding: 0 16px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.step-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
.step-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.ord__chip {
|
||||
scroll-snap-align: start;
|
||||
flex: 0 0 auto;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: @inkSoft;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ord__chip.is-on {
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
.ord__chip:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.ord__body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.ord__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.ord__empty {
|
||||
margin-top: 8px;
|
||||
padding: 34px 20px;
|
||||
border: 1px dashed @hairline;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
.ord__empty p {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
/* ── Detail drawer ─────────────────────────────────────────────────── */
|
||||
.detail {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: @canvas;
|
||||
}
|
||||
|
||||
.detail__bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid @hairline;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.detail__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.detail__close {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @ink;
|
||||
background: @hairlineSoft;
|
||||
}
|
||||
|
||||
.detail__spacer {
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.detail__scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.detail__block {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.detail__blockTitle {
|
||||
margin: 0 0 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.detail__steps {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.detail__step {
|
||||
flex: 1 0 74px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail__stepDot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: @inkFaint;
|
||||
background: @hairlineSoft;
|
||||
border: 1px solid @hairline;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.detail__step.is-done .detail__stepDot {
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
|
||||
.detail__step.is-active .detail__stepDot {
|
||||
color: #fff;
|
||||
background: @wine;
|
||||
border-color: @wine;
|
||||
}
|
||||
|
||||
.detail__stepTitle {
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.detail__step.is-done .detail__stepTitle {
|
||||
color: @inkSoft;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail__headline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.detail__eyebrow {
|
||||
font-size: 11px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.detail__number {
|
||||
padding-top: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.detail__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail__specs {
|
||||
margin: 0;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail__spec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid @hairlineSoft;
|
||||
}
|
||||
|
||||
.detail__spec:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.detail__spec dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.detail__spec dd {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.detail__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,70 +1,74 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/profile.vue,
|
||||
heavily simplified — legacy's original (971 lines, the largest view in the
|
||||
app) is a full edit form (bank info, KYC document uploads, sub-user
|
||||
management for banker accounts). None of that has a backend to back it:
|
||||
there's no update-customer endpoint, no document-upload endpoint, and (as
|
||||
in DashboardView) no banker/sub-account concept in the new Customer model
|
||||
at all. Kept, faithfully styled: the avatar circle and the real read-only
|
||||
info card (name/mobile/national code/verification status/city), all backed
|
||||
by real fields the API already returns.
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
DROPPED: "تکمیل پروفایل" (complete profile) button, "افزودن کاربر" (add
|
||||
sub-user) FAB, the "کارشناس فروش" (sales agent) contact card — that last
|
||||
one needs the referring AdminUser's name/mobile, which the current
|
||||
GET /customers/:id response doesn't expose (only referredByAgentId, a bare
|
||||
id) — not resolved here to keep apps/api untouched this batch.
|
||||
Heavily simplified when first ported, and still is — legacy's original (971
|
||||
lines, the largest view in the app) is a full edit form (bank info, KYC
|
||||
document uploads, sub-user management for banker accounts). None of that has
|
||||
a backend to back it: there's no update-customer endpoint, no
|
||||
document-upload endpoint, and (as in DashboardView) no banker/sub-account
|
||||
concept in the new Customer model at all. Kept: the real read-only info
|
||||
card, all backed by fields the API genuinely returns.
|
||||
|
||||
DROPPED, unchanged from the previous version: "تکمیل پروفایل", the
|
||||
"افزودن کاربر" sub-user FAB, and the "کارشناس فروش" contact card — that last
|
||||
one needs the referring AdminUser's name/mobile, which GET /customers/:id
|
||||
does not expose (only referredByAgentId, a bare id).
|
||||
-->
|
||||
<template>
|
||||
<div class="fixed top-0 z-10 w-full">
|
||||
<AppHeader class="bg-white shadowHeader">
|
||||
<div class="flex justify-between items-center">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="router.push({ name: 'userPage::hasSubRoute' })" />
|
||||
<h3 class="t bold mb-0">اطلاعات کاربری</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
</AppHeader>
|
||||
</div>
|
||||
<div class="h-20"></div>
|
||||
<div class="prof">
|
||||
<PageBar title="اطلاعات کاربری" back-to="userPage::hasSubRoute" />
|
||||
|
||||
<container>
|
||||
<div class="flex justify-center mb-8">
|
||||
<div class="h-[150px] w-[150px] bg-[#F8F7F6] rounded-full flex justify-center items-center bg-opacity-50">
|
||||
<div class="bg-[#F9F5F0] h-[120px] w-[120px] flex justify-center items-center rounded-full">
|
||||
<div class="rounded-full w-[90px] h-[90px] flex justify-center bg-[#F1E8DE]">
|
||||
<img :src="editImg" class="p-6" alt="henza" />
|
||||
</div>
|
||||
<container class="prof__body">
|
||||
<!-- Identity -->
|
||||
<section class="prof__identity">
|
||||
<div class="prof__avatar" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:user" size="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="prof__name">{{ customer?.fullName ?? '—' }}</h2>
|
||||
<p v-if="customer" class="prof__mobile">{{ customer.mobile }}</p>
|
||||
<span v-if="customer" class="prof__seal" :class="isVerified ? 'is-ok' : 'is-pending'">
|
||||
{{ VERIFICATION_LABELS[customer.verificationStep] ?? customer.verificationStep }}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<div v-if="customer" class="border-solid border-[1px] border-[#F3F4F6] rounded-[6px] p-8 mt-2 bg-[#FEFEFE]">
|
||||
<div class="flex justify-between">
|
||||
<div class="text-[#9CA3AF] font-medium">نام و نام خانوادگی :</div>
|
||||
<div class="text-[#374151] text-[16px] font-bold text-left">{{ customer.fullName }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<div class="text-[#9CA3AF] font-medium">تلفن همراه :</div>
|
||||
<div class="text-[#374151] text-[16px] font-bold text-left">{{ customer.mobile }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<div class="text-[#9CA3AF] font-medium">کد ملی :</div>
|
||||
<div class="text-[#374151] text-[16px] font-bold text-left">{{ customer.nationalCode }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-8">
|
||||
<div class="text-[#9CA3AF] font-medium">وضعیت احراز هویت :</div>
|
||||
<div class="text-[#374151] text-[16px] font-bold text-left">{{ VERIFICATION_LABELS[customer.verificationStep] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</container>
|
||||
<!-- Real, read-only fields -->
|
||||
<section v-if="customer" class="prof__block">
|
||||
<SectionHead title="مشخصات" subtitle="اطلاعات ثبتشده حساب شما" />
|
||||
<dl class="prof__specs">
|
||||
<div class="prof__spec">
|
||||
<dt>نام و نام خانوادگی</dt>
|
||||
<dd>{{ customer.fullName }}</dd>
|
||||
</div>
|
||||
<div class="prof__spec">
|
||||
<dt>تلفن همراه</dt>
|
||||
<dd>{{ customer.mobile }}</dd>
|
||||
</div>
|
||||
<div class="prof__spec">
|
||||
<dt>کد ملی</dt>
|
||||
<dd>{{ customer.nationalCode }}</dd>
|
||||
</div>
|
||||
<div class="prof__spec">
|
||||
<dt>وضعیت احراز هویت</dt>
|
||||
<dd>{{ VERIFICATION_LABELS[customer.verificationStep] ?? customer.verificationStep }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section v-else-if="!loading" class="prof__empty">
|
||||
<Icon icon="vuesax-linear:user" size="26" />
|
||||
<p>اطلاعات حساب در دسترس نیست.</p>
|
||||
</section>
|
||||
</container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import editImg from '../../assets/img/henza.svg'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type CustomerDetail } from '../../api/client'
|
||||
|
||||
@@ -75,19 +79,138 @@ const VERIFICATION_LABELS: Record<string, string> = {
|
||||
VERIFIED: 'تایید شده',
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
const customer = ref<CustomerDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const isVerified = computed(() => customer.value?.verificationStep === 'VERIFIED')
|
||||
|
||||
onMounted(async () => {
|
||||
if (session.customerId) {
|
||||
if (!session.customerId) return
|
||||
loading.value = true
|
||||
try {
|
||||
customer.value = await api.getCustomer(session.customerId)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shadowHeader {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||||
<style scoped lang="less">
|
||||
.prof {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.prof__body {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.prof__identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 26px 20px 28px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.prof__avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.prof__name {
|
||||
margin: 14px 0 0;
|
||||
font-size: 15.5px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.prof__mobile {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: @inkMuted;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.prof__seal {
|
||||
margin-top: 12px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prof__seal.is-ok {
|
||||
color: @green;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.prof__seal.is-pending {
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.prof__block {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.prof__specs {
|
||||
margin: 0;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prof__spec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 13px 14px;
|
||||
border-bottom: 1px solid @hairlineSoft;
|
||||
}
|
||||
|
||||
.prof__spec:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.prof__spec dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.prof__spec dd {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.prof__empty {
|
||||
margin-top: 26px;
|
||||
padding: 34px 20px;
|
||||
border: 1px dashed @hairline;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
.prof__empty p {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,84 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Register.vue.
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Register.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
DROPPED as confirmed-dead code (not "possibly dead" — verified unreachable):
|
||||
legacy rendered a whole cascading country->province->city picker
|
||||
DROPPED as confirmed-dead code (verified unreachable, not merely "possibly
|
||||
dead"): legacy rendered a whole cascading country->province->city picker
|
||||
(WorkActionSheet + selectCityArrays + getCities() against POST /cities/list)
|
||||
alongside a plain "شهر" text <Input>. The only element that ever opened the
|
||||
picker (`#citybtn`) was itself commented out in legacy's own template, so
|
||||
the picker was unreachable UI even in the original app; the visible form
|
||||
uses the plain text input. That whole complex, and the parallel unused
|
||||
`userFormModel`/`defaultUserFormModel` objects (the template binds
|
||||
`formModel.*`, a *different*, separately-declared empty reactive object —
|
||||
`userFormModel` is never referenced anywhere), are not ported.
|
||||
picker (`#citybtn`) was itself commented out in legacy's own template, so the
|
||||
picker was unreachable UI even in the original app; the visible form uses the
|
||||
plain text input. That whole complex, and the parallel unused
|
||||
`userFormModel`/`defaultUserFormModel` objects, are not ported.
|
||||
|
||||
STUBBED, not real: POST /major/register — apps/api has no create-customer
|
||||
endpoint. Submitting shows legacy's exact success dialog without actually
|
||||
persisting anything.
|
||||
STUBBED, not real, and unchanged from the previous version: legacy's
|
||||
POST /major/register — apps/api has no create-customer endpoint. Submitting
|
||||
shows legacy's exact success message without persisting anything. The copy
|
||||
says the account awaits review, which is what legacy said too, so the screen
|
||||
does not claim an account exists when none was created.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="fixed w-full">
|
||||
<img class="w-full absolute left-0 right-0" :src="pattern1" alt="" />
|
||||
<img class="absolute w-full left-0 right-0" :src="pattern2" alt="" />
|
||||
</div>
|
||||
<div class="fixed top-0 z-10 w-full">
|
||||
<AppHeader class="bg-white shadowHeader">
|
||||
<div class="flex justify-between items-center">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="router.push({ name: 'home' })" />
|
||||
<h3 class="t bold mb-0">ثبت نام</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
</AppHeader>
|
||||
</div>
|
||||
<div class="relative w-full py-16 px-5">
|
||||
<div class="mt-14">
|
||||
<Spinner :spinning="profileData.loading" tip="در حال ثبت اطلاعات">
|
||||
<Form :model="formModel" layout="vertical" @finish="register" class="w-full">
|
||||
<FormItem style="font-size: 16px" name="fname" label="نام" :rules="required('نام')">
|
||||
<div class="auth">
|
||||
<PageBar title="ثبت نام" back-to="home" />
|
||||
|
||||
<Spinner :spinning="profileData.loading" tip="در حال ثبت اطلاعات">
|
||||
<container class="auth__body">
|
||||
<section class="auth__intro">
|
||||
<div class="auth__mark" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:user-add" size="28" />
|
||||
</div>
|
||||
<h2 class="auth__title">ساخت حساب کاربری</h2>
|
||||
<p class="auth__sub">اطلاعات فروشگاه خود را وارد کنید</p>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<Form :model="formModel" layout="vertical" @finish="register" class="auth__form">
|
||||
<FormItem name="fname" label="نام" :rules="required('نام')">
|
||||
<Input v-model:value="formModel.fname" />
|
||||
</FormItem>
|
||||
<FormItem style="font-size: 16px" name="lname" label="نام خانوادگی" :rules="required('نام خانوادگی')">
|
||||
<FormItem name="lname" label="نام خانوادگی" :rules="required('نام خانوادگی')">
|
||||
<Input v-model:value="formModel.lname" />
|
||||
</FormItem>
|
||||
<FormItem style="font-size: 16px" name="mobile" label="شماره موبایل" :rules="[mobileRule('شماره موبایل'), required('شماره موبایل')]">
|
||||
<FormItem
|
||||
name="mobile"
|
||||
label="شماره موبایل"
|
||||
:rules="[mobileRule('شماره موبایل'), required('شماره موبایل')]"
|
||||
>
|
||||
<Input v-model:value="formModel.mobile" type="number" />
|
||||
</FormItem>
|
||||
<FormItem style="font-size: 16px" name="store_name" label="نام فروشگاه" :rules="required('نام فروشگاه')">
|
||||
<FormItem name="store_name" label="نام فروشگاه" :rules="required('نام فروشگاه')">
|
||||
<Input v-model:value="formModel.store_name" />
|
||||
</FormItem>
|
||||
<FormItem style="font-size: 16px" name="phone" label="تلفن فروشگاه" :rules="[required('تلفن فروشگاه')]">
|
||||
<FormItem name="phone" label="تلفن فروشگاه" :rules="[required('تلفن فروشگاه')]">
|
||||
<Input v-model:value="formModel.phone" type="number" />
|
||||
</FormItem>
|
||||
<FormItem style="font-size: 16px" name="address" label="آدرس پستی" :rules="required('آدرس')">
|
||||
<Textarea v-model:value="formModel.address" />
|
||||
<FormItem name="address" label="آدرس پستی" :rules="required('آدرس')">
|
||||
<Textarea v-model:value="formModel.address" :rows="3" />
|
||||
</FormItem>
|
||||
<FormItem name="city" label="شهر" :rules="required('شهر')">
|
||||
<Input v-model:value="formModel.city" />
|
||||
</FormItem>
|
||||
<Button type="primary" class="w-full max-w-[768px] m-auto" style="height: 42px" html-type="submit">
|
||||
<span style="font-size: 18px">ثبت اطلاعات</span>
|
||||
</Button>
|
||||
|
||||
<button type="submit" class="auth__cta">ثبت اطلاعات</button>
|
||||
</Form>
|
||||
</Spinner>
|
||||
<div class="flex justify-center mt-10">
|
||||
<div>اگر حساب کاربری دارید از این قسمت</div>
|
||||
<div class="pr-1 text-primary font-bold" @click="emit('register', false)">وارد شوید.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر حساب کاربری دارید
|
||||
<button type="button" @click="emit('register', false)">وارد شوید</button>
|
||||
</p>
|
||||
</container>
|
||||
</Spinner>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="registeredInfo" :closeOnClickOverlay="false">
|
||||
<template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-5">ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.</div>
|
||||
<div class="p-5">
|
||||
ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center" @click="router.push({ name: 'home' })">متوجه شدم</div>
|
||||
@@ -84,12 +90,10 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Form, FormItem, Input, Textarea } from 'ant-design-vue'
|
||||
import pattern1 from '../../assets/img/pattern2.svg'
|
||||
import pattern2 from '../../assets/img/pattern3.svg'
|
||||
import { Form, FormItem, Input, Textarea } from 'ant-design-vue'
|
||||
import registerDelayImage from '../../assets/img/registerDelay.svg'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import DialogVant from '../../components/DialogVant.vue'
|
||||
import { required, mobile as mobileRule } from '../../utils/validator'
|
||||
@@ -121,8 +125,94 @@ const register = async () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shadowHeader {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.auth__body {
|
||||
padding-top: 26px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.auth__intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px 20px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.auth__cta:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__switch button {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,74 +1,63 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/src/components/transactions.vue
|
||||
("اسناد" — financial documents). Replaces the English AccountView scaffold
|
||||
that had no legacy counterpart.
|
||||
("اسناد" — financial documents), restyled into the redesigned design
|
||||
language (see components/home/* and HomeView).
|
||||
|
||||
Data source: legacy listed `Cash` rows from POST /get-my-findoc. The new
|
||||
system's equivalent is LedgerEntry, already exposed by
|
||||
GET /customers/:id/ledger-entries, so the list is real data.
|
||||
|
||||
Legacy's colour/icon logic is preserved exactly: `hasFactor(item)` was
|
||||
`item.type != 9` (type 9 = سفارش, the debt-creating order entry) → green
|
||||
#F0FDF4/#22C55E + add-square for everything else, red #FEF2F2/#F44336 +
|
||||
minus-square for orders. That maps 1:1 onto our CREDIT/DEBIT direction,
|
||||
since an ORDER_STOCK entry is exactly the DEBIT case.
|
||||
Legacy's colour/icon logic is preserved in meaning: `hasFactor(item)` was
|
||||
`item.type != 9` (type 9 = سفارش, the debt-creating order entry) → a green
|
||||
"in" treatment for everything else, red "out" for orders. That maps 1:1 onto
|
||||
our CREDIT/DEBIT direction, since an ORDER_STOCK entry is exactly the DEBIT
|
||||
case. The palette is now the design system's own rather than hardcoded hexes.
|
||||
|
||||
DROPPED: the "دریافت فایل" per-document download — legacy's getFile() hit the
|
||||
old Laravel host; there is no document-file endpoint in the new API.
|
||||
DROPPED, unchanged from the previous version: the "دریافت فایل" per-document
|
||||
download — legacy's getFile() hit the old Laravel host; there is no
|
||||
document-file endpoint in the new API.
|
||||
-->
|
||||
<template>
|
||||
<div class="fixed top-0 z-10 w-full">
|
||||
<AppHeader>
|
||||
<div class="flex justify-between items-center">
|
||||
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer text-white" size="25" @click="router.push({ name: 'userPage::hasSubRoute' })" />
|
||||
<h3 class="text-white bold mb-0">اسناد</h3>
|
||||
<div></div>
|
||||
</div>
|
||||
</AppHeader>
|
||||
</div>
|
||||
<div class="h-24 bg-[#F9FAFB]"></div>
|
||||
<div class="tx">
|
||||
<PageBar title="اسناد" back-to="userPage::hasSubRoute" />
|
||||
|
||||
<container class="overflow-y-auto bg-[#F9FAFB]">
|
||||
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" cls="no-scrollbar overflow-auto">
|
||||
<div
|
||||
v-if="entries.length"
|
||||
v-for="item in entries"
|
||||
:key="item.id"
|
||||
@click="openEntry(item)"
|
||||
class="rounded-[10px] border-[#E8EAEE] py-[16px] px-[14px] w-full mb-[16px] grid grid-cols-8 items-center cursor-pointer bg-white"
|
||||
>
|
||||
<div class="col-span-1 items-end h-full">
|
||||
<Icon
|
||||
:icon="getIcon(item)"
|
||||
:style="{ background: getColor(item), color: getColor2(item) }"
|
||||
class="rounded-full p-[10px] ml-2 h-[40px] w-[40px]"
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات">
|
||||
<container class="tx__body">
|
||||
<template v-if="entries.length">
|
||||
<SectionHead title="گردش حساب" :subtitle="`${entries.length} سند`" />
|
||||
|
||||
<div class="col-span-7 pr-1 text-primary mt-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-bold mb-2">{{ item.note || '- -' }}</div>
|
||||
<div
|
||||
class="rounded-[5px] px-[20px] py-[4px] w-fit text-[16px] mb-2 font-semibold"
|
||||
:style="{ background: getColor(item), color: getColor2(item) }"
|
||||
>
|
||||
{{ SOURCE_LABELS[item.sourceType] ?? item.sourceType }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-[14px] font-semibold mb-2">{{ formatAmount(item) }}</div>
|
||||
<div class="h-10"></div>
|
||||
</div>
|
||||
<div class="mb-2 text-[14px] text-grey font-semibold">{{ jdate(item.createdAt) }}</div>
|
||||
<div class="flex justify-between pt-1">
|
||||
<div class="text-[12px] font-bold">{{ item.confirmed ? 'تایید شده' : 'در انتظار تایید' }}</div>
|
||||
<Icon icon="vuesax-linear:arrow-left-1" class="cursor-pointer text-primary" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tx__list">
|
||||
<article v-for="item in entries" :key="item.id" class="tx__row" @click="openEntry(item)">
|
||||
<span class="tx__mark" :class="isCredit(item) ? 'is-in' : 'is-out'" aria-hidden="true">
|
||||
<Icon :icon="isCredit(item) ? 'vuesax-linear:arrow-down' : 'vuesax-linear:arrow-up'" size="16" />
|
||||
</span>
|
||||
|
||||
<EmptyState v-else :image="transactionEmpty" />
|
||||
<div class="tx__main">
|
||||
<div class="tx__head">
|
||||
<h3 class="tx__kind">{{ SOURCE_LABELS[item.sourceType] ?? item.sourceType }}</h3>
|
||||
<span class="tx__amount" :class="isCredit(item) ? 'is-in' : 'is-out'">
|
||||
{{ formatAmount(item) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="item.note" class="tx__note">{{ item.note }}</p>
|
||||
|
||||
<div class="tx__foot">
|
||||
<span class="tx__date">{{ jdate(item.createdAt) }}</span>
|
||||
<span class="tx__state" :class="{ 'is-pending': !item.confirmed }">
|
||||
{{ item.confirmed ? 'تایید شده' : 'در انتظار تایید' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-else-if="!loading" class="tx__empty">
|
||||
<Icon icon="vuesax-linear:receipt-text" size="26" />
|
||||
<p>سندی برای حساب شما ثبت نشده است.</p>
|
||||
</section>
|
||||
</container>
|
||||
</Spinner>
|
||||
|
||||
<Drawer
|
||||
@@ -76,48 +65,46 @@
|
||||
placement="bottom"
|
||||
v-if="activeEntry"
|
||||
:closable="false"
|
||||
height="450px"
|
||||
:class="['henza-rounded-drawer rtl', 'bg-white', 'text-primary']"
|
||||
:bodyStyle="{ display: 'flex', flexDirection: 'column' }"
|
||||
height="420px"
|
||||
class="rtl"
|
||||
:bodyStyle="{ padding: 0 }"
|
||||
>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="pb-[10px] border-b border-solid border-[#E5E7EB] flex text-[16px] font-bold">جزئیات سند</div>
|
||||
<div class="flex-1 py-[10px] overflow-auto no-scrollbar">
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-grey">نوع سند :</div>
|
||||
<div class="font-semibold">{{ SOURCE_LABELS[activeEntry.sourceType] ?? activeEntry.sourceType }}</div>
|
||||
<div class="sheet">
|
||||
<h3 class="sheet__title">جزئیات سند</h3>
|
||||
<dl class="sheet__specs">
|
||||
<div class="sheet__spec">
|
||||
<dt>نوع سند</dt>
|
||||
<dd>{{ SOURCE_LABELS[activeEntry.sourceType] ?? activeEntry.sourceType }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-grey">مبلغ :</div>
|
||||
<div class="font-semibold">{{ formatAmount(activeEntry) }}</div>
|
||||
<div class="sheet__spec">
|
||||
<dt>مبلغ</dt>
|
||||
<dd :class="isCredit(activeEntry) ? 'is-in' : 'is-out'">{{ formatAmount(activeEntry) }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-grey">وضعیت :</div>
|
||||
<div class="font-semibold">{{ activeEntry.confirmed ? 'تایید شده' : 'در انتظار تایید' }}</div>
|
||||
<div class="sheet__spec">
|
||||
<dt>وضعیت</dt>
|
||||
<dd>{{ activeEntry.confirmed ? 'تایید شده' : 'در انتظار تایید' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-grey">تاریخ :</div>
|
||||
<div class="font-semibold">{{ jdate(activeEntry.createdAt) }}</div>
|
||||
<div class="sheet__spec">
|
||||
<dt>تاریخ</dt>
|
||||
<dd>{{ jdate(activeEntry.createdAt) }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-grey">توضیحات :</div>
|
||||
<div class="font-semibold">{{ activeEntry.note || '- -' }}</div>
|
||||
<div class="sheet__spec">
|
||||
<dt>توضیحات</dt>
|
||||
<dd>{{ activeEntry.note || '—' }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</Drawer>
|
||||
</container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import AppHeader from '../../components/AppHeader.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import transactionEmpty from '../../assets/img/empty/transactionEmpty.svg'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import { jdate, price } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type LedgerEntrySummary } from '../../api/client'
|
||||
@@ -133,7 +120,6 @@ const SOURCE_LABELS: Record<string, string> = {
|
||||
ADJUSTMENT: 'اصلاحیه',
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -144,9 +130,6 @@ const activeEntry = ref<LedgerEntrySummary | null>(null)
|
||||
// legacy hasFactor(item) === item.type != 9 (9 = سفارش/order). Our DEBIT
|
||||
// entries are exactly that case.
|
||||
const isCredit = (item: LedgerEntrySummary) => item.direction === 'CREDIT'
|
||||
const getIcon = (item: LedgerEntrySummary) => (isCredit(item) ? 'vuesax-linear:add-square' : 'vuesax-linear:minus-square')
|
||||
const getColor = (item: LedgerEntrySummary) => (isCredit(item) ? '#F0FDF4' : '#FEF2F2')
|
||||
const getColor2 = (item: LedgerEntrySummary) => (isCredit(item) ? '#22C55E' : '#F44336')
|
||||
|
||||
const formatAmount = (item: LedgerEntrySummary) =>
|
||||
item.currency === 'GOLD_GRAM' ? `${item.amount} گرم` : `${price(item.amount)} ریال`
|
||||
@@ -166,3 +149,194 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tx {
|
||||
background: @canvas;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.tx__body {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.tx__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tx__row {
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.tx__row:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.tx__mark {
|
||||
flex: 0 0 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.tx__mark.is-in {
|
||||
color: @green;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.tx__mark.is-out {
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
|
||||
.tx__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tx__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tx__kind {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tx__amount {
|
||||
font-size: 12.5px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tx__amount.is-in {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.tx__amount.is-out {
|
||||
color: @wine;
|
||||
}
|
||||
|
||||
.tx__note {
|
||||
margin: 6px 0 0;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.7;
|
||||
color: @inkMuted;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tx__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tx__date {
|
||||
font-size: 11px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.tx__state {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.tx__state.is-pending {
|
||||
color: @notify2;
|
||||
}
|
||||
|
||||
.tx__empty {
|
||||
margin-top: 8px;
|
||||
padding: 34px 20px;
|
||||
border: 1px dashed @hairline;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
.tx__empty p {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
padding: 18px 16px 22px;
|
||||
}
|
||||
|
||||
.sheet__title {
|
||||
margin: 0 0 14px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.sheet__specs {
|
||||
margin: 0;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sheet__spec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid @hairlineSoft;
|
||||
}
|
||||
|
||||
.sheet__spec:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.sheet__spec dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.sheet__spec dd {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sheet__spec dd.is-in {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.sheet__spec dd.is-out {
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user