Initial commit: fabric-trade-user Vue 3 app with mobile and desktop layouts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 09:57:24 +02:00
co-authored by Cursor
commit fd3e5e5126
142 changed files with 19798 additions and 0 deletions
@@ -0,0 +1,723 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useOrderStore } from '@/stores/order'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import IconEditPen from '@/components/icons/IconEditPen.vue'
import IconOrders from '@/components/icons/IconOrders.vue'
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
import IconCash from '@/components/icons/IconCash.vue'
import IconBuilding from '@/components/icons/IconBuilding.vue'
import IconBolt from '@/components/icons/IconBolt.vue'
import IconSettings from '@/components/icons/IconSettings.vue'
import IconProfile from '@/components/icons/IconProfile.vue'
import IconBell from '@/components/icons/IconBell.vue'
import IconMapPin from '@/components/icons/IconMapPin.vue'
import IconHeadset from '@/components/icons/IconHeadset.vue'
import IconCashSettlement from '@/components/icons/IconCashSettlement.vue'
import IconPhone from '@/components/icons/IconPhone.vue'
import IconMail from '@/components/icons/IconMail.vue'
import IconVerified from '@/components/icons/IconVerified.vue'
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
import IconLock from '@/components/icons/IconLock.vue'
import IconIdCard from '@/components/icons/IconIdCard.vue'
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
import type { Component } from 'vue'
const router = useRouter()
const auth = useAuthStore()
const orderStore = useOrderStore()
const emit = defineEmits<{ logout: [] }>()
const initials = computed(() => {
const first = auth.user?.firstName?.[0] ?? ''
const last = auth.user?.lastName?.[0] ?? ''
return `${first}${last}` || '؟'
})
const ordersCount = computed(() => {
if (!auth.user) return 0
return orderStore.userOrders(auth.user.id).length
})
const checksCount = computed(() => {
if (!auth.user) return 0
const submitted = orderStore
.userOrders(auth.user.id)
.reduce((sum, order) => sum + (order.checks?.length ?? 0), 0)
return submitted + orderStore.draftChecks.length
})
const customerCode = computed(() => {
if (auth.user?.nationalId) return toPersianDigits(auth.user.nationalId.slice(-6).padStart(6, '0'))
return toPersianDigits(auth.user?.id.slice(-6) ?? '—')
})
const displayEmail = computed(() => {
const phone = auth.user?.phone ?? ''
if (!phone) return '—'
return `user${phone.slice(-4)}@parsbaft.ir`
})
const joinDate = computed(() =>
auth.user?.createdAt ? formatDate(auth.user.createdAt) : '—',
)
const creditBalance = computed(() => 2_450_000)
const companyRows = computed(() => [
{ label: 'نام شرکت', value: auth.fullName || '—' },
{ label: 'شناسه ملی', value: auth.user?.nationalId ? toPersianDigits(auth.user.nationalId) : '—' },
{
label: 'کد اقتصادی',
value: auth.user?.nationalId ? toPersianDigits(`4${auth.user.nationalId.slice(0, 10)}`) : '—',
},
{ label: 'تاریخ عضویت', value: joinDate.value },
{ label: 'نوع مشتری', value: 'مشتری عمده', badge: true },
{ label: 'وضعیت حساب', value: 'فعال', badge: true },
])
const quickAccess = [
{ label: 'حساب‌های بانکی', icon: IconCashSettlement },
{ label: 'آدرس‌ها', icon: IconMapPin },
{ label: 'حمایت و پشتیبانی', icon: IconHeadset, href: 'tel:02112345678' },
{ label: 'فاکتورهای من', icon: IconOrders, path: '/orders' },
]
const settingsItems: Array<{
id: string
label: string
description: string
icon: Component
path?: string
action?: 'logout'
}> = [
{
id: 'profile',
label: 'اطلاعات کاربری',
description: 'ویرایش نام، موبایل و ایمیل',
icon: IconProfile,
path: '/profile/edit',
},
{
id: 'security',
label: 'امنیت حساب',
description: 'تغییر رمز عبور و تنظیمات ورود',
icon: IconLock,
path: '/profile/edit',
},
{
id: 'notifications',
label: 'اعلان‌ها',
description: 'مدیریت اعلان‌های ایمیل و پیامک',
icon: IconBell,
path: '/profile/edit',
},
{
id: 'display',
label: 'تنظیمات نمایش',
description: 'زبان، واحد پول و حالت نمایش',
icon: IconLayoutGrid,
path: '/profile/edit',
},
]
function handleQuick(item: (typeof quickAccess)[number]) {
if (item.path) router.push(item.path)
else if (item.href) window.location.href = item.href
}
function handleSetting(item: (typeof settingsItems)[number]) {
if (item.action === 'logout') {
emit('logout')
return
}
if (item.path) router.push(item.path)
}
</script>
<template>
<div class="desktop-page desktop-profile">
<header class="desktop-profile__head">
<h1 class="desktop-profile__title">پروفایل</h1>
<p class="desktop-profile__subtitle">مدیریت اطلاعات حساب کاربری و تنظیمات</p>
</header>
<section class="profile-overview">
<div class="profile-overview__identity">
<div class="profile-overview__aside">
<div class="profile-overview__avatar">{{ initials }}</div>
<button
type="button"
class="profile-overview__edit"
@click="router.push('/profile/edit')"
>
<IconEditPen :size="16" />
ویرایش اطلاعات
</button>
</div>
<div class="profile-overview__details">
<div class="profile-overview__name-row">
<h2 class="profile-overview__name">{{ auth.fullName }}</h2>
<span class="profile-overview__verified" title="حساب تأیید شده">
<IconVerified :size="18" />
</span>
</div>
<p class="profile-overview__meta">
<IconIdCard :size="15" class="profile-overview__meta-icon" />
<span class="profile-overview__meta-label">کد مشتری:</span>
<span>{{ customerCode }}</span>
</p>
<p class="profile-overview__meta">
<IconPhone :size="15" class="profile-overview__meta-icon" />
<span dir="ltr">{{ auth.user?.phone }}</span>
</p>
<p class="profile-overview__meta">
<IconMail :size="15" class="profile-overview__meta-icon" />
<span dir="ltr">{{ displayEmail }}</span>
</p>
</div>
</div>
<div class="profile-overview__stats">
<article class="profile-stat">
<span class="profile-stat__icon profile-stat__icon--amber">
<IconCash :size="20" />
</span>
<strong class="profile-stat__value profile-stat__value--sm">
{{ formatCurrency(creditBalance) }}
</strong>
<span class="profile-stat__label">موجودی اعتبار</span>
<button type="button" class="profile-stat__link">مشاهده جزئیات</button>
</article>
<article class="profile-stat">
<span class="profile-stat__icon profile-stat__icon--green">
<IconCheckBook :size="20" />
</span>
<strong class="profile-stat__value">{{ formatNumber(checksCount) }}</strong>
<span class="profile-stat__label">چک فعال</span>
<button type="button" class="profile-stat__link" @click="router.push('/profile/checks')">
مشاهده همه
</button>
</article>
<article class="profile-stat">
<span class="profile-stat__icon profile-stat__icon--blue">
<IconOrders :size="20" />
</span>
<strong class="profile-stat__value">{{ formatNumber(ordersCount) }}</strong>
<span class="profile-stat__label">سفارش ثبت‌شده</span>
<button type="button" class="profile-stat__link" @click="router.push('/orders')">
مشاهده همه
</button>
</article>
</div>
</section>
<div class="desktop-profile__grid">
<section class="profile-panel profile-panel--settings">
<h3 class="profile-panel__title">
<IconSettings :size="18" />
تنظیمات حساب
</h3>
<nav class="profile-settings" aria-label="تنظیمات حساب">
<button
v-for="item in settingsItems"
:key="item.id"
type="button"
class="profile-settings__item"
@click="handleSetting(item)"
>
<span class="profile-settings__icon">
<component :is="item.icon" :size="20" />
</span>
<span class="profile-settings__body">
<strong class="profile-settings__label">{{ item.label }}</strong>
<span class="profile-settings__desc">{{ item.description }}</span>
</span>
<IconChevronLeft :size="16" class="profile-settings__chevron" aria-hidden="true" />
</button>
</nav>
</section>
<section class="profile-panel profile-panel--quick">
<h3 class="profile-panel__title">
<IconBolt :size="18" />
دسترسی سریع
</h3>
<div class="profile-quick-grid">
<button
v-for="item in quickAccess"
:key="item.label"
type="button"
class="profile-quick-grid__item"
@click="handleQuick(item)"
>
<span class="profile-quick-grid__icon">
<component :is="item.icon" :size="22" />
</span>
<span class="profile-quick-grid__label">{{ item.label }}</span>
</button>
</div>
</section>
<section class="profile-panel profile-panel--company">
<h3 class="profile-panel__title">
<IconBuilding :size="18" />
اطلاعات شرکت
</h3>
<dl class="profile-info-list">
<div
v-for="row in companyRows"
:key="row.label"
class="profile-info-list__row"
>
<dt>{{ row.label }}</dt>
<dd>
<span v-if="row.badge" class="profile-badge">{{ row.value }}</span>
<template v-else>{{ row.value }}</template>
</dd>
</div>
</dl>
</section>
</div>
</div>
</template>
<style scoped>
.desktop-profile {
max-width: 980px;
margin-inline: auto;
padding-bottom: 32px;
}
.desktop-profile__head {
margin-bottom: 20px;
}
.desktop-profile__title {
margin: 0 0 6px;
font-size: 1.375rem;
font-weight: 700;
color: var(--color-text);
}
.desktop-profile__subtitle {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.profile-overview {
direction: rtl;
display: flex;
align-items: stretch;
gap: 0;
padding: 22px 24px;
margin-bottom: 16px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.profile-overview__identity {
flex: 1.1;
display: flex;
align-items: flex-start;
gap: 18px;
padding-inline-end: 24px;
border-inline-end: 1px solid var(--color-border);
}
.profile-overview__aside {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.profile-overview__avatar {
width: 92px;
height: 92px;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.875rem;
font-weight: 700;
}
.profile-overview__edit {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 132px;
padding: 9px 14px;
border: 1px solid var(--color-primary);
border-radius: var(--radius-md);
background: #fff;
color: var(--color-primary);
font-family: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.profile-overview__edit:hover {
background: var(--color-primary-soft);
}
.profile-overview__details {
min-width: 0;
flex: 1;
padding-top: 6px;
}
.profile-overview__name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.profile-overview__name {
margin: 0;
font-size: 1.0625rem;
font-weight: 700;
color: var(--color-text);
}
.profile-overview__verified {
color: var(--color-success);
display: flex;
flex-shrink: 0;
}
.profile-overview__meta {
margin: 0 0 8px;
font-size: 0.8125rem;
color: var(--color-text-muted);
display: flex;
align-items: center;
gap: 8px;
}
.profile-overview__meta-icon {
color: #94a3b8;
flex-shrink: 0;
}
.profile-overview__meta-label {
color: var(--color-text);
font-weight: 600;
}
.profile-overview__stats {
direction: ltr;
flex: 1;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: stretch;
}
.profile-stat {
direction: rtl;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 12px 14px;
border-inline-start: 1px solid var(--color-border);
}
.profile-stat:first-child {
border-inline-start: none;
}
.profile-stat__icon {
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
.profile-stat__icon--blue {
background: var(--color-primary-soft);
color: var(--color-primary);
}
.profile-stat__icon--green {
background: var(--color-success-soft);
color: var(--color-success);
}
.profile-stat__icon--amber {
background: #fff6e5;
color: #d97706;
}
.profile-stat__value {
font-size: 1.375rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 4px;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.profile-stat__value--sm {
font-size: 0.875rem;
line-height: 1.45;
}
.profile-stat__label {
font-size: 0.75rem;
color: var(--color-text-muted);
margin-bottom: 8px;
}
.profile-stat__link {
border: none;
background: none;
padding: 0;
color: var(--color-primary);
font-family: inherit;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
}
.profile-stat__link:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.desktop-profile__grid {
direction: ltr;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
align-items: stretch;
}
.profile-panel {
direction: rtl;
padding: 18px 18px 16px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
}
.profile-panel__title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f5f9;
font-size: 0.9375rem;
font-weight: 700;
color: var(--color-text);
}
.profile-panel__title svg {
color: var(--color-primary);
flex-shrink: 0;
}
.profile-info-list {
margin: 0;
}
.profile-info-list__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid #f1f5f9;
font-size: 0.8125rem;
}
.profile-info-list__row:last-child {
border-bottom: none;
}
.profile-info-list__row dt {
color: var(--color-text-muted);
font-weight: 500;
}
.profile-info-list__row dd {
margin: 0;
color: var(--color-text);
font-weight: 600;
text-align: left;
}
.profile-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
background: var(--color-success-soft);
color: var(--color-success);
font-size: 0.75rem;
font-weight: 600;
}
.profile-quick-grid {
direction: ltr;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.profile-quick-grid__item {
direction: rtl;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 108px;
padding: 16px 10px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: #f8fafc;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, background 0.15s;
}
.profile-quick-grid__item:hover {
border-color: #c7d7ff;
background: var(--color-primary-soft);
}
.profile-quick-grid__icon {
width: 42px;
height: 42px;
border-radius: 12px;
background: #fff;
color: var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-sm);
}
.profile-quick-grid__label {
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text);
text-align: center;
line-height: 1.35;
}
.profile-settings {
display: flex;
flex-direction: column;
gap: 0;
}
.profile-settings__item {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 4px;
border: none;
border-bottom: 1px solid #f1f5f9;
border-radius: 0;
background: transparent;
cursor: pointer;
text-align: right;
font-family: inherit;
transition: background 0.15s;
}
.profile-settings__item:last-child {
border-bottom: none;
}
.profile-settings__item:hover {
background: #f8fafc;
}
.profile-settings__icon {
width: 38px;
height: 38px;
border-radius: 10px;
background: var(--color-primary-soft);
color: var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.profile-settings__body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.profile-settings__label {
font-size: 0.8125rem;
font-weight: 700;
color: var(--color-text);
}
.profile-settings__desc {
font-size: 0.6875rem;
color: var(--color-text-muted);
line-height: 1.45;
}
.profile-settings__chevron {
color: #cbd5e1;
flex-shrink: 0;
}
@media (max-width: 900px) {
.profile-overview {
flex-direction: column;
}
.profile-overview__identity {
border-inline-end: none;
padding-inline-end: 0;
padding-bottom: 16px;
border-bottom: 1px solid var(--color-border);
}
.profile-overview__stats {
padding-top: 8px;
}
.profile-stat:first-child {
border-inline-start: none;
}
.desktop-profile__grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,224 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useOrderStore } from '@/stores/order'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import IconProfile from '@/components/icons/IconProfile.vue'
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
import IconOrders from '@/components/icons/IconOrders.vue'
const router = useRouter()
const auth = useAuthStore()
const orderStore = useOrderStore()
const initials = computed(() => {
const first = auth.user?.firstName?.[0] ?? ''
const last = auth.user?.lastName?.[0] ?? ''
return `${first}${last}` || '؟'
})
const draftCheckCount = computed(() => orderStore.draftChecks.length)
const menuItems = computed(() => [
{
id: 'edit',
label: 'اطلاعات کاربری',
description: 'نام، نام خانوادگی و کد ملی',
path: '/profile/edit',
icon: IconProfile,
},
{
id: 'checks',
label: 'لیست چک‌ها',
description: 'چک‌های ثبت‌شده و رأس چک',
path: '/profile/checks',
icon: IconCheckBook,
badge: draftCheckCount.value,
},
{
id: 'orders',
label: 'سفارش‌های من',
description: 'پیگیری وضعیت سفارش‌ها',
path: '/orders',
icon: IconOrders,
},
])
const emit = defineEmits<{ logout: [] }>()
</script>
<template>
<div class="page profile-page">
<div class="profile-hero">
<div class="profile-hero__avatar">{{ initials }}</div>
<h1 class="profile-hero__name">{{ auth.fullName }}</h1>
<p class="profile-hero__phone" dir="ltr">{{ auth.user?.phone }}</p>
</div>
<div class="page__scroll page__scroll--with-nav profile-page__scroll">
<nav class="profile-menu" aria-label="منوی پروفایل">
<button
v-for="item in menuItems"
:key="item.id"
type="button"
class="profile-menu__item"
@click="router.push(item.path)"
>
<span class="profile-menu__icon">
<component :is="item.icon" :size="22" />
</span>
<span class="profile-menu__content">
<strong class="profile-menu__label">{{ item.label }}</strong>
<span class="profile-menu__desc">{{ item.description }}</span>
</span>
<span v-if="item.badge" class="profile-menu__badge">{{ item.badge }}</span>
<IconChevronLeft :size="18" class="profile-menu__chevron" />
</button>
</nav>
<button type="button" class="profile-logout" @click="emit('logout')">خروج از حساب</button>
</div>
</div>
</template>
<style scoped>
.profile-page {
overflow: hidden;
padding-top: 0;
background: var(--color-bg);
}
.profile-hero {
flex-shrink: 0;
margin: 0 calc(-1 * var(--page-padding));
padding: calc(28px + env(safe-area-inset-top, 0)) var(--page-padding) 32px;
background: linear-gradient(160deg, var(--color-primary) 0%, #2a4a73 100%);
text-align: center;
color: #fff;
}
.profile-hero__avatar {
width: 72px;
height: 72px;
margin: 0 auto 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
border: 2px solid rgba(255, 255, 255, 0.35);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
}
.profile-hero__name {
margin: 0 0 6px;
font-size: 1.125rem;
font-weight: 700;
}
.profile-hero__phone {
margin: 0;
font-size: 0.875rem;
opacity: 0.9;
}
.profile-page__scroll {
margin-top: -18px;
padding-top: 0;
}
.profile-menu {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.profile-menu__item {
display: grid;
grid-template-columns: 44px 1fr auto auto;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 16px;
border: none;
border-radius: 16px;
background: var(--color-surface);
box-shadow: var(--shadow-sm);
cursor: pointer;
text-align: right;
transition: box-shadow 0.15s;
}
.profile-menu__item:hover {
box-shadow: var(--shadow-md);
}
.profile-menu__icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--color-primary-light);
color: var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
}
.profile-menu__content {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.profile-menu__label {
font-size: 0.9375rem;
font-weight: 700;
color: var(--color-text);
}
.profile-menu__desc {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.profile-menu__badge {
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
background: var(--color-danger);
color: #fff;
font-size: 0.6875rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.profile-menu__chevron {
color: var(--color-text-muted);
transform: scaleX(-1);
}
.profile-logout {
width: 100%;
height: var(--btn-height);
border: 1px solid #fecaca;
border-radius: 14px;
background: #fff;
color: var(--color-danger);
font-family: inherit;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
margin-bottom: 8px;
}
.profile-logout:hover {
background: var(--color-danger-light);
}
</style>