Initialize the user app and load home banners from the API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterView, useRouter } from 'vue-router'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { clamp } from 'lodash-es'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const router = useRouter()
|
||||
|
||||
const transitionName = ref('page-fade')
|
||||
|
||||
function routeDepth(path: string) {
|
||||
return path.split('/').filter(Boolean).length
|
||||
}
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (!from.name) {
|
||||
transitionName.value = 'page-fade'
|
||||
return
|
||||
}
|
||||
|
||||
const delta = clamp(routeDepth(to.path) - routeDepth(from.path), -1, 1)
|
||||
if (delta > 0) transitionName.value = 'page-forward'
|
||||
else if (delta < 0) transitionName.value = 'page-back'
|
||||
else transitionName.value = 'page-fade'
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.hydrate()
|
||||
if (authStore.isAuthenticated) {
|
||||
await Promise.all([
|
||||
catalogStore.loadCompanies(),
|
||||
cartStore.hydrate(),
|
||||
orderStore.hydrate(),
|
||||
notificationStore.hydrate(),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
// خبرهای سفارش از سمت ادمین میآید، پس شمارندهٔ زنگ بهآرامی تازه میشود
|
||||
useIntervalFn(
|
||||
() => {
|
||||
if (authStore.isAuthenticated) void notificationStore.refreshUnreadCount()
|
||||
},
|
||||
60_000,
|
||||
{ immediate: false },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="transitionName" mode="out-in">
|
||||
<div :key="route.path" class="page-transition page-shell">
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</DefaultLayout>
|
||||
<ConfirmDialog />
|
||||
</template>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,209 @@
|
||||
/* Application shell — global (unscoped) layout breakpoints */
|
||||
|
||||
.app-shell {
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background: var(--color-shell-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding-inline: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-transition {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Mobile: full-width native app (< 768px) ── */
|
||||
.app-shell--with-nav {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.app-shell--with-nav .app-frame {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── Tablet + Desktop: sidebar + header shell (>= 768px) ── */
|
||||
@media (min-width: 768px) {
|
||||
.app-shell--with-nav {
|
||||
background: var(--color-page-background);
|
||||
}
|
||||
|
||||
.app-shell--with-nav .app-frame {
|
||||
background: var(--color-page-background);
|
||||
}
|
||||
|
||||
.app-shell--with-nav .app-layout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* One continuous divider under sidebar brand + top header */
|
||||
.app-shell--with-nav .app-layout::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: var(--header-height);
|
||||
inset-inline: 0;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scroll lives on app-main__content. Children must not shrink+clip
|
||||
* (flex + overflow:hidden), or short viewports hide the bottom of pages.
|
||||
*/
|
||||
.app-shell--with-nav .app-main__content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.app-shell--with-nav .app-content,
|
||||
.app-shell--with-nav .page-transition {
|
||||
flex: 1 0 auto;
|
||||
min-height: min-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Let route roots grow with content so the shell scroll area can work */
|
||||
.app-shell--with-nav .page-transition > * {
|
||||
flex: 1 0 auto;
|
||||
min-height: min-content;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Guest pages (login, register, checkout) — all breakpoints ── */
|
||||
.app-shell--no-nav {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: var(--color-page-background);
|
||||
}
|
||||
|
||||
.app-shell--no-nav .app-layout {
|
||||
max-width: var(--form-max-width);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100dvh - 32px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell--no-nav .app-frame {
|
||||
max-width: var(--form-max-width);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100dvh - 32px);
|
||||
min-height: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-frame);
|
||||
background: var(--color-surface);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.app-shell--no-nav .app-main,
|
||||
.app-shell--no-nav .app-main__content,
|
||||
.app-shell--no-nav .app-content,
|
||||
.app-shell--no-nav .page-transition {
|
||||
flex: 1 0 auto;
|
||||
min-height: min-content;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.app-shell--no-nav .app-content {
|
||||
padding-inline: var(--page-padding);
|
||||
}
|
||||
|
||||
.app-shell--no-nav .page {
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.app-shell--no-nav.app-shell--wide .app-layout,
|
||||
.app-shell--no-nav.app-shell--wide .app-frame {
|
||||
max-width: min(var(--content-max-width), calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.app-shell--no-nav.app-shell--wide .app-frame {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.app-shell--no-nav {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.app-shell--no-nav .app-layout,
|
||||
.app-shell--no-nav .app-frame {
|
||||
max-height: calc(100dvh - 48px);
|
||||
}
|
||||
|
||||
.app-shell--no-nav.app-shell--wide .app-layout,
|
||||
.app-shell--no-nav.app-shell--wide .app-frame {
|
||||
max-width: min(var(--content-max-width), calc(100vw - 48px));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/* Desktop page composition — >= 768px shell, >= 1200px full desktop */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-page {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: auto;
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.desktop-page__head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.desktop-page__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-page__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.desktop-two-col {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.desktop-two-col__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desktop-summary-panel {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
padding: 20px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.desktop-summary-panel__title {
|
||||
margin: 0 0 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row span:first-child {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.desktop-summary-panel__row strong {
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__action {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.desktop-table-wrap {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.desktop-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
min-width: 560px;
|
||||
}
|
||||
|
||||
.desktop-table th {
|
||||
padding: 12px 16px;
|
||||
text-align: start;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
vertical-align: middle;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.desktop-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.desktop-table tbody tr:hover td {
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.desktop-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.desktop-grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.desktop-grid-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.desktop-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.desktop-toolbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-btn-compact {
|
||||
width: auto !important;
|
||||
min-width: 120px;
|
||||
padding-inline: 20px !important;
|
||||
}
|
||||
|
||||
.desktop-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.desktop-form-grid__full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.desktop-checkout-shell {
|
||||
width: 100%;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.desktop-settlement-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(280px, 380px));
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
max-width: 820px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.desktop-company-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(280px, 400px));
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
max-width: 900px;
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-page {
|
||||
padding: 24px var(--page-padding) 32px;
|
||||
}
|
||||
|
||||
.desktop-checkout-shell {
|
||||
padding: 24px var(--page-padding) 32px;
|
||||
}
|
||||
|
||||
.desktop-page__head {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.desktop-two-col:has(> .desktop-summary-panel) {
|
||||
grid-template-columns: minmax(0, 1fr) var(--summary-panel-width, 320px);
|
||||
}
|
||||
|
||||
.desktop-grid-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.desktop-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.desktop-grid-3 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
/* App shell page layouts — 768px and above */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--page-padding: var(--page-padding-tablet);
|
||||
--nav-height: 0px;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 20px var(--page-padding-tablet) 28px;
|
||||
background: transparent;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
min-height: auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.page__scroll {
|
||||
overflow: visible;
|
||||
flex: none;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.page__scroll--with-nav {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Shared page headings */
|
||||
.page-head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-head__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-head__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Orders dashboard */
|
||||
.orders-page__desktop-head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.orders-page__desktop-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-page__desktop-subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.orders-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.orders-stat-card {
|
||||
padding: 16px 18px 14px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.orders-stat-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.orders-stat-card__label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.orders-stat-card__label--total,
|
||||
.orders-stat-card__label--review {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-stat-card__label--active {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.orders-stat-card__label--done {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.orders-stat-card__value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1.15;
|
||||
margin: 0 0 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.orders-stat-card__link {
|
||||
margin-top: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.orders-stat-card__link:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.orders-stat-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orders-stat-card__icon--total {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.orders-stat-card__icon--review {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.orders-stat-card__icon--active {
|
||||
background: var(--color-warning-light);
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.orders-stat-card__icon--done {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.orders-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orders-toolbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orders-toolbar__filter-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-toolbar__filter-btn:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.orders-toolbar__pills {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.orders-filter-pill {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.orders-filter-pill:hover {
|
||||
border-color: #cbd5e1;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-filter-pill--active {
|
||||
background: var(--color-primary-soft);
|
||||
border-color: #c7d7ff;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.orders-table-wrap {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.orders-table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
.orders-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: start;
|
||||
font-weight: 600;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-table td {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
vertical-align: middle;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.orders-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.orders-table tbody tr:hover td {
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.orders-table__number {
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-table__amount {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-table__date {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-table__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.orders-table__action {
|
||||
padding: 7px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-table__action:hover {
|
||||
background: var(--color-primary-soft);
|
||||
border-color: #c7d7ff;
|
||||
}
|
||||
|
||||
.orders-table__menu {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.orders-table__menu:hover {
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orders-pagination__summary {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders-pagination__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.orders-pagination__arrow {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-pagination__arrow:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.orders-pagination__arrow:not(:disabled):hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.orders-pagination__arrow-icon--prev {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.orders-pagination__arrow-icon--next {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.orders-pagination__ellipsis {
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.orders-pagination__page {
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.orders-pagination__page:hover:not(.orders-pagination__page--active) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.orders-pagination__page--active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Product page desktop layout */
|
||||
.product-page__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.product-page__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-page__sidebar {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.cart-summary-panel {
|
||||
padding: 20px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.cart-summary-panel__title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cart-summary-panel__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.cart-summary-panel__row span:first-child {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-summary-panel__row strong {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-summary-panel__btn {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.product-page__scroll {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.product-page .sticky-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
/* Cart desktop */
|
||||
.cart-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding-tablet) 28px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cart-page__desktop-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-page__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 320px);
|
||||
gap: 20px 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cart-page__company {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Order detail desktop */
|
||||
.order-detail-page {
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
padding: 24px var(--page-padding) 32px;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.order-detail-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Company select desktop */
|
||||
.company-select-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 24px var(--page-padding-desktop);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Catalog desktop */
|
||||
.catalog-page {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
/* Profile desktop */
|
||||
.profile-page {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.profile-hero {
|
||||
border-radius: var(--radius-md);
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
:root {
|
||||
--page-padding: var(--page-padding-desktop);
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 24px var(--page-padding-desktop) 32px;
|
||||
}
|
||||
|
||||
.orders-stats {
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.orders-page__desktop-head {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.orders-toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.product-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum).woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_UltraLight.woff2') format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_Medium.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IRANSans';
|
||||
src: url('../fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
:root {
|
||||
/* Brand — sampled from mockups/11-my-profile + FAB/CTA screens */
|
||||
--color-primary: #0a353c;
|
||||
--color-primary-hover: #072a2f;
|
||||
--color-primary-soft: #e8f4f1;
|
||||
--color-primary-dark: #062428;
|
||||
--color-primary-light: #e8f4f1;
|
||||
--color-primary-rgb: 10, 53, 60;
|
||||
--color-accent-navy: #1b223c;
|
||||
--color-accent-navy-rgb: 27, 34, 60;
|
||||
--color-success: #1b8a5a;
|
||||
--color-success-light: #eaf8f1;
|
||||
--color-success-soft: #eaf8f1;
|
||||
--color-warning: #e8a317;
|
||||
--color-warning-light: #fff6e5;
|
||||
--color-danger: #dc3545;
|
||||
--color-danger-light: #fee2e2;
|
||||
--color-purple: #7c3aed;
|
||||
--color-purple-light: #ede9fe;
|
||||
--color-info: #3b6fa0;
|
||||
--color-info-light: #eef3f7;
|
||||
--color-bg: #f6f8f7;
|
||||
--color-page-background: #f6f8f7;
|
||||
--color-shell-bg: #eef1f0;
|
||||
--color-surface: #ffffff;
|
||||
--color-border: #e5e9e8;
|
||||
--color-text: #121212;
|
||||
--color-text-primary: #121212;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-text-secondary: #6b7280;
|
||||
--color-tab-inactive-bg: #f1f5f9;
|
||||
--color-tab-inactive-text: #64748b;
|
||||
--color-summary-bg: #f1f5f9;
|
||||
--color-elevated: #ffffff;
|
||||
--color-brand-warm: #f3ece3;
|
||||
--color-brand-warm-text: #1a202c;
|
||||
--color-brand-warm-muted: #718096;
|
||||
--color-level-gold: #c9a227;
|
||||
--color-level-gold-soft: #f7efd6;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
--shadow-md: 0 4px 16px rgba(16, 24, 40, 0.06);
|
||||
--shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04), 0 4px 16px rgba(16, 24, 40, 0.04);
|
||||
--shadow-frame: 0 8px 32px rgba(15, 23, 42, 0.1);
|
||||
--nav-height: 64px;
|
||||
--page-padding: 16px;
|
||||
--page-padding-tablet: 20px;
|
||||
--page-padding-desktop: 28px;
|
||||
--page-gap: 12px;
|
||||
--content-max-width: 1360px;
|
||||
--form-max-width: 520px;
|
||||
--auth-form-max-width: 440px;
|
||||
--summary-panel-width: 320px;
|
||||
--sidebar-width: 240px;
|
||||
--sidebar-width-compact: 72px;
|
||||
--header-height: 70px;
|
||||
--app-shell-breakpoint: 768px;
|
||||
--font-family: 'IRANSans', 'Tahoma', sans-serif;
|
||||
--btn-height: 48px;
|
||||
--desktop-breakpoint: 1200px;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--color-primary: #5eb8ad;
|
||||
--color-primary-hover: #7ad0c5;
|
||||
--color-primary-soft: #14353a;
|
||||
--color-primary-dark: #7ad0c5;
|
||||
--color-primary-light: #14353a;
|
||||
--color-primary-rgb: 94, 184, 173;
|
||||
--color-accent-navy: #9bb0d0;
|
||||
--color-accent-navy-rgb: 155, 176, 208;
|
||||
--color-success: #3dd68c;
|
||||
--color-success-light: #123528;
|
||||
--color-success-soft: #123528;
|
||||
--color-warning: #f5c14a;
|
||||
--color-warning-light: #3a2f14;
|
||||
--color-danger: #f47979;
|
||||
--color-danger-light: #3f1d22;
|
||||
--color-purple: #b39afa;
|
||||
--color-purple-light: #2a2144;
|
||||
--color-info: #7b9cff;
|
||||
--color-info-light: #1b2740;
|
||||
--color-bg: #0b0f14;
|
||||
--color-page-background: #0b0f14;
|
||||
--color-shell-bg: #080b10;
|
||||
--color-surface: #121821;
|
||||
--color-elevated: #1a2230;
|
||||
--color-border: #2a3444;
|
||||
--color-text: #eef2f8;
|
||||
--color-text-primary: #eef2f8;
|
||||
--color-text-muted: #9aa8bc;
|
||||
--color-text-secondary: #9aa8bc;
|
||||
--color-tab-inactive-bg: #1a2230;
|
||||
--color-tab-inactive-text: #9aa8bc;
|
||||
--color-summary-bg: #1a2230;
|
||||
--color-brand-warm: #2a241c;
|
||||
--color-brand-warm-text: #f3ebe0;
|
||||
--color-brand-warm-muted: #b9a88d;
|
||||
--color-level-gold: #e0c35a;
|
||||
--color-level-gold-soft: #3a3420;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
|
||||
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
--shadow-frame: 0 12px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-shell-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
direction: rtl;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.page__scroll--with-nav {
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 4px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--color-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.auth-page .card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.card + .card {
|
||||
margin-top: var(--page-gap);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: var(--color-warning-light);
|
||||
color: #9a3412;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--color-success-light);
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: var(--color-danger-light);
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sticky-footer {
|
||||
position: fixed;
|
||||
bottom: calc(var(--nav-height) + 12px);
|
||||
inset-inline: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
z-index: 50;
|
||||
padding-inline: var(--page-padding);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sticky-footer-inner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.no-nav .page {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.no-nav .sticky-footer {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.dialog-overlay .dialog {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Responsive visibility */
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-hidden {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.desktop-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-only,
|
||||
.mobile-block {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.mobile-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.desktop-hidden {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.desktop-only-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.desktop-only-grid {
|
||||
display: grid !important;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.page__scroll--with-nav {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Page route transitions */
|
||||
.page-fade-enter-active,
|
||||
.page-fade-leave-active,
|
||||
.page-forward-enter-active,
|
||||
.page-forward-leave-active,
|
||||
.page-back-enter-active,
|
||||
.page-back-leave-active {
|
||||
transition:
|
||||
opacity 0.22s ease,
|
||||
transform 0.22s ease;
|
||||
}
|
||||
|
||||
.page-fade-enter-from,
|
||||
.page-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.page-forward-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-18px);
|
||||
}
|
||||
|
||||
.page-forward-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
.page-back-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.page-back-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.page-fade-enter-active,
|
||||
.page-fade-leave-active,
|
||||
.page-forward-enter-active,
|
||||
.page-forward-leave-active,
|
||||
.page-back-enter-active,
|
||||
.page-back-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/* Responsive breakpoints:
|
||||
Mobile: < 768px
|
||||
Tablet: 768px – 1199px
|
||||
Desktop: >= 1200px
|
||||
Large desktop: >= 1440px
|
||||
*/
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--page-padding: var(--page-padding-tablet);
|
||||
--nav-height: 0px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sticky-footer {
|
||||
bottom: 0;
|
||||
max-width: min(var(--content-max-width), calc(100% - 2 * var(--page-padding)));
|
||||
}
|
||||
|
||||
.no-nav .sticky-footer {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Auth pages */
|
||||
.auth-page__scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 48px;
|
||||
}
|
||||
|
||||
.auth-header,
|
||||
.auth-page .card,
|
||||
.auth-footer {
|
||||
width: 100%;
|
||||
max-width: var(--form-max-width);
|
||||
}
|
||||
|
||||
/* Company selection */
|
||||
.company-select-page__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.company-brand-card {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
/* Catalog & product grids */
|
||||
.catalog-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--page-gap);
|
||||
}
|
||||
|
||||
.product-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--page-gap);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.product-page__scroll {
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
|
||||
/* Settlement & check mode */
|
||||
.settlement-options,
|
||||
.mode-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 720px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Company page container */
|
||||
.company-select-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding-inline: var(--page-padding);
|
||||
}
|
||||
|
||||
.company-select-page__header {
|
||||
max-width: 560px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Catalog & orders page containers */
|
||||
.catalog-page .page__scroll,
|
||||
.orders-page__scroll,
|
||||
.product-page .product-page__scroll {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Cart two-column layout */
|
||||
.cart-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding-inline: var(--page-padding);
|
||||
}
|
||||
|
||||
.cart-page__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 300px);
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 16px 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cart-page__company {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 0.9375rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cart-page__scroll {
|
||||
grid-column: 1;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.cart-page__footer {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / -1;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
border-top: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cart-table__head {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* Check list */
|
||||
.check-list-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding-inline: var(--page-padding);
|
||||
}
|
||||
|
||||
.check-list-page__scroll {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.checks-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.rass-panel {
|
||||
max-width: 480px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Profile */
|
||||
.profile-page__scroll {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-menu__item {
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Order detail & tracking */
|
||||
.order-detail-page__body {
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.order-detail-page__footer {
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Checkout forms */
|
||||
.checkout-page__scroll,
|
||||
.cash-page__scroll,
|
||||
.settlement-page__scroll,
|
||||
.check-mode-page__scroll {
|
||||
max-width: var(--form-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Cash settlement desktop */
|
||||
.cash-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cash-page__footer {
|
||||
max-width: var(--form-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
border-top: none;
|
||||
box-shadow: none;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.accounts-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bank-card + .bank-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cash-page__submit {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.order-info-card {
|
||||
max-width: 520px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Profile edit */
|
||||
.profile-edit-page .page__scroll {
|
||||
max-width: var(--form-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Success page */
|
||||
.success-page__scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: var(--form-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Dialogs */
|
||||
.dialog-overlay .dialog,
|
||||
.dialog-overlay .card.dialog {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
:root {
|
||||
--page-padding: var(--page-padding-desktop);
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.checks-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.product-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
:root {
|
||||
--content-max-width: 1120px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import icons from '@/components/BasicPacks/tools/icons/icons.json'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
icon: string
|
||||
color?: string | null
|
||||
size?: string | number
|
||||
strokeWidth?: string | number
|
||||
}>(),
|
||||
{
|
||||
color: null,
|
||||
size: 24,
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
)
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const svgHtml = computed(() => {
|
||||
const raw = (icons as Record<string, string>)[props.icon]
|
||||
if (!raw) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`[BasicIcon] missing icon: ${props.icon}`)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const size = String(props.size)
|
||||
return raw
|
||||
.replace(/\swidth="[^"]*"/, ` width="${size}"`)
|
||||
.replace(/\sheight="[^"]*"/, ` height="${size}"`)
|
||||
.replace(/stroke="#[^"]*"/g, 'stroke="currentColor"')
|
||||
})
|
||||
|
||||
const style = computed(() => ({
|
||||
color: props.color || undefined,
|
||||
width: `${props.size}px`,
|
||||
height: `${props.size}px`,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 0,
|
||||
flexShrink: 0,
|
||||
}))
|
||||
const ariaHidden = computed(() => {
|
||||
const value = attrs['aria-hidden']
|
||||
if (value === undefined || value === null) return true
|
||||
return value as boolean | 'true' | 'false'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="basic-icon"
|
||||
v-bind="attrs"
|
||||
:style="style"
|
||||
:aria-hidden="ariaHidden"
|
||||
v-html="svgHtml"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.basic-icon :deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.basic-icon {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.basic-icon :deep(svg path) {
|
||||
stroke: currentColor;
|
||||
stroke-width: v-bind('String(props.strokeWidth)');
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"vuesax-linear:add": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M6 12H18\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 18V6\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:arrow-down": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M19.9201 8.94995L13.4001 15.47C12.6301 16.24 11.3701 16.24 10.6001 15.47L4.08008 8.94995\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:arrow-left-1": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M15 19.9201L8.47997 13.4001C7.70997 12.6301 7.70997 11.3701 8.47997 10.6001L15 4.08008\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:arrow-right-1": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.91003 19.9201L15.43 13.4001C16.2 12.6301 16.2 11.3701 15.43 10.6001L8.91003 4.08008\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:bag-2": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M7.5 7.67001V6.70001C7.5 4.45001 9.31 2.24001 11.56 2.03001C14.24 1.77001 16.5 3.88001 16.5 6.51001V7.89001\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.99999 22H15C19.02 22 19.74 20.39 19.95 18.43L20.7 12.43C20.97 9.99 20.27 8 16 8H7.99999C3.72999 8 3.02999 9.99 3.29999 12.43L4.04999 18.43C4.25999 20.39 4.97999 22 8.99999 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.4955 12H15.5045\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.49451 12H8.50349\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:bag-cross": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M13.39 17.36L10.64 14.61\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M13.36 14.64L10.61 17.39\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.81 2L5.19 5.63\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.19 2L18.81 5.63\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 7.84998C2 5.99998 2.99 5.84998 4.22 5.84998H19.78C21.01 5.84998 22 5.99998 22 7.84998C22 9.99998 21.01 9.84998 19.78 9.84998H4.22C2.99 9.84998 2 9.99998 2 7.84998Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n<path d=\"M3.5 10L4.91 18.64C5.23 20.58 6 22 8.86 22H14.89C18 22 18.46 20.64 18.82 18.76L20.5 10\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:box-1": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M3.17004 7.43994L12 12.5499L20.77 7.46994\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 21.61V12.54\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9.93001 2.48004L4.59001 5.44004C3.38001 6.11004 2.39001 7.79004 2.39001 9.17004V14.82C2.39001 16.2 3.38001 17.88 4.59001 18.55L9.93001 21.52C11.07 22.15 12.94 22.15 14.08 21.52L19.42 18.55C20.63 17.88 21.62 16.2 21.62 14.82V9.17004C21.62 7.79004 20.63 6.11004 19.42 5.44004L14.08 2.47004C12.93 1.84004 11.07 1.84004 9.93001 2.48004Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:box-remove": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M3.17004 7.43994L12 12.5499L20.77 7.46991\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 21.6099V12.5399\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21.61 9.17V14.83C21.61 14.88 21.61 14.92 21.6 14.97C20.9 14.36 20 14 19 14C18.06 14 17.19 14.33 16.5 14.88C15.58 15.61 15 16.74 15 18C15 18.75 15.21 19.46 15.58 20.06C15.67 20.22 15.78 20.37 15.9 20.51L14.07 21.52C12.93 22.16 11.07 22.16 9.92999 21.52L4.59 18.56C3.38 17.89 2.39001 16.21 2.39001 14.83V9.17C2.39001 7.79 3.38 6.11002 4.59 5.44002L9.92999 2.48C11.07 1.84 12.93 1.84 14.07 2.48L19.41 5.44002C20.62 6.11002 21.61 7.79 21.61 9.17Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M23 18C23 18.75 22.79 19.46 22.42 20.06C22.21 20.42 21.94 20.74 21.63 21C20.93 21.63 20.01 22 19 22C17.54 22 16.27 21.22 15.58 20.06C15.21 19.46 15 18.75 15 18C15 16.74 15.58 15.61 16.5 14.88C17.19 14.33 18.06 14 19 14C21.21 14 23 15.79 23 18Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20.07 19.0399L17.95 16.9299\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20.05 16.96L17.93 19.0699\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:box-search": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M3.17004 7.43994L12 12.5499L20.77 7.46991\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 21.6099V12.5399\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21.61 12.83V9.17C21.61 7.79 20.62 6.11002 19.41 5.44002L14.07 2.48C12.93 1.84 11.07 1.84 9.92999 2.48L4.59 5.44002C3.38 6.11002 2.39001 7.79 2.39001 9.17V14.83C2.39001 16.21 3.38 17.89 4.59 18.56L9.92999 21.52C10.5 21.84 11.25 22 12 22C12.75 22 13.5 21.84 14.07 21.52\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M19.2 21.4C20.9673 21.4 22.4 19.9673 22.4 18.2C22.4 16.4327 20.9673 15 19.2 15C17.4327 15 16 16.4327 16 18.2C16 19.9673 17.4327 21.4 19.2 21.4Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M23 22L22 21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:building": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12.5 22.0001H4.07997C2.91997 22.0001 1.96997 21.0701 1.96997 19.9301V5.09011C1.96997 2.47011 3.91997 1.2801 6.30997 2.4501L10.75 4.63011C11.71 5.10011 12.5 6.3501 12.5 7.4101V22.0001Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21.97 15.0602V18.8402C21.97 21.0002 20.97 22.0002 18.81 22.0002H12.5V10.4202L12.97 10.5202L17.47 11.5302L19.5 11.9802C20.82 12.2702 21.9 12.9502 21.96 14.8702C21.97 14.9302 21.97 14.9902 21.97 15.0602Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.5 9H8.97\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.5 13H8.97\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17.47 11.53V14.75C17.47 15.99 16.46 17 15.22 17C13.98 17 12.97 15.99 12.97 14.75V10.52L17.47 11.53Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21.96 14.87C21.9 16.05 20.92 17 19.72 17C18.48 17 17.47 15.99 17.47 14.75V11.53L19.5 11.98C20.82 12.27 21.9 12.95 21.96 14.87Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:buildings": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M13 22H5C3 22 2 21 2 19V11C2 9 3 8 5 8H10V19C10 21 11 22 13 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.11 4C10.03 4.3 10 4.63 10 5V8H5V6C5 4.9 5.9 4 7 4H10.11Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M14 8V13\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18 8V13\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 17H15C14.45 17 14 17.45 14 18V22H18V18C18 17.45 17.55 17 17 17Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6 13V17\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10 19V5C10 3 11 2 13 2H19C21 2 22 3 22 5V19C22 21 21 22 19 22H13C11 22 10 21 10 19Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:calendar": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8 2V5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 2V5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M3.5 9.08997H20.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.6947 13.7H15.7037\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.6947 16.7H15.7037\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.9955 13.7H12.0045\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.9955 16.7H12.0045\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.29431 13.7H8.30329\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.29431 16.7H8.30329\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:call": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21.97 18.33C21.97 18.69 21.89 19.06 21.72 19.42C21.55 19.78 21.33 20.12 21.04 20.44C20.55 20.98 20.01 21.37 19.4 21.62C18.8 21.87 18.15 22 17.45 22C16.43 22 15.34 21.76 14.19 21.27C13.04 20.78 11.89 20.12 10.75 19.29C9.6 18.45 8.51 17.52 7.47 16.49C6.44 15.45 5.51 14.36 4.68 13.22C3.86 12.08 3.2 10.94 2.72 9.81C2.24 8.67 2 7.58 2 6.54C2 5.86 2.12 5.21 2.36 4.61C2.6 4 2.98 3.44 3.51 2.94C4.15 2.31 4.85 2 5.59 2C5.87 2 6.15 2.06 6.4 2.18C6.66 2.3 6.89 2.48 7.07 2.74L9.39 6.01C9.57 6.26 9.7 6.49 9.79 6.71C9.88 6.92 9.93 7.13 9.93 7.32C9.93 7.56 9.86 7.8 9.72 8.03C9.59 8.26 9.4 8.5 9.16 8.74L8.4 9.53C8.29 9.64 8.24 9.77 8.24 9.93C8.24 10.01 8.25 10.08 8.27 10.16C8.3 10.24 8.33 10.3 8.35 10.36C8.53 10.69 8.84 11.12 9.28 11.64C9.73 12.16 10.21 12.69 10.73 13.22C11.27 13.75 11.79 14.24 12.32 14.69C12.84 15.13 13.27 15.43 13.61 15.61C13.66 15.63 13.72 15.66 13.79 15.69C13.87 15.72 13.95 15.73 14.04 15.73C14.21 15.73 14.34 15.67 14.45 15.56L15.21 14.81C15.46 14.56 15.7 14.37 15.93 14.25C16.16 14.11 16.39 14.04 16.64 14.04C16.83 14.04 17.03 14.08 17.25 14.17C17.47 14.26 17.7 14.39 17.95 14.56L21.26 16.91C21.52 17.09 21.7 17.3 21.81 17.55C21.91 17.8 21.97 18.05 21.97 18.33Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\"/>\n</svg>\n",
|
||||
"vuesax-linear:card": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M2 8.50488H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6 16.5049H8\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.5 16.5049H14.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6.44 3.50488H17.55C21.11 3.50488 22 4.38488 22 7.89488V16.1049C22 19.6149 21.11 20.4949 17.56 20.4949H6.44C2.89 20.5049 2 19.6249 2 16.1149V7.89488C2 4.38488 2.89 3.50488 6.44 3.50488Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:card-receive": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M2 8.5H14.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6 16.5H8\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.5 16.5H14.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 14.03V16.11C22 19.62 21.11 20.5 17.56 20.5H6.44C2.89 20.5 2 19.62 2 16.11V7.89C2 4.38 2.89 3.5 6.44 3.5H14.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20 3.5V9.5L22 7.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20 9.5L18 7.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:category": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M5 10H7C9 10 10 9 10 7V5C10 3 9 2 7 2H5C3 2 2 3 2 5V7C2 9 3 10 5 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 10H19C21 10 22 9 22 7V5C22 3 21 2 19 2H17C15 2 14 3 14 5V7C14 9 15 10 17 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 22H19C21 22 22 21 22 19V17C22 15 21 14 19 14H17C15 14 14 15 14 17V19C14 21 15 22 17 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5 22H7C9 22 10 21 10 19V17C10 15 9 14 7 14H5C3 14 2 15 2 17V19C2 21 3 22 5 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:chart-2": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.5 18.5C16.6 18.5 17.5 17.6 17.5 16.5V7.5C17.5 6.4 16.6 5.5 15.5 5.5C14.4 5.5 13.5 6.4 13.5 7.5V16.5C13.5 17.6 14.39 18.5 15.5 18.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 18.5C9.6 18.5 10.5 17.6 10.5 16.5V13C10.5 11.9 9.6 11 8.5 11C7.4 11 6.5 11.9 6.5 13V16.5C6.5 17.6 7.39 18.5 8.5 18.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:clipboard-close": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M14 16.16L10.04 12.2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M13.96 12.24L10 16.2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10 6H14C16 6 16 5 16 4C16 2 15 2 14 2H10C9 2 8 2 8 4C8 6 9 6 10 6Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 4.02002C19.33 4.20002 21 5.43002 21 10V16C21 20 20 22 15 22H9C4 22 3 20 3 16V10C3 5.44002 4.67 4.20002 8 4.02002\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:clock": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 9.66003V12.45L13.4 13.85\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.5 12C5.5 8.41 8.41 5.5 12 5.5C15.59 5.5 18.5 8.41 18.5 12C18.5 14.08 17.52 15.94 16 17.13H15.99C14.89 17.99 13.51 18.5 12 18.5C10.51 18.5 9.14 18 8.04 17.15H8.03C6.49 15.96 5.5 14.1 5.5 12Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.03003 17.15H8.04003C9.14003 18 10.51 18.5 12 18.5C13.51 18.5 14.89 17.99 15.99 17.13H16L15.49 19.6C15 21.5 13.9 22 12.55 22H11.46C10.11 22 9.00003 21.5 8.52003 19.59L8.03003 17.15Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.03003 6.85H8.04003C9.14003 6 10.51 5.5 12 5.5C13.51 5.5 14.89 6.01 15.99 6.87H16L15.49 4.4C15 2.5 13.9 2 12.55 2H11.46C10.11 2 9.00003 2.5 8.52003 4.41L8.03003 6.85Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:close-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9.16998 14.83L14.83 9.17004\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M14.83 14.83L9.16998 9.17004\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:cpu": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M9.6 20H14.4C18.4 20 20 18.4 20 14.4V9.6C20 5.6 18.4 4 14.4 4H9.6C5.6 4 4 5.6 4 9.6V14.4C4 18.4 5.6 20 9.6 20Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.5 17H13.5C16 17 17 16 17 13.5V10.5C17 8 16 7 13.5 7H10.5C8 7 7 8 7 10.5V13.5C7 16 8 17 10.5 17Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.01001 4V2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 4V2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 4V2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20 8H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20 12H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20 16H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 20V22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12.01 20V22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.01001 20V22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 8H4\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 12H4\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 16H4\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:diamonds": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M16.29 2.15002H7.69995C5.99995 2.15002 5.24995 3.00002 4.78995 4.04002L2.22995 9.80002C1.76995 10.84 2.01995 12.39 2.78995 13.23L9.64995 20.77C10.95 22.19 13.07 22.19 14.36 20.77L21.21 13.22C21.98 12.37 22.23 10.83 21.76 9.79002L19.2 4.03002C18.74 3.00002 17.99 2.15002 16.29 2.15002Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M3.5 8H20.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:document-text": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21 7V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V7C3 4 4.5 2 8 2H16C19.5 2 21 4 21 7Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M14.5 4.5V6.5C14.5 7.6 15.4 8.5 16.5 8.5H18.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8 13H12\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8 17H16\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:edit-2": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M13.26 3.59997L5.04997 12.29C4.73997 12.62 4.43997 13.27 4.37997 13.72L4.00997 16.96C3.87997 18.13 4.71997 18.93 5.87997 18.73L9.09997 18.18C9.54997 18.1 10.18 17.77 10.49 17.43L18.7 8.73997C20.12 7.23997 20.76 5.52997 18.55 3.43997C16.35 1.36997 14.68 2.09997 13.26 3.59997Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.89 5.05005C12.32 7.81005 14.56 9.92005 17.34 10.2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M3 22H21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:element-3": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M22 8.52V3.98C22 2.57 21.36 2 19.77 2H15.73C14.14 2 13.5 2.57 13.5 3.98V8.51C13.5 9.93 14.14 10.49 15.73 10.49H19.77C21.36 10.5 22 9.93 22 8.52Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 19.77V15.73C22 14.14 21.36 13.5 19.77 13.5H15.73C14.14 13.5 13.5 14.14 13.5 15.73V19.77C13.5 21.36 14.14 22 15.73 22H19.77C21.36 22 22 21.36 22 19.77Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.5 8.52V3.98C10.5 2.57 9.86 2 8.27 2H4.23C2.64 2 2 2.57 2 3.98V8.51C2 9.93 2.64 10.49 4.23 10.49H8.27C9.86 10.5 10.5 9.93 10.5 8.52Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.5 19.77V15.73C10.5 14.14 9.86 13.5 8.27 13.5H4.23C2.64 13.5 2 14.14 2 15.73V19.77C2 21.36 2.64 22 4.23 22H8.27C9.86 22 10.5 21.36 10.5 19.77Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:eye": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M15.58 12C15.58 13.98 13.98 15.58 12 15.58C10.02 15.58 8.42004 13.98 8.42004 12C8.42004 10.02 10.02 8.42004 12 8.42004C13.98 8.42004 15.58 10.02 15.58 12Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 20.27C15.53 20.27 18.82 18.19 21.11 14.59C22.01 13.18 22.01 10.81 21.11 9.39997C18.82 5.79997 15.53 3.71997 12 3.71997C8.46997 3.71997 5.17997 5.79997 2.88997 9.39997C1.98997 10.81 1.98997 13.18 2.88997 14.59C5.17997 18.19 8.46997 20.27 12 20.27Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:eye-slash": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M14.53 9.47004L9.47004 14.53C8.82004 13.88 8.42004 12.99 8.42004 12C8.42004 10.02 10.02 8.42004 12 8.42004C12.99 8.42004 13.88 8.82004 14.53 9.47004Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17.82 5.76998C16.07 4.44998 14.07 3.72998 12 3.72998C8.46997 3.72998 5.17997 5.80998 2.88997 9.40998C1.98997 10.82 1.98997 13.19 2.88997 14.6C3.67997 15.84 4.59997 16.91 5.59997 17.77\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.42004 19.5301C9.56004 20.0101 10.77 20.2701 12 20.2701C15.53 20.2701 18.82 18.1901 21.11 14.5901C22.01 13.1801 22.01 10.8101 21.11 9.40005C20.78 8.88005 20.42 8.39005 20.05 7.93005\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.5099 12.7C15.2499 14.11 14.0999 15.26 12.6899 15.52\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9.47 14.53L2 22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 2L14.53 9.47\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:filter": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M5.40002 2.09998H18.6C19.7 2.09998 20.6 2.99998 20.6 4.09998V6.29998C20.6 7.09998 20.1 8.09998 19.6 8.59998L15.3 12.4C14.7 12.9 14.3 13.9 14.3 14.7V19C14.3 19.6 13.9 20.4 13.4 20.7L12 21.6C10.7 22.4 8.90002 21.5 8.90002 19.9V14.6C8.90002 13.9 8.50002 13 8.10002 12.5L4.30002 8.49998C3.80002 7.99998 3.40002 7.09998 3.40002 6.49998V4.19998C3.40002 2.99998 4.30002 2.09998 5.40002 2.09998Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.93 2.09998L6 9.99998\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:flash": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M9.31993 13.28H12.4099V20.48C12.4099 21.54 13.7299 22.04 14.4299 21.24L21.9999 12.64C22.6599 11.89 22.1299 10.72 21.1299 10.72H18.0399V3.51997C18.0399 2.45997 16.7199 1.95997 16.0199 2.75997L8.44994 11.36C7.79994 12.11 8.32993 13.28 9.31993 13.28Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 4H1.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7.5 20H1.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M4.5 12H1.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:headphone": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M5.46005 18.49V15.57C5.46005 14.6 6.22005 13.73 7.30005 13.73C8.27005 13.73 9.14005 14.49 9.14005 15.57V18.38C9.14005 20.33 7.52005 21.9501 5.57005 21.9501C3.62005 21.9501 2.00005 20.32 2.00005 18.38V12.22C1.89005 6.60005 6.33005 2.05005 11.95 2.05005C17.57 2.05005 22 6.60005 22 12.11V18.2701C22 20.2201 20.38 21.84 18.43 21.84C16.48 21.84 14.86 20.2201 14.86 18.2701V15.46C14.86 14.49 15.62 13.62 16.7 13.62C17.67 13.62 18.54 14.38 18.54 15.46V18.49\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:home": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 18V15\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.07 2.81997L3.14002 8.36997C2.36002 8.98997 1.86002 10.3 2.03002 11.28L3.36002 19.24C3.60002 20.66 4.96002 21.81 6.40002 21.81H17.6C19.03 21.81 20.4 20.65 20.64 19.24L21.97 11.28C22.13 10.3 21.63 8.98997 20.86 8.36997L13.93 2.82997C12.86 1.96997 11.13 1.96997 10.07 2.81997Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:info-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 8V13\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.9945 16H12.0035\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:lamp-on": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.30011 18.0399V16.8799C6.00011 15.4899 4.11011 12.7799 4.11011 9.89993C4.11011 4.94993 8.66011 1.06993 13.8001 2.18993C16.0601 2.68993 18.0401 4.18993 19.0701 6.25993C21.1601 10.4599 18.9601 14.9199 15.7301 16.8699V18.0299C15.7301 18.3199 15.8401 18.9899 14.7701 18.9899H9.26011C8.16011 18.9999 8.30011 18.5699 8.30011 18.0399Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 22C10.79 21.35 13.21 21.35 15.5 22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:location": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 13.4299C13.7231 13.4299 15.12 12.0331 15.12 10.3099C15.12 8.58681 13.7231 7.18994 12 7.18994C10.2769 7.18994 8.88 8.58681 8.88 10.3099C8.88 12.0331 10.2769 13.4299 12 13.4299Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n<path d=\"M3.62001 8.49C5.59001 -0.169998 18.42 -0.159997 20.38 8.5C21.53 13.58 18.37 17.88 15.6 20.54C13.59 22.48 10.41 22.48 8.39001 20.54C5.63001 17.88 2.47001 13.57 3.62001 8.49Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n</svg>\n",
|
||||
"vuesax-linear:lock": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M6 10V8C6 4.69 7 2 12 2C17 2 18 4.69 18 8V10\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 22H7C3 22 2 21 2 17V15C2 11 3 10 7 10H17C21 10 22 11 22 15V17C22 21 21 22 17 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.9965 16H16.0054\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.9955 16H12.0045\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7.99451 16H8.00349\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:logout": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.90002 7.55999C9.21002 3.95999 11.06 2.48999 15.11 2.48999H15.24C19.71 2.48999 21.5 4.27999 21.5 8.74999V15.27C21.5 19.74 19.71 21.53 15.24 21.53H15.11C11.09 21.53 9.24002 20.08 8.91002 16.54\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15 12H3.62\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.85 8.6499L2.5 11.9999L5.85 15.3499\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:menu": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M3 7H21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n<path d=\"M3 12H21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n<path d=\"M3 17H21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:message": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.5 19H8C4 19 2 18 2 13V8C2 4 4 2 8 2H16C20 2 22 4 22 8V13C22 17 20 19 16 19H15.5C15.19 19 14.89 19.15 14.7 19.4L13.2 21.4C12.54 22.28 11.46 22.28 10.8 21.4L9.3 19.4C9.14 19.18 8.77 19 8.5 19Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.9965 11H16.0054\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11.9955 11H12.0045\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7.99451 11H8.00349\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:mobile": "<svg width=\"18\" height=\"22\" viewBox=\"0 0 18 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M17 6V16C17 20 16 21 12 21H6C2 21 1 20 1 16V6C1 2 2 1 6 1H12C16 1 17 2 17 6Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11 4.5H7\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.99995 18.1C9.85599 18.1 10.55 17.406 10.55 16.55C10.55 15.694 9.85599 15 8.99995 15C8.14391 15 7.44995 15.694 7.44995 16.55C7.44995 17.406 8.14391 18.1 8.99995 18.1Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:money-4": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M17 20.5H7C4 20.5 2 19 2 15.5V8.5C2 5 4 3.5 7 3.5H17C20 3.5 22 5 22 8.5V15.5C22 19 20 20.5 17 20.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 6H7C5.62 6 4.5 7.12 4.5 8.5V10\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.5 6H17C18.38 6 19.5 7.12 19.5 8.5V10\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 18H7C5.62 18 4.5 16.88 4.5 15.5V14\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.5 18H17C18.38 18 19.5 16.88 19.5 15.5V14\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:monitor": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M6.44 2H17.55C21.11 2 22 2.89 22 6.44V12.77C22 16.33 21.11 17.21 17.56 17.21H6.44C2.89 17.22 2 16.33 2 12.78V6.44C2 2.89 2.89 2 6.44 2Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 17.22V22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 13H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7.5 22H16.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:moon": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M2.03009 12.42C2.39009 17.57 6.76009 21.76 11.9901 21.99C15.6801 22.15 18.9801 20.43 20.9601 17.72C21.7801 16.61 21.3401 15.87 19.9701 16.12C19.3001 16.24 18.6101 16.29 17.8901 16.26C13.0001 16.06 9.00009 11.97 8.98009 7.13996C8.97009 5.83996 9.24009 4.60996 9.73009 3.48996C10.2701 2.24996 9.62009 1.65996 8.37009 2.18996C4.41009 3.85996 1.70009 7.84996 2.03009 12.42Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:more": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M5 10C3.9 10 3 10.9 3 12C3 13.1 3.9 14 5 14C6.1 14 7 13.1 7 12C7 10.9 6.1 10 5 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n<path d=\"M19 10C17.9 10 17 10.9 17 12C17 13.1 17.9 14 19 14C20.1 14 21 13.1 21 12C21 10.9 20.1 10 19 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n<path d=\"M12 10C10.9 10 10 10.9 10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\"/>\n</svg>\n",
|
||||
"vuesax-linear:note-remove": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M7 14H12\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7 5.95996L3.25 2.20996\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6.96002 2.25L3.21002 6\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7 10H15\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10 2H16C19.33 2.18 21 3.41 21 7.99V16\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M3 9.01001V15.98C3 19.99 4 22 9 22H12C12.17 22 14.84 22 15 22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21 16L15 22V19C15 17 16 16 18 16H21Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:notification": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12.02 2.90991C8.70997 2.90991 6.01997 5.59991 6.01997 8.90991V11.7999C6.01997 12.4099 5.75997 13.3399 5.44997 13.8599L4.29997 15.7699C3.58997 16.9499 4.07997 18.2599 5.37997 18.6999C9.68997 20.1399 14.34 20.1399 18.65 18.6999C19.86 18.2999 20.39 16.8699 19.73 15.7699L18.58 13.8599C18.28 13.3399 18.02 12.4099 18.02 11.7999V8.90991C18.02 5.60991 15.32 2.90991 12.02 2.90991Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\"/>\n<path d=\"M13.87 3.19994C13.56 3.10994 13.24 3.03994 12.91 2.99994C11.95 2.87994 11.03 2.94994 10.17 3.19994C10.46 2.45994 11.18 1.93994 12.02 1.93994C12.86 1.93994 13.58 2.45994 13.87 3.19994Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15.02 19.0601C15.02 20.7101 13.67 22.0601 12.02 22.0601C11.2 22.0601 10.44 21.7201 9.90002 21.1801C9.36002 20.6401 9.02002 19.8801 9.02002 19.0601\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\"/>\n</svg>\n",
|
||||
"vuesax-linear:pen-add": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21.19 8.0399L18.01 4.85991C16.81 3.65991 15.16 3.71993 14.35 5.00993L12.58 7.80992L18.25 13.4799L21.05 11.7099C22.26 10.9399 22.33 9.1699 21.19 8.0399Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.25 13.47L18.49 17.59C18.72 19.89 17.92 20.69 15.74 20.95L7.02 21.98C5.18 22.19 3.86 20.87 4.08 19.04L5.06 10.76\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12.58 7.80994L10.83 7.69995\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.28 20.7799L8.46001 17.5898\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M11 6.5C11 6.91 10.94 7.32001 10.83 7.70001C10.72 8.10001 10.56 8.47001 10.35 8.82001C10.11 9.22001 9.81001 9.58 9.46001 9.88C8.67001 10.58 7.64 11 6.5 11C5.99 11 5.51 10.92 5.06 10.76C4.04 10.42 3.18999 9.72001 2.64999 8.82001C2.23999 8.14001 2 7.34 2 6.5C2 5.08 2.65 3.80999 3.69 2.98999C4.46 2.36999 5.44 2 6.5 2C8.99 2 11 4.01 11 6.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6.52 8.18005V4.82007\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.16 6.5H4.8\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:people": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M18 7.16C17.94 7.15 17.87 7.15 17.81 7.16C16.43 7.11 15.33 5.98 15.33 4.58C15.33 3.15 16.48 2 17.91 2C19.34 2 20.49 3.16 20.49 4.58C20.48 5.98 19.38 7.11 18 7.16Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16.9699 14.44C18.3399 14.67 19.8499 14.43 20.9099 13.72C22.3199 12.78 22.3199 11.24 20.9099 10.3C19.8399 9.59004 18.3099 9.35003 16.9399 9.59003\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.96998 7.16C6.02998 7.15 6.09998 7.15 6.15998 7.16C7.53998 7.11 8.63998 5.98 8.63998 4.58C8.63998 3.15 7.48998 2 6.05998 2C4.62998 2 3.47998 3.16 3.47998 4.58C3.48998 5.98 4.58998 7.11 5.96998 7.16Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M6.99994 14.44C5.62994 14.67 4.11994 14.43 3.05994 13.72C1.64994 12.78 1.64994 11.24 3.05994 10.3C4.12994 9.59004 5.65994 9.35003 7.02994 9.59003\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 14.63C11.94 14.62 11.87 14.62 11.81 14.63C10.43 14.58 9.32996 13.45 9.32996 12.05C9.32996 10.62 10.48 9.46997 11.91 9.46997C13.34 9.46997 14.49 10.63 14.49 12.05C14.48 13.45 13.38 14.59 12 14.63Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9.08997 17.78C7.67997 18.72 7.67997 20.26 9.08997 21.2C10.69 22.27 13.31 22.27 14.91 21.2C16.32 20.26 16.32 18.72 14.91 17.78C13.32 16.72 10.69 16.72 9.08997 17.78Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:personalcard": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M17 21H7C3 21 2 20 2 16V8C2 4 3 3 7 3H17C21 3 22 4 22 8V16C22 20 21 21 17 21Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M14 8H19\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M15 12H19\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 16H19\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.49994 11.2899C9.49958 11.2899 10.3099 10.4796 10.3099 9.47992C10.3099 8.48029 9.49958 7.66992 8.49994 7.66992C7.50031 7.66992 6.68994 8.48029 6.68994 9.47992C6.68994 10.4796 7.50031 11.2899 8.49994 11.2899Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 16.33C11.86 14.88 10.71 13.74 9.26 13.61C8.76 13.56 8.25 13.56 7.74 13.61C6.29 13.75 5.14 14.88 5 16.33\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:printer": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M7.25 7H16.75V5C16.75 3 16 2 13.75 2H10.25C8 2 7.25 3 7.25 5V7Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 15V19C16 21 15 22 13 22H11C9 22 8 21 8 19V15H16Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21 10V15C21 17 20 18 18 18H16V15H8V18H6C4 18 3 17 3 15V10C3 8 4 7 6 7H18C20 7 21 8 21 10Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 15H15.79H7\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7 11H10\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:profile-2user": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M9.16006 10.87C9.06006 10.86 8.94006 10.86 8.83006 10.87C6.45006 10.79 4.56006 8.84 4.56006 6.44C4.56006 3.99 6.54006 2 9.00006 2C11.4501 2 13.4401 3.99 13.4401 6.44C13.4301 8.84 11.5401 10.79 9.16006 10.87Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16.41 4C18.35 4 19.91 5.57 19.91 7.5C19.91 9.39 18.41 10.93 16.54 11C16.46 10.99 16.37 10.99 16.28 11\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M4.15997 14.56C1.73997 16.18 1.73997 18.82 4.15997 20.43C6.90997 22.27 11.42 22.27 14.17 20.43C16.59 18.81 16.59 16.17 14.17 14.56C11.43 12.73 6.91997 12.73 4.15997 14.56Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.3401 20C19.0601 19.85 19.7401 19.56 20.3001 19.13C21.8601 17.96 21.8601 16.03 20.3001 14.86C19.7501 14.44 19.0801 14.16 18.3701 14\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:profile-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12.12 12.78C12.05 12.77 11.96 12.77 11.88 12.78C10.12 12.72 8.71997 11.28 8.71997 9.50998C8.71997 7.69998 10.18 6.22998 12 6.22998C13.81 6.22998 15.28 7.69998 15.28 9.50998C15.27 11.28 13.88 12.72 12.12 12.78Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.74 19.3801C16.96 21.0101 14.6 22.0001 12 22.0001C9.40001 22.0001 7.04001 21.0101 5.26001 19.3801C5.36001 18.4401 5.96001 17.5201 7.03001 16.8001C9.77001 14.9801 14.25 14.9801 16.97 16.8001C18.04 17.5201 18.64 18.4401 18.74 19.3801Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:receipt-search": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M20.5 11.3V7.04001C20.5 3.01001 19.56 2 15.78 2H8.22C4.44 2 3.5 3.01001 3.5 7.04001V18.3C3.5 20.96 4.96001 21.59 6.73001 19.69L6.73999 19.68C7.55999 18.81 8.80999 18.88 9.51999 19.83L10.53 21.18\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.2 21.4C19.9673 21.4 21.4 19.9673 21.4 18.2C21.4 16.4327 19.9673 15 18.2 15C16.4327 15 15 16.4327 15 18.2C15 19.9673 16.4327 21.4 18.2 21.4Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 22L21 21\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8 7H16\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9 11H15\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:search-normal": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M11 20C15.9706 20 20 15.9706 20 11C20 6.02944 15.9706 2 11 2C6.02944 2 2 6.02944 2 11C2 15.9706 6.02944 20 11 20Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.9299 20.6898C19.4599 22.2898 20.6699 22.4498 21.5999 21.0498C22.4499 19.7698 21.8899 18.7198 20.3499 18.7198C19.2099 18.7098 18.5699 19.5998 18.9299 20.6898Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:search-status": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M14 5H20\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M14 8H17\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21 11.5C21 16.75 16.75 21 11.5 21C6.25 21 2 16.75 2 11.5C2 6.25 6.25 2 11.5 2\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 22L20 20\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:setting-2": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M2 12.8799V11.1199C2 10.0799 2.85 9.21994 3.9 9.21994C5.71 9.21994 6.45 7.93994 5.54 6.36994C5.02 5.46994 5.33 4.29994 6.24 3.77994L7.97 2.78994C8.76 2.31994 9.78 2.59994 10.25 3.38994L10.36 3.57994C11.26 5.14994 12.74 5.14994 13.65 3.57994L13.76 3.38994C14.23 2.59994 15.25 2.31994 16.04 2.78994L17.77 3.77994C18.68 4.29994 18.99 5.46994 18.47 6.36994C17.56 7.93994 18.3 9.21994 20.11 9.21994C21.15 9.21994 22.01 10.0699 22.01 11.1199V12.8799C22.01 13.9199 21.16 14.7799 20.11 14.7799C18.3 14.7799 17.56 16.0599 18.47 17.6299C18.99 18.5399 18.68 19.6999 17.77 20.2199L16.04 21.2099C15.25 21.6799 14.23 21.3999 13.76 20.6099L13.65 20.4199C12.75 18.8499 11.27 18.8499 10.36 20.4199L10.25 20.6099C9.78 21.3999 8.76 21.6799 7.97 21.2099L6.24 20.2199C5.33 19.6999 5.02 18.5299 5.54 17.6299C6.45 16.0599 5.71 14.7799 3.9 14.7799C2.85 14.7799 2 13.9199 2 12.8799Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:shopping-bag": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.39999 6.5H15.6C19 6.5 19.34 8.09 19.57 10.03L20.47 17.53C20.76 19.99 20 22 16.5 22H7.50999C3.99999 22 3.23999 19.99 3.53999 17.53L4.44 10.03C4.66 8.09 4.99999 6.5 8.39999 6.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8 8V4.5C8 3 9 2 10.5 2H13.5C15 2 16 3 16 4.5V8\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M20.41 17.03H8\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:sms": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M17 20.5H7C4 20.5 2 19 2 15.5V8.5C2 5 4 3.5 7 3.5H17C20 3.5 22 5 22 8.5V15.5C22 19 20 20.5 17 20.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17 9L13.87 11.5C12.84 12.32 11.15 12.32 10.12 11.5L7 9\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:sun-1": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 18.5C15.5899 18.5 18.5 15.5899 18.5 12C18.5 8.41015 15.5899 5.5 12 5.5C8.41015 5.5 5.5 8.41015 5.5 12C5.5 15.5899 8.41015 18.5 12 18.5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M19.14 19.14L19.01 19.01M19.01 4.99L19.14 4.86L19.01 4.99ZM4.86 19.14L4.99 19.01L4.86 19.14ZM12 2.08V2V2.08ZM12 22V21.92V22ZM2.08 12H2H2.08ZM22 12H21.92H22ZM4.99 4.99L4.86 4.86L4.99 4.99Z\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:tick-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M7.75 12L10.58 14.83L16.25 9.17004\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:timer-1": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M20.75 13.25C20.75 18.08 16.83 22 12 22C7.17 22 3.25 18.08 3.25 13.25C3.25 8.42 7.17 4.5 12 4.5C16.83 4.5 20.75 8.42 20.75 13.25Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 8V13\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9 2H15\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:trash": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21 5.97998C17.67 5.64998 14.32 5.47998 10.98 5.47998C9 5.47998 7.02 5.57998 5.04 5.77998L3 5.97998\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8.5 4.97L8.72 3.66C8.88 2.71 9 2 10.69 2H13.31C15 2 15.13 2.75 15.28 3.67L15.5 4.97\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M18.85 9.14001L18.2 19.21C18.09 20.78 18 22 15.21 22H8.79002C6.00002 22 5.91002 20.78 5.80002 19.21L5.15002 9.14001\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.33 16.5H13.66\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9.5 12.5H14.5\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:truck-time": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M15 2V12C15 13.1 14.1 14 13 14H2V7.62C2.73 8.49 3.85003 9.03 5.09003 9C6.10003 8.98 7.01 8.59 7.69 7.94C8 7.68 8.26002 7.34999 8.46002 6.98999C8.82002 6.37999 9.02 5.65997 9 4.90997C8.97 3.73997 8.45001 2.71 7.64001 2H15Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 14V17C22 18.66 20.66 20 19 20H18C18 18.9 17.1 18 16 18C14.9 18 14 18.9 14 20H10C10 18.9 9.1 18 8 18C6.9 18 6 18.9 6 20H5C3.34 20 2 18.66 2 17V14H13C14.1 14 15 13.1 15 12V5H16.84C17.56 5 18.22 5.39001 18.58 6.01001L20.29 9H19C18.45 9 18 9.45 18 10V13C18 13.55 18.45 14 19 14H22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M8 22C9.10457 22 10 21.1046 10 20C10 18.8954 9.10457 18 8 18C6.89543 18 6 18.8954 6 20C6 21.1046 6.89543 22 8 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M16 22C17.1046 22 18 21.1046 18 20C18 18.8954 17.1046 18 16 18C14.8954 18 14 18.8954 14 20C14 21.1046 14.8954 22 16 22Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 12V14H19C18.45 14 18 13.55 18 13V10C18 9.45 18.45 9 19 9H20.29L22 12Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M9 5C9 6.2 8.47001 7.27 7.64001 8C6.93001 8.62 6.01 9 5 9C2.79 9 1 7.21 1 5C1 3.74 1.58 2.61 2.5 1.88C3.19 1.33 4.06 1 5 1C7.21 1 9 2.79 9 5Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M5.25 3.75V5.25L4 6\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-miterlimit=\"10\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:verify": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M8.38 12L10.79 14.42L15.62 9.57996\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M10.75 2.44995C11.44 1.85995 12.57 1.85995 13.27 2.44995L14.85 3.80995C15.15 4.06995 15.71 4.27995 16.11 4.27995H17.81C18.87 4.27995 19.74 5.14995 19.74 6.20995V7.90995C19.74 8.29995 19.95 8.86995 20.21 9.16995L21.57 10.7499C22.16 11.4399 22.16 12.5699 21.57 13.2699L20.21 14.8499C19.95 15.1499 19.74 15.7099 19.74 16.1099V17.8099C19.74 18.8699 18.87 19.7399 17.81 19.7399H16.11C15.72 19.7399 15.15 19.9499 14.85 20.2099L13.27 21.5699C12.58 22.1599 11.45 22.1599 10.75 21.5699L9.17 20.2099C8.87 19.9499 8.31 19.7399 7.91 19.7399H6.18C5.12 19.7399 4.25 18.8699 4.25 17.8099V16.0999C4.25 15.7099 4.04 15.1499 3.79 14.8499L2.44 13.2599C1.86 12.5699 1.86 11.4499 2.44 10.7599L3.79 9.16995C4.04 8.86995 4.25 8.30995 4.25 7.91995V6.19995C4.25 5.13995 5.12 4.26995 6.18 4.26995H7.91C8.3 4.26995 8.87 4.05995 9.17 3.79995L10.75 2.44995Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:wallet": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M22 12V17C22 20 20 22 17 22H7C4 22 2 20 2 17V12C2 9.28 3.64 7.38 6.19 7.06C6.45 7.02 6.72 7 7 7H17C17.26 7 17.51 7.00999 17.75 7.04999C20.33 7.34999 22 9.26 22 12Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M17.7514 7.05C17.5114 7.01 17.2614 7.00001 17.0014 7.00001H7.00141C6.72141 7.00001 6.45141 7.02001 6.19141 7.06001C6.33141 6.78001 6.53141 6.52001 6.77141 6.28001L10.0214 3.02C11.3914 1.66 13.6114 1.66 14.9814 3.02L16.7314 4.79002C17.3714 5.42002 17.7114 6.22 17.7514 7.05Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M22 12.5H19C17.9 12.5 17 13.4 17 14.5C17 15.6 17.9 16.5 19 16.5H22\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
|
||||
"vuesax-linear:warning-2": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M12 7.75V13\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M21.08 8.58003V15.42C21.08 16.54 20.48 17.58 19.51 18.15L13.57 21.58C12.6 22.14 11.4 22.14 10.42 21.58L4.47998 18.15C3.50998 17.59 2.90997 16.55 2.90997 15.42V8.58003C2.90997 7.46003 3.50998 6.41999 4.47998 5.84999L10.42 2.42C11.39 1.86 12.59 1.86 13.57 2.42L19.51 5.84999C20.48 6.41999 21.08 7.45003 21.08 8.58003Z\" stroke=\"#292D32\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n<path d=\"M12 16.2V16.2999\" stroke=\"#292D32\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:notification"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:flash"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
color?: string
|
||||
filled?: boolean
|
||||
}>(),
|
||||
{ size: 18, color: '#9AA3AF', filled: false },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6.75 3.25h10.5c.97 0 1.75.78 1.75 1.75v15.2c0 .67-.73 1.08-1.3.73L12 17.02 6.3 20.93c-.57.35-1.3-.06-1.3-.73V5c0-.97.78-1.75 1.75-1.75Z"
|
||||
:stroke="color"
|
||||
:fill="filled ? color : 'none'"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:buildings"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:bag-2"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:money-4"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:card"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:category"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:card-receive"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:tick-circle"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:arrow-right-1"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:arrow-left-1"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:clipboard-close"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:diamonds"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:edit-2"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ size?: number }>(), { size: 72 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
class="icon-logo"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 72 72"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<rect x="14" y="14" width="28" height="28" rx="4" transform="rotate(12 28 28)" />
|
||||
<rect x="30" y="30" width="28" height="28" rx="4" transform="rotate(-12 44 44)" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-logo {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:pen-add"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:filter"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:headphone"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import IconBookmark from '@/components/icons/IconBookmark.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
color?: string
|
||||
filled?: boolean
|
||||
}>(),
|
||||
{ size: 18, color: '#9AA3AF', filled: false },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBookmark v-bind="props" />
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:home"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:timer-1"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:personalcard"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:element-3"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:lamp-on"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:lock"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:sms"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:location"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
}>(),
|
||||
{ size: 44 },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
class="icon-medal"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 28.5L14 44l10-5 10 5-4-15.5" fill="#C0C7D1" />
|
||||
<path d="M18 28.5L14 44l10-5V28.5H18Z" fill="#A8B0BC" />
|
||||
<circle cx="24" cy="18" r="14" fill="#D8DEE6" />
|
||||
<circle cx="24" cy="18" r="11" fill="#F3F5F8" stroke="#B8C0CB" stroke-width="1.5" />
|
||||
<path
|
||||
d="M24 10.5l2.2 4.5 5 .7-3.6 3.5.9 5L24 21.8 19.5 24l.9-5-3.6-3.5 5-.7L24 10.5Z"
|
||||
fill="#8B95A5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:more"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:document-text"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ size?: number }>(), { size: 72 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
class="icon-logo"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 72 72"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<text
|
||||
x="36"
|
||||
y="50"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
fill="currentColor"
|
||||
style="font-family: IRANSans, Tahoma, sans-serif; font-size: 48px; font-weight: 700"
|
||||
>
|
||||
پ
|
||||
</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-logo {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:call"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:add"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:profile-circle"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:clock"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:search-normal"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:setting-2"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:shopping-bag"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:tick-circle"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:trash"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string | null
|
||||
}>(),
|
||||
{ size: 24, strokeWidth: 1.5, color: null },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:verify"
|
||||
:size="size"
|
||||
:stroke-width="strokeWidth"
|
||||
:color="color"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<slot />
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { MAIN_NAV_ITEMS, isNavItemActive } from '@/utils/navigation'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const cartCount = computed(() => cartStore.itemCount)
|
||||
|
||||
function navigate(path: string, event: Event) {
|
||||
const target = event.currentTarget
|
||||
if (target instanceof HTMLElement) target.blur()
|
||||
|
||||
// «ثبت سفارش»: سبد خالی → انتخاب شرکت؛ در غیر این صورت سبد خرید
|
||||
if (path === '/cart' && cartStore.itemCount === 0) {
|
||||
router.push('/companies')
|
||||
return
|
||||
}
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="bottom-nav" aria-label="ناوبری اصلی">
|
||||
<button
|
||||
v-for="item in MAIN_NAV_ITEMS"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="nav-item"
|
||||
:class="{
|
||||
active: isNavItemActive(item.name, item.path, route.path),
|
||||
'nav-item--fab': item.name === 'order-fab',
|
||||
}"
|
||||
:aria-current="isNavItemActive(item.name, item.path, route.path) ? 'page' : undefined"
|
||||
:aria-label="item.label"
|
||||
@click="navigate(item.path, $event)"
|
||||
>
|
||||
<span class="nav-icon" :class="{ 'nav-icon--fab': item.name === 'order-fab' }">
|
||||
<component
|
||||
:is="item.icon"
|
||||
:size="item.name === 'order-fab' ? 26 : 22"
|
||||
:stroke-width="item.name === 'order-fab' ? 1.8 : 1.5"
|
||||
:color="item.name === 'order-fab' ? '#ffffff' : undefined"
|
||||
/>
|
||||
<span
|
||||
v-if="item.name === 'order-fab' && cartCount > 0"
|
||||
class="nav-badge"
|
||||
>{{ cartCount }}</span>
|
||||
</span>
|
||||
<span v-if="item.name !== 'order-fab'" class="nav-label">{{ item.label }}</span>
|
||||
<span v-else class="nav-label nav-label--fab">{{ item.label }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bottom-nav {
|
||||
flex-shrink: 0;
|
||||
min-height: var(--nav-height);
|
||||
background: var(--color-surface);
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bottom-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 4px 2px 2px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.nav-item:focus,
|
||||
.nav-item:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-item--fab {
|
||||
justify-content: flex-end;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.nav-item--fab.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.nav-icon--fab {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-top: -28px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 16px rgba(var(--color-primary-rgb), 0.35);
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-label--fab {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
inset-inline-end: -2px;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
currentStep: 1 | 2 | 3 | 4
|
||||
}>()
|
||||
|
||||
const steps = [
|
||||
{ num: 1, label: 'سبد خرید' },
|
||||
{ num: 2, label: 'پیشسفارش' },
|
||||
{ num: 3, label: 'صدور فاکتور' },
|
||||
{ num: 4, label: 'تسویه' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="checkout-stepper" aria-label="مراحل تسویه">
|
||||
<template v-for="(step, index) in steps" :key="step.num">
|
||||
<div
|
||||
class="checkout-stepper__item"
|
||||
:class="{
|
||||
'checkout-stepper__item--done': step.num < currentStep,
|
||||
'checkout-stepper__item--active': step.num === currentStep,
|
||||
}"
|
||||
>
|
||||
<span class="checkout-stepper__dot">
|
||||
<span v-if="step.num < currentStep" class="checkout-stepper__check">✓</span>
|
||||
<template v-else>{{ step.num }}</template>
|
||||
</span>
|
||||
<span class="checkout-stepper__label">{{ step.label }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="index < steps.length - 1"
|
||||
class="checkout-stepper__bridge"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
class="checkout-stepper__bridge-line"
|
||||
:class="{ 'checkout-stepper__bridge-line--done': index < currentStep }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.checkout-stepper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
direction: rtl;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
padding: 22px 28px 20px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.checkout-stepper__item {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 88px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checkout-stepper__bridge {
|
||||
flex: 1 1 0;
|
||||
min-width: 20px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.checkout-stepper__bridge-line {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.checkout-stepper__bridge-line--done {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.checkout-stepper__dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: var(--color-surface);
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkout-stepper__item--done .checkout-stepper__dot {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.checkout-stepper__item--active .checkout-stepper__dot {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.checkout-stepper__check {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.checkout-stepper__label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkout-stepper__item--active .checkout-stepper__label {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.checkout-stepper__item--done .checkout-stepper__label {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import UserAvatar from '@/components/ui/UserAvatar.vue'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const cartStore = useCartStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const showUserMenu = ref(false)
|
||||
|
||||
const unreadLabel = computed(() =>
|
||||
toPersianDigits(
|
||||
notificationStore.unreadCount > 9 ? '9+' : String(notificationStore.unreadCount),
|
||||
),
|
||||
)
|
||||
|
||||
const initials = computed(() => {
|
||||
const first = authStore.user?.firstName?.[0] ?? ''
|
||||
const last = authStore.user?.lastName?.[0] ?? ''
|
||||
return `${first}${last}` || '؟'
|
||||
})
|
||||
|
||||
const companyLabel = computed(
|
||||
() => cartStore.companyNamesLabel || cartStore.activeCart?.companyName || '',
|
||||
)
|
||||
|
||||
function toggleUserMenu() {
|
||||
showUserMenu.value = !showUserMenu.value
|
||||
}
|
||||
|
||||
function goProfile() {
|
||||
showUserMenu.value = false
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
function logout() {
|
||||
showUserMenu.value = false
|
||||
authStore.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="desktop-header">
|
||||
<div class="desktop-header__spacer" aria-hidden="true" />
|
||||
|
||||
<div class="desktop-header__actions">
|
||||
<span v-if="companyLabel" class="desktop-header__company">{{ companyLabel }}</span>
|
||||
|
||||
<AuthThemeToggle variant="inline" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="desktop-header__icon-btn"
|
||||
aria-label="اعلانها"
|
||||
@click="router.push('/profile/notifications')"
|
||||
>
|
||||
<IconBell :size="20" />
|
||||
<span
|
||||
v-if="notificationStore.hasUnread"
|
||||
class="desktop-header__notif-badge"
|
||||
:aria-label="`${notificationStore.unreadCount} اعلان خواندهنشده`"
|
||||
>
|
||||
{{ unreadLabel }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="desktop-header__user-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="desktop-header__user"
|
||||
:aria-expanded="showUserMenu"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
<UserAvatar
|
||||
class="desktop-header__avatar"
|
||||
:src="authStore.user?.avatarUrl"
|
||||
:initials="initials"
|
||||
:size="32"
|
||||
/>
|
||||
<span class="desktop-header__user-name">{{ authStore.fullName }}</span>
|
||||
<IconChevronLeft :size="16" class="desktop-header__chevron" />
|
||||
</button>
|
||||
|
||||
<div v-if="showUserMenu" class="desktop-header__menu">
|
||||
<button type="button" @click="goProfile">پروفایل</button>
|
||||
<button type="button" class="desktop-header__menu-logout" @click="logout">خروج</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-header {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
height: var(--header-height);
|
||||
min-height: var(--header-height);
|
||||
max-height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 0 28px;
|
||||
background: var(--color-surface);
|
||||
/* Border drawn by shared shell rule so it aligns with the sidebar brand */
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-header {
|
||||
display: flex;
|
||||
padding: 0 var(--page-padding);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-header {
|
||||
padding: 0 var(--page-padding);
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-header__spacer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desktop-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.desktop-header__company {
|
||||
display: none;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
padding-inline-end: 8px;
|
||||
border-inline-end: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.desktop-header__company {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-header__icon-btn {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-header__notif-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
inset-inline-end: 4px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
background: var(--color-danger);
|
||||
color: var(--color-on-primary, #fff);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
border: 2px solid var(--color-surface);
|
||||
}
|
||||
|
||||
.desktop-header__user-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.desktop-header__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 12px 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.desktop-header__avatar {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.desktop-header__user-name {
|
||||
display: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-header__user-name {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-header__chevron {
|
||||
color: var(--color-text-muted);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.desktop-header__menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
inset-inline-end: 0;
|
||||
min-width: 160px;
|
||||
padding: 6px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.desktop-header__menu button {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: start;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-header__menu button:hover {
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.desktop-header__menu-logout {
|
||||
color: var(--color-danger) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,286 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import {
|
||||
DESKTOP_NAV_ITEMS,
|
||||
isNavItemActive,
|
||||
resolveNavPath,
|
||||
} from '@/utils/navigation'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const cartCount = computed(() => cartStore.itemCount)
|
||||
const draftCheckCount = computed(() => orderStore.draftChecks.length)
|
||||
const activeCompanyId = computed(
|
||||
() => cartStore.selectedCompanyId || cartStore.activeCart?.companyId || null,
|
||||
)
|
||||
|
||||
function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
router.push(resolveNavPath(item, activeCompanyId.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="desktop-sidebar" aria-label="ناوبری اصلی">
|
||||
<div class="desktop-sidebar__brand">
|
||||
<span class="desktop-sidebar__logo-mark" aria-hidden="true">پ</span>
|
||||
<strong class="desktop-sidebar__title">پارس بافت</strong>
|
||||
</div>
|
||||
|
||||
<nav class="desktop-sidebar__nav">
|
||||
<button
|
||||
v-for="item in DESKTOP_NAV_ITEMS"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="desktop-sidebar__item"
|
||||
:class="{ active: isNavItemActive(item.name, item.path, route.path) }"
|
||||
:aria-current="isNavItemActive(item.name, item.path, route.path) ? 'page' : undefined"
|
||||
:aria-label="item.label"
|
||||
:title="item.label"
|
||||
@click="navigate(item)"
|
||||
>
|
||||
<span class="desktop-sidebar__icon">
|
||||
<component :is="item.icon" :size="20" />
|
||||
</span>
|
||||
<span class="desktop-sidebar__label">{{ item.label }}</span>
|
||||
<span v-if="item.name === 'cart' && cartCount > 0" class="desktop-sidebar__badge">
|
||||
{{ cartCount }}
|
||||
</span>
|
||||
<span v-if="item.name === 'checks' && draftCheckCount > 0" class="desktop-sidebar__badge">
|
||||
{{ draftCheckCount }}
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="desktop-sidebar__support">
|
||||
<span class="desktop-sidebar__support-icon" aria-hidden="true">
|
||||
<IconHeadset :size="22" />
|
||||
</span>
|
||||
<strong class="desktop-sidebar__support-title">پشتیبانی و راهنما</strong>
|
||||
<p class="desktop-sidebar__support-text">
|
||||
در صورت نیاز به راهنمایی یا گزارش مشکل با تیم پشتیبانی تماس بگیرید.
|
||||
</p>
|
||||
<a href="tel:02112345678" class="desktop-sidebar__support-btn">تماس با پشتیبانی</a>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-sidebar {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: var(--color-surface);
|
||||
border-inline-start: 1px solid var(--color-border);
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-sidebar {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
height: var(--header-height);
|
||||
min-height: var(--header-height);
|
||||
max-height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
/* Border drawn by shared shell rule so it aligns with the top header */
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__brand {
|
||||
padding: 0 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__logo-mark {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-sidebar__title {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.desktop-sidebar__nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__nav {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: start;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__item {
|
||||
padding: 11px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__item:hover {
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-sidebar__item:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.desktop-sidebar__item.active {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desktop-sidebar__item.active .desktop-sidebar__icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.desktop-sidebar__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.desktop-sidebar__label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-sidebar__badge {
|
||||
margin-inline-start: auto;
|
||||
background: var(--color-danger);
|
||||
color: var(--color-surface);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: auto 12px 20px;
|
||||
padding: 14px;
|
||||
background: var(--color-primary-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__support {
|
||||
margin: auto 16px 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-text {
|
||||
margin: 0;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-btn {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
showBack?: boolean
|
||||
centered?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
back: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="centered">
|
||||
<header class="page-nav">
|
||||
<button
|
||||
v-if="showBack"
|
||||
type="button"
|
||||
class="nav-btn"
|
||||
aria-label="بازگشت"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<span v-else class="nav-btn" />
|
||||
<h1 class="page-nav__title">{{ title }}</h1>
|
||||
<div class="page-nav__trailing">
|
||||
<slot name="trailing" />
|
||||
<AuthThemeToggle variant="inline" class="mobile-only" />
|
||||
</div>
|
||||
</header>
|
||||
<p v-if="subtitle" class="page-nav__subtitle">{{ subtitle }}</p>
|
||||
</template>
|
||||
|
||||
<header v-else class="page-header">
|
||||
<button
|
||||
v-if="showBack"
|
||||
type="button"
|
||||
class="back-btn"
|
||||
aria-label="بازگشت"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">{{ title }}</h1>
|
||||
<p v-if="subtitle" class="page-subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="page-header__trailing">
|
||||
<slot name="trailing" />
|
||||
<AuthThemeToggle variant="inline" class="mobile-only" />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding-inline: var(--page-padding);
|
||||
padding-top: 8px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-nav__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-nav__subtitle {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.page-nav__trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: var(--page-padding);
|
||||
padding-top: 8px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-header__trailing {
|
||||
margin-inline-start: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text);
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-summary-bg);
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 4px 0 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import type { StatusHeroConfig } from '@/utils/orderTracking'
|
||||
|
||||
defineProps<{
|
||||
config: StatusHeroConfig
|
||||
}>()
|
||||
|
||||
const icons = {
|
||||
waiting: 'vuesax-linear:timer-1',
|
||||
success: 'vuesax-linear:tick-circle',
|
||||
info: 'vuesax-linear:info-circle',
|
||||
none: 'vuesax-linear:info-circle',
|
||||
} as const
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="config.variant !== 'none'" class="status-hero" :class="`status-hero--${config.variant}`">
|
||||
<div class="status-hero__icon" aria-hidden="true">
|
||||
<BasicIcon :icon="icons[config.variant]" :size="28" :stroke-width="1.5" />
|
||||
</div>
|
||||
<div class="status-hero__copy">
|
||||
<h2 class="status-hero__title">{{ config.title }}</h2>
|
||||
<p v-if="config.description" class="status-hero__desc">{{ config.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 8px 12px 20px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.status-hero__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-hero--waiting .status-hero__icon {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.status-hero--success .status-hero__icon {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-hero--info .status-hero__icon {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.status-hero__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-hero__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.status-hero--success .status-hero__title {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-hero__desc {
|
||||
margin: 0;
|
||||
max-width: 320px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.status-hero {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
text-align: start;
|
||||
padding: 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-hero__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-hero__title {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.status-hero__desc {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Order } from '@/types'
|
||||
import { formatCurrency, formatNumber, toPersianDigits } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
order: Order
|
||||
}>()
|
||||
|
||||
const checksTotal = computed(() =>
|
||||
props.order.checks?.reduce((sum, check) => sum + check.amount, 0) ?? 0,
|
||||
)
|
||||
|
||||
function settlementLabel(type: 'cash' | 'check' | null) {
|
||||
if (!type) return 'در انتظار انتخاب مشتری'
|
||||
return type === 'cash' ? 'تسویه نقدی' : 'تسویه چکی'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tracking-summary">
|
||||
<div class="tracking-summary__row">
|
||||
<span>مبلغ فاکتور</span>
|
||||
<strong>
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : 'در انتظار قیمتگذاری' }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="tracking-summary__row">
|
||||
<span>نوع تسویه</span>
|
||||
<strong>{{ settlementLabel(order.settlementType) }}</strong>
|
||||
</div>
|
||||
|
||||
<template v-if="order.settlementType === 'check' && order.checks?.length">
|
||||
<div class="tracking-summary__row">
|
||||
<span>تعداد چکها</span>
|
||||
<strong>{{ toPersianDigits(String(order.checks.length)) }} فقره</strong>
|
||||
</div>
|
||||
<div class="tracking-summary__row">
|
||||
<span>مجموع مبلغ چکها</span>
|
||||
<strong>{{ formatCurrency(checksTotal) }}</strong>
|
||||
</div>
|
||||
<div v-if="order.checkRassDays != null" class="tracking-summary__row">
|
||||
<span>رأس چک (تقریبی)</span>
|
||||
<strong>{{ toPersianDigits(String(order.checkRassDays)) }} روز</strong>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="order.settlementType === 'cash' && order.cashReceipt">
|
||||
<div class="tracking-summary__row">
|
||||
<span>مبلغ واریزی</span>
|
||||
<strong>{{ formatCurrency(order.cashReceipt.amount) }}</strong>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tracking-summary {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--color-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.tracking-summary__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tracking-summary__row:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.tracking-summary__row span {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tracking-summary__row strong {
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Order, OrderStatus } from '@/types'
|
||||
import {
|
||||
getOrderTrackingSteps,
|
||||
getStepTimestamp,
|
||||
getTrackingStepIndex,
|
||||
} from '@/utils/orderTracking'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
order: Order
|
||||
}>()
|
||||
|
||||
const steps = computed(() => getOrderTrackingSteps(props.order))
|
||||
const currentIndex = computed(() => getTrackingStepIndex(props.order.status))
|
||||
|
||||
function stepState(index: number): 'done' | 'current' | 'pending' {
|
||||
if (index < currentIndex.value) return 'done'
|
||||
if (index === currentIndex.value) return 'current'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function timestampFor(step: OrderStatus) {
|
||||
const raw = getStepTimestamp(props.order, step)
|
||||
return raw ? formatDateTime(raw) : null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tracking-timeline">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.status"
|
||||
class="tracking-timeline__item"
|
||||
:class="`tracking-timeline__item--${stepState(index)}`"
|
||||
>
|
||||
<div class="tracking-timeline__marker-wrap">
|
||||
<div class="tracking-timeline__line" aria-hidden="true" />
|
||||
<div class="tracking-timeline__marker">
|
||||
<span v-if="stepState(index) === 'done'" class="tracking-timeline__check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tracking-timeline__content"
|
||||
:class="{ 'tracking-timeline__content--current': stepState(index) === 'current' }"
|
||||
>
|
||||
<strong class="tracking-timeline__label">{{ step.label }}</strong>
|
||||
<span v-if="timestampFor(step.status)" class="tracking-timeline__time">
|
||||
{{ timestampFor(step.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tracking-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tracking-timeline__item {
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
align-items: start;
|
||||
/* Same vertical rhythm for every step so the connector stays continuous */
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.tracking-timeline__marker-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.tracking-timeline__line {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-border);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tracking-timeline__item:last-child .tracking-timeline__line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tracking-timeline__item--done .tracking-timeline__line {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.tracking-timeline__item--current .tracking-timeline__line {
|
||||
background: color-mix(in srgb, var(--color-primary) 35%, var(--color-border));
|
||||
}
|
||||
|
||||
.tracking-timeline__marker {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-border);
|
||||
border: 2px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tracking-timeline__item--done .tracking-timeline__marker {
|
||||
background: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.tracking-timeline__item--current .tracking-timeline__marker {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 28%, transparent);
|
||||
}
|
||||
|
||||
.tracking-timeline__item--pending .tracking-timeline__marker {
|
||||
background: var(--color-elevated);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.tracking-timeline__check {
|
||||
color: var(--color-surface);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tracking-timeline__content {
|
||||
min-width: 0;
|
||||
padding: 2px 0 14px;
|
||||
}
|
||||
|
||||
.tracking-timeline__content--current {
|
||||
padding: 10px 14px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary-soft);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tracking-timeline__label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tracking-timeline__item--done .tracking-timeline__label,
|
||||
.tracking-timeline__item--current .tracking-timeline__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tracking-timeline__time {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** floating = auth pages; inline = header / toolbar */
|
||||
variant?: 'floating' | 'inline'
|
||||
}>(),
|
||||
{ variant: 'floating' },
|
||||
)
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { isDark } = storeToRefs(themeStore)
|
||||
|
||||
const themeIcon = computed(() =>
|
||||
isDark.value ? 'vuesax-linear:sun-1' : 'vuesax-linear:moon',
|
||||
)
|
||||
const themeLabel = computed(() =>
|
||||
isDark.value ? 'تغییر به تم روشن' : 'تغییر به تم تاریک',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="auth-theme-toggle"
|
||||
:class="variant === 'inline' ? 'auth-theme-toggle--inline' : 'auth-theme-toggle--floating'"
|
||||
:aria-label="themeLabel"
|
||||
:title="themeLabel"
|
||||
@click="themeStore.toggleLightDark()"
|
||||
>
|
||||
<BasicIcon :icon="themeIcon" :size="18" :stroke-width="1.5" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.auth-theme-toggle--floating {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
inset-inline-end: 16px;
|
||||
z-index: 100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.auth-theme-toggle--inline {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.auth-theme-toggle:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
variant?: 'warning' | 'success' | 'info' | 'purple' | 'danger' | 'neutral'
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="badge" :class="`badge-${variant ?? 'neutral'}`">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
background: var(--color-purple-light);
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
background: var(--color-summary-bg);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
block?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
type?: 'button' | 'submit'
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type ?? 'button'"
|
||||
class="btn"
|
||||
:class="[`btn-${variant ?? 'primary'}`, `btn-${size ?? 'md'}`, { 'btn-block': block }]"
|
||||
:disabled="disabled || loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-spinner" />
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
opacity 0.15s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
min-height: var(--btn-height);
|
||||
padding: 0 24px;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
min-height: 52px;
|
||||
padding: 0 28px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.btn-primary:not(:disabled):hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 1.5px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--color-danger);
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.btn-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string | number
|
||||
label?: string
|
||||
type?: string
|
||||
placeholder?: string
|
||||
hint?: string
|
||||
error?: string
|
||||
disabled?: boolean
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
maxlength?: number | string
|
||||
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
|
||||
autocomplete?: string
|
||||
dir?: string
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
blur: [event: FocusEvent]
|
||||
}>()
|
||||
|
||||
const isNumericField = computed(() => {
|
||||
const type = props.type ?? 'text'
|
||||
return (
|
||||
type === 'number' ||
|
||||
type === 'tel' ||
|
||||
props.inputmode === 'numeric' ||
|
||||
props.inputmode === 'decimal' ||
|
||||
props.inputmode === 'tel' ||
|
||||
props.dir === 'ltr'
|
||||
)
|
||||
})
|
||||
|
||||
/** Numbers default to right; OTP/login can still force center via `align`. */
|
||||
const resolvedAlign = computed(() => props.align ?? (isNumericField.value ? 'end' : 'start'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label v-if="label" class="form-label">{{ label }}</label>
|
||||
<input
|
||||
class="input"
|
||||
:class="[`input--align-${resolvedAlign}`, { 'input-error': error }]"
|
||||
:type="type ?? 'text'"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:maxlength="maxlength"
|
||||
:inputmode="inputmode"
|
||||
:autocomplete="autocomplete"
|
||||
:dir="dir"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('blur', $event)"
|
||||
/>
|
||||
<p v-if="error" class="input-error-text">{{ error }}</p>
|
||||
<p v-else-if="hint" class="form-hint">{{ hint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
height: var(--btn-height);
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9375rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.input--align-start {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.input--align-center {
|
||||
text-align: center;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.input--align-end {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { formatMeterage } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
current: number
|
||||
max: number
|
||||
}>()
|
||||
|
||||
const remaining = computed(() => Math.max(0, props.max - props.current))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="capacity-summary">
|
||||
<div class="capacity-summary__row">
|
||||
<span class="capacity-summary__label">جمع متراژ</span>
|
||||
<span class="capacity-summary__value">{{ formatMeterage(current) }}</span>
|
||||
</div>
|
||||
<div class="capacity-summary__row">
|
||||
<span class="capacity-summary__label">حداکثر مجاز</span>
|
||||
<span class="capacity-summary__value">{{ formatMeterage(max) }}</span>
|
||||
</div>
|
||||
<div class="capacity-summary__row">
|
||||
<span class="capacity-summary__label">باقیمانده</span>
|
||||
<span class="capacity-summary__value capacity-summary__value--accent">
|
||||
{{ formatMeterage(remaining) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.capacity-summary {
|
||||
background: var(--color-summary-bg);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.capacity-summary__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.capacity-summary__row:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.capacity-summary__label {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.capacity-summary__value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.capacity-summary__value--accent {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { formatMeterage, toPersianDigits } from '@/utils/format'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
|
||||
defineProps<{
|
||||
totalMeterage: number
|
||||
itemCount?: number
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-meterage-footer"
|
||||
aria-label="مشاهده سبد خرید"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<span class="cart-meterage-footer__info">
|
||||
<span class="cart-meterage-footer__label">کل متراژ</span>
|
||||
<span class="cart-meterage-footer__value">{{ formatMeterage(totalMeterage) }}</span>
|
||||
</span>
|
||||
|
||||
<span class="cart-meterage-footer__action">
|
||||
<span class="cart-meterage-footer__action-text">مشاهده سبد</span>
|
||||
<span class="cart-meterage-footer__icon-wrap">
|
||||
<IconCart :size="20" />
|
||||
<span v-if="itemCount" class="cart-meterage-footer__badge">
|
||||
{{ toPersianDigits(String(itemCount)) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cart-meterage-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-md);
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
filter 0.15s ease;
|
||||
}
|
||||
|
||||
.cart-meterage-footer:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.cart-meterage-footer:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.cart-meterage-footer__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__label {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__action-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__icon-wrap {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-meterage-footer__badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
inset-inline-start: -4px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { resolveConfirm, useConfirmState } from '@/composables/useConfirm'
|
||||
|
||||
const state = useConfirmState()
|
||||
|
||||
function cancel() {
|
||||
resolveConfirm(false)
|
||||
}
|
||||
|
||||
function confirmAction() {
|
||||
resolveConfirm(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="state.open"
|
||||
class="dialog-overlay confirm-dialog-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-labelledby="'confirm-dialog-title'"
|
||||
@click.self="cancel"
|
||||
>
|
||||
<div class="dialog card confirm-dialog">
|
||||
<h3 id="confirm-dialog-title" class="confirm-dialog__title">{{ state.title }}</h3>
|
||||
<p class="confirm-dialog__message">{{ state.message }}</p>
|
||||
<div class="dialog-actions confirm-dialog__actions">
|
||||
<BaseButton variant="outline" @click="cancel">{{ state.cancelLabel }}</BaseButton>
|
||||
<BaseButton :variant="state.tone === 'danger' ? 'danger' : 'primary'" @click="confirmAction">
|
||||
{{ state.confirmLabel }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirm-dialog {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.confirm-dialog__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.confirm-dialog__message {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.confirm-dialog__actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.confirm-dialog__actions {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { extractNumericString, formatNumber } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
hint?: string
|
||||
error?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const displayValue = ref('')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const numeric = extractNumericString(value ?? '')
|
||||
displayValue.value = numeric ? formatNumber(Number(numeric)) : ''
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function onInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const numeric = extractNumericString(input.value)
|
||||
displayValue.value = numeric ? formatNumber(Number(numeric)) : ''
|
||||
emit('update:modelValue', numeric)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label v-if="label" class="form-label">{{ label }}</label>
|
||||
<input
|
||||
class="input"
|
||||
:class="{ 'input-error': error }"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
dir="ltr"
|
||||
:value="displayValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
@input="onInput"
|
||||
/>
|
||||
<p v-if="error" class="input-error-text">{{ error }}</p>
|
||||
<p v-else-if="hint" class="form-hint">{{ hint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
height: var(--btn-height);
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9375rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeoutFn, onKeyStroke } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useThemeStore, type ThemePreference } from '@/stores/theme'
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
|
||||
const theme = useThemeStore()
|
||||
const saved = ref(false)
|
||||
|
||||
const { start: hideSaved } = useTimeoutFn(() => {
|
||||
saved.value = false
|
||||
}, 2000)
|
||||
|
||||
const themeOptions: {
|
||||
value: ThemePreference
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
}[] = [
|
||||
{
|
||||
value: 'light',
|
||||
label: 'روشن',
|
||||
description: 'پسزمینه روشن و کنتراست بالا',
|
||||
icon: 'vuesax-linear:sun-1',
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: 'تاریک',
|
||||
description: 'مناسب استفاده در شب',
|
||||
icon: 'vuesax-linear:moon',
|
||||
},
|
||||
{
|
||||
value: 'system',
|
||||
label: 'سیستم',
|
||||
description: 'همتراز با تنظیمات دستگاه',
|
||||
icon: 'vuesax-linear:mobile',
|
||||
},
|
||||
]
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function save() {
|
||||
saved.value = true
|
||||
hideSaved()
|
||||
window.setTimeout(() => {
|
||||
close()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
if (open.value) close()
|
||||
})
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (!isOpen) saved.value = false
|
||||
document.body.style.overflow = isOpen ? 'hidden' : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="display-sheet">
|
||||
<div
|
||||
v-if="open"
|
||||
class="display-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="display-settings-title"
|
||||
@click.self="close"
|
||||
>
|
||||
<div class="display-sheet">
|
||||
<div class="display-sheet__handle" aria-hidden="true" />
|
||||
<header class="display-sheet__head">
|
||||
<h2 id="display-settings-title">تنظیمات نمایش</h2>
|
||||
<button type="button" class="display-sheet__close" aria-label="بستن" @click="close">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<fieldset class="display-fieldset">
|
||||
<legend class="display-fieldset__legend">حالت نمایش</legend>
|
||||
<div class="display-grid" role="radiogroup" aria-label="حالت نمایش">
|
||||
<button
|
||||
v-for="option in themeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="theme-card"
|
||||
:class="{ 'theme-card--active': theme.preference === option.value }"
|
||||
role="radio"
|
||||
:aria-checked="theme.preference === option.value"
|
||||
@click="theme.preference = option.value"
|
||||
>
|
||||
<span class="theme-card__icon" aria-hidden="true">
|
||||
<BasicIcon :icon="option.icon" :size="24" :stroke-width="1.5" />
|
||||
</span>
|
||||
<span class="theme-card__body">
|
||||
<strong class="theme-card__label">{{ option.label }}</strong>
|
||||
<span class="theme-card__desc">{{ option.description }}</span>
|
||||
</span>
|
||||
<span class="theme-card__check" aria-hidden="true">
|
||||
<BasicIcon
|
||||
v-if="theme.preference === option.value"
|
||||
icon="vuesax-linear:tick-circle"
|
||||
:size="20"
|
||||
:stroke-width="1.75"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<BaseButton block @click="save">ذخیره تنظیمات</BaseButton>
|
||||
<p v-if="saved" class="display-saved">تنظیمات نمایش ذخیره شد ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.display-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.display-sheet {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: min(88dvh, 640px);
|
||||
overflow-y: auto;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 10px 16px calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.display-sheet__handle {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border);
|
||||
margin: 4px auto 0;
|
||||
}
|
||||
|
||||
.display-sheet__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-sheet__head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.display-sheet__close {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.display-fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.display-fieldset__legend {
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.display-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 24px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-text);
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-card--active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-soft);
|
||||
box-shadow: 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
.theme-card__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.theme-card--active .theme-card__icon {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-card__body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.theme-card__label {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.theme-card__desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.theme-card--active .theme-card__label {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.theme-card__check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.display-saved {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.display-sheet-enter-active,
|
||||
.display-sheet-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.display-sheet-enter-active .display-sheet,
|
||||
.display-sheet-leave-active .display-sheet {
|
||||
transition: transform 0.22s ease;
|
||||
}
|
||||
|
||||
.display-sheet-enter-from,
|
||||
.display-sheet-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.display-sheet-enter-from .display-sheet,
|
||||
.display-sheet-leave-to .display-sheet {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.display-overlay {
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.display-sheet {
|
||||
border-radius: 16px;
|
||||
max-height: min(80dvh, 560px);
|
||||
padding: 20px 20px 24px;
|
||||
}
|
||||
|
||||
.display-sheet__handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
message?: string
|
||||
title?: string
|
||||
/** Context for default icon + copy */
|
||||
variant?: 'default' | 'products' | 'catalogs' | 'orders' | 'cart' | 'checks' | 'search'
|
||||
icon?: string
|
||||
iconSize?: number
|
||||
}>(),
|
||||
{
|
||||
message: undefined,
|
||||
title: undefined,
|
||||
variant: 'default',
|
||||
icon: undefined,
|
||||
iconSize: 72,
|
||||
},
|
||||
)
|
||||
|
||||
const variantIcons: Record<string, string> = {
|
||||
default: 'vuesax-linear:clipboard-close',
|
||||
products: 'vuesax-linear:box-remove',
|
||||
catalogs: 'vuesax-linear:category',
|
||||
orders: 'vuesax-linear:document-text',
|
||||
cart: 'vuesax-linear:bag-cross',
|
||||
checks: 'vuesax-linear:card-receive',
|
||||
search: 'vuesax-linear:search-status',
|
||||
}
|
||||
|
||||
const variantTitles: Record<string, string> = {
|
||||
default: '',
|
||||
products: 'محصولی یافت نشد',
|
||||
catalogs: 'کالیتهای یافت نشد',
|
||||
orders: 'سفارشی یافت نشد',
|
||||
cart: 'سبد خرید خالی است',
|
||||
checks: 'چکی ثبت نشده است',
|
||||
search: 'نتیجهای یافت نشد',
|
||||
}
|
||||
|
||||
const resolvedIcon = computed(
|
||||
() => props.icon ?? variantIcons[props.variant] ?? 'vuesax-linear:clipboard-close',
|
||||
)
|
||||
const resolvedTitle = computed(() => props.title ?? variantTitles[props.variant] ?? '')
|
||||
const resolvedMessage = computed(() => props.message ?? '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__icon" aria-hidden="true">
|
||||
<slot name="icon">
|
||||
<BasicIcon :icon="resolvedIcon" :size="iconSize" :stroke-width="1.25" />
|
||||
</slot>
|
||||
</div>
|
||||
<h2 v-if="resolvedTitle || $slots.title" class="empty-state__title">
|
||||
<slot name="title">{{ resolvedTitle }}</slot>
|
||||
</h2>
|
||||
<p v-if="resolvedMessage || $slots.message" class="empty-state__message">
|
||||
<slot name="message">{{ resolvedMessage }}</slot>
|
||||
</p>
|
||||
<div v-if="$slots.default" class="empty-state__action">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 48px 24px;
|
||||
min-height: 280px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state__icon {
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.empty-state__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-state__message {
|
||||
margin: 0 0 24px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.empty-state__message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.empty-state__action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
tabs: { id: string; label: string }[]
|
||||
equal?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter-tabs" :class="{ 'filter-tabs--equal': equal }" role="tablist">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="filter-tab"
|
||||
:class="{ active: modelValue === tab.id }"
|
||||
:aria-selected="modelValue === tab.id"
|
||||
@click="$emit('update:modelValue', tab.id)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.filter-tabs--equal {
|
||||
gap: 6px;
|
||||
overflow: visible;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex-shrink: 0;
|
||||
height: 36px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: var(--color-tab-inactive-bg);
|
||||
color: var(--color-tab-inactive-text);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.filter-tabs--equal .filter-tab {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 38px;
|
||||
padding: 0 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-tabs--equal .filter-tab.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-tab:not(.active):hover {
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
message?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="message" class="form-error-banner" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-error-banner {
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-danger-light);
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
|
||||
color: var(--color-danger);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { formatMeterage } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
current: number
|
||||
max: number
|
||||
}>()
|
||||
|
||||
const remaining = computed(() => Math.max(0, props.max - props.current))
|
||||
const percentage = computed(() => Math.min(100, (props.current / props.max) * 100))
|
||||
const isNearFull = computed(() => percentage.value >= 80)
|
||||
const isFull = computed(() => props.current >= props.max)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="meterage-bar" :class="{ near: isNearFull, full: isFull }">
|
||||
<div class="meterage-info">
|
||||
<span
|
||||
>متراژ سبد: <strong>{{ formatMeterage(current) }}</strong></span
|
||||
>
|
||||
<span
|
||||
>باقیمانده: <strong>{{ formatMeterage(remaining) }}</strong></span
|
||||
>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" :style="{ width: `${percentage}%` }" />
|
||||
</div>
|
||||
<p class="meterage-max text-sm text-muted">حداکثر مجاز: {{ formatMeterage(max) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.meterage-bar {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.meterage-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-primary);
|
||||
border-radius: 999px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.meterage-bar.near .progress-fill {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
.meterage-bar.full .progress-fill {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.meterage-max {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import SkeletonBlock from '@/components/ui/SkeletonBlock.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** list | cards | grid | detail | profile | home | form */
|
||||
variant?: 'list' | 'cards' | 'grid' | 'detail' | 'profile' | 'home' | 'form'
|
||||
rows?: number
|
||||
}>(),
|
||||
{
|
||||
variant: 'list',
|
||||
rows: 4,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-skeleton" role="status" aria-live="polite" aria-busy="true" aria-label="در حال بارگذاری">
|
||||
<!-- Home -->
|
||||
<template v-if="variant === 'home'">
|
||||
<div class="page-skeleton__banner">
|
||||
<SkeletonBlock height="148px" radius="16px" />
|
||||
</div>
|
||||
<div class="page-skeleton__quick">
|
||||
<div v-for="n in 4" :key="`q-${n}`" class="page-skeleton__quick-item">
|
||||
<SkeletonBlock circle width="48px" height="48px" />
|
||||
<SkeletonBlock width="64%" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBlock width="40%" height="16px" />
|
||||
<div class="page-skeleton__grid">
|
||||
<div v-for="n in 4" :key="`g-${n}`" class="page-skeleton__card">
|
||||
<SkeletonBlock height="110px" radius="12px" />
|
||||
<SkeletonBlock width="80%" height="12px" />
|
||||
<SkeletonBlock width="50%" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Profile -->
|
||||
<template v-else-if="variant === 'profile'">
|
||||
<div class="page-skeleton__profile-hero">
|
||||
<SkeletonBlock circle width="72px" height="72px" />
|
||||
<SkeletonBlock width="45%" height="18px" />
|
||||
<SkeletonBlock width="30%" height="12px" />
|
||||
</div>
|
||||
<div class="page-skeleton__panel">
|
||||
<div v-for="n in 4" :key="`s-${n}`" class="page-skeleton__stat">
|
||||
<SkeletonBlock width="50%" height="10px" />
|
||||
<SkeletonBlock width="70%" height="18px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-skeleton__list">
|
||||
<div v-for="n in 5" :key="`m-${n}`" class="page-skeleton__row">
|
||||
<SkeletonBlock circle width="36px" height="36px" />
|
||||
<div class="page-skeleton__row-text">
|
||||
<SkeletonBlock width="55%" height="12px" />
|
||||
<SkeletonBlock width="35%" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Detail -->
|
||||
<template v-else-if="variant === 'detail'">
|
||||
<SkeletonBlock height="220px" radius="0" />
|
||||
<div class="page-skeleton__detail-body">
|
||||
<SkeletonBlock width="70%" height="20px" />
|
||||
<SkeletonBlock width="40%" height="12px" />
|
||||
<SkeletonBlock height="72px" radius="12px" />
|
||||
<SkeletonBlock width="100%" height="12px" />
|
||||
<SkeletonBlock width="92%" height="12px" />
|
||||
<SkeletonBlock width="85%" height="12px" />
|
||||
<SkeletonBlock height="48px" radius="12px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Product / category grid -->
|
||||
<template v-else-if="variant === 'grid' || variant === 'cards'">
|
||||
<SkeletonBlock width="55%" height="16px" />
|
||||
<div class="page-skeleton__chips">
|
||||
<SkeletonBlock v-for="n in 4" :key="`c-${n}`" width="72px" height="32px" radius="999px" />
|
||||
</div>
|
||||
<div class="page-skeleton__grid">
|
||||
<div v-for="n in rows" :key="`card-${n}`" class="page-skeleton__card">
|
||||
<SkeletonBlock height="120px" radius="12px" />
|
||||
<SkeletonBlock width="85%" height="12px" />
|
||||
<SkeletonBlock width="55%" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Form -->
|
||||
<template v-else-if="variant === 'form'">
|
||||
<div class="page-skeleton__form">
|
||||
<div v-for="n in rows" :key="`f-${n}`" class="page-skeleton__field">
|
||||
<SkeletonBlock width="30%" height="10px" />
|
||||
<SkeletonBlock height="44px" radius="10px" />
|
||||
</div>
|
||||
<SkeletonBlock height="48px" radius="12px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Default list -->
|
||||
<template v-else>
|
||||
<div class="page-skeleton__list">
|
||||
<div v-for="n in rows" :key="`r-${n}`" class="page-skeleton__row page-skeleton__row--card">
|
||||
<SkeletonBlock circle width="40px" height="40px" />
|
||||
<div class="page-skeleton__row-text">
|
||||
<SkeletonBlock width="62%" height="13px" />
|
||||
<SkeletonBlock width="42%" height="10px" />
|
||||
<SkeletonBlock width="28%" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 4px 0 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-skeleton__banner,
|
||||
.page-skeleton__panel,
|
||||
.page-skeleton__detail-body,
|
||||
.page-skeleton__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-skeleton__quick {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.page-skeleton__quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-skeleton__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__profile-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px 0 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: var(--color-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.page-skeleton__stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.page-skeleton__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-skeleton__row--card {
|
||||
padding: 12px;
|
||||
background: var(--color-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.page-skeleton__row-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-skeleton__detail-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-skeleton__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page-skeleton__grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,336 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
JALALI_MONTH_NAMES,
|
||||
JALALI_WEEKDAY_NAMES,
|
||||
buildCalendarDays,
|
||||
formatJalaliLabel,
|
||||
isoToJalali,
|
||||
jalaliToISO,
|
||||
todayJalali,
|
||||
} from '@/utils/jalali'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
hint?: string
|
||||
error?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const isOpen = ref(false)
|
||||
const viewYear = ref(todayJalali().jy)
|
||||
const viewMonth = ref(todayJalali().jm)
|
||||
|
||||
const selectedJalali = computed(() => isoToJalali(props.modelValue ?? ''))
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (!selectedJalali.value) return ''
|
||||
const { jy, jm, jd } = selectedJalali.value
|
||||
return formatJalaliLabel(jy, jm, jd)
|
||||
})
|
||||
|
||||
const calendarDays = computed(() => buildCalendarDays(viewYear.value, viewMonth.value))
|
||||
|
||||
const monthLabel = computed(
|
||||
() => `${JALALI_MONTH_NAMES[viewMonth.value - 1]} ${toPersianDigits(String(viewYear.value))}`,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const jalali = isoToJalali(value ?? '')
|
||||
if (jalali) {
|
||||
viewYear.value = jalali.jy
|
||||
viewMonth.value = jalali.jm
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function toggleCalendar() {
|
||||
if (props.disabled) return
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
function closeCalendar() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function selectDay(day: number) {
|
||||
emit('update:modelValue', jalaliToISO(viewYear.value, viewMonth.value, day))
|
||||
closeCalendar()
|
||||
}
|
||||
|
||||
function changeMonth(delta: number) {
|
||||
let month = viewMonth.value + delta
|
||||
let year = viewYear.value
|
||||
|
||||
if (month > 12) {
|
||||
month = 1
|
||||
year += 1
|
||||
} else if (month < 1) {
|
||||
month = 12
|
||||
year -= 1
|
||||
}
|
||||
|
||||
viewMonth.value = month
|
||||
viewYear.value = year
|
||||
}
|
||||
|
||||
function selectToday() {
|
||||
const today = todayJalali()
|
||||
viewYear.value = today.jy
|
||||
viewMonth.value = today.jm
|
||||
emit('update:modelValue', jalaliToISO(today.jy, today.jm, today.jd))
|
||||
closeCalendar()
|
||||
}
|
||||
|
||||
function isSelected(day: number) {
|
||||
if (!selectedJalali.value) return false
|
||||
const { jy, jm, jd } = selectedJalali.value
|
||||
return jy === viewYear.value && jm === viewMonth.value && jd === day
|
||||
}
|
||||
|
||||
function isToday(day: number) {
|
||||
const today = todayJalali()
|
||||
return today.jy === viewYear.value && today.jm === viewMonth.value && today.jd === day
|
||||
}
|
||||
|
||||
function onDocumentClick(event: MouseEvent) {
|
||||
if (!rootRef.value?.contains(event.target as Node)) {
|
||||
closeCalendar()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rootRef" class="form-group persian-date-input">
|
||||
<label v-if="label" class="form-label">{{ label }}</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="input persian-date-input__trigger"
|
||||
:class="{ 'input-error': error, 'is-open': isOpen }"
|
||||
:disabled="disabled"
|
||||
@click.stop="toggleCalendar"
|
||||
>
|
||||
<span :class="{ 'persian-date-input__placeholder': !displayValue }">
|
||||
{{ displayValue || placeholder || 'انتخاب تاریخ' }}
|
||||
</span>
|
||||
<span class="persian-date-input__icon" aria-hidden="true">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="5" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M8 3v4M16 3v4M3 10h18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div v-if="isOpen" class="persian-date-input__panel" @click.stop>
|
||||
<div class="persian-date-input__header">
|
||||
<button type="button" class="persian-date-input__nav" @click="changeMonth(-1)">‹</button>
|
||||
<strong class="persian-date-input__month">{{ monthLabel }}</strong>
|
||||
<button type="button" class="persian-date-input__nav" @click="changeMonth(1)">›</button>
|
||||
</div>
|
||||
|
||||
<div class="persian-date-input__weekdays">
|
||||
<span v-for="weekday in JALALI_WEEKDAY_NAMES" :key="weekday">{{ weekday }}</span>
|
||||
</div>
|
||||
|
||||
<div class="persian-date-input__grid">
|
||||
<template v-for="(day, index) in calendarDays" :key="index">
|
||||
<span v-if="day === null" class="persian-date-input__cell persian-date-input__cell--empty" />
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="persian-date-input__cell"
|
||||
:class="{
|
||||
'persian-date-input__cell--selected': isSelected(day),
|
||||
'persian-date-input__cell--today': isToday(day),
|
||||
}"
|
||||
@click="selectDay(day)"
|
||||
>
|
||||
{{ toPersianDigits(String(day)) }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button type="button" class="persian-date-input__today" @click="selectToday">امروز</button>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="input-error-text">{{ error }}</p>
|
||||
<p v-else-if="hint" class="form-hint">{{ hint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.persian-date-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: var(--btn-height);
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9375rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.persian-date-input__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.persian-date-input__trigger:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.persian-date-input__trigger.is-open,
|
||||
.persian-date-input__trigger:focus-visible {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.persian-date-input__trigger:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.persian-date-input__placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.persian-date-input__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.persian-date-input__panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 120;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.persian-date-input__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.persian-date-input__month {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.persian-date-input__nav {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.persian-date-input__weekdays,
|
||||
.persian-date-input__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.persian-date-input__weekdays {
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.persian-date-input__cell {
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.persian-date-input__cell--empty {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.persian-date-input__cell--today {
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.persian-date-input__cell--selected {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary, #fff);
|
||||
}
|
||||
|
||||
.persian-date-input__today {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.persian-date-input__today:hover {
|
||||
background: var(--color-primary-soft);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-field">
|
||||
<input
|
||||
class="search-field__input"
|
||||
type="search"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-field__input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.search-field__input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.search-field__input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
width?: string
|
||||
height?: string
|
||||
radius?: string
|
||||
circle?: boolean
|
||||
}>(),
|
||||
{
|
||||
width: '100%',
|
||||
height: '14px',
|
||||
radius: '8px',
|
||||
circle: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="sk"
|
||||
:class="{ 'sk--circle': circle }"
|
||||
:style="{
|
||||
width,
|
||||
height,
|
||||
borderRadius: circle ? '50%' : radius,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sk {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--color-border) 55%, var(--color-surface)) 0%,
|
||||
color-mix(in srgb, var(--color-border) 85%, var(--color-elevated, var(--color-surface))) 45%,
|
||||
color-mix(in srgb, var(--color-border) 55%, var(--color-surface)) 100%
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
animation: sk-shimmer 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sk--circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes sk-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sk {
|
||||
animation: none;
|
||||
background: var(--color-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import BaseBadge from './BaseBadge.vue'
|
||||
import { ORDER_STATUS_LABELS, STATUS_BADGE_VARIANT } from '@/utils/constants'
|
||||
import type { OrderStatus } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
status: OrderStatus
|
||||
}>()
|
||||
|
||||
const label = computed(() => ORDER_STATUS_LABELS[props.status] ?? props.status)
|
||||
const variant = computed(() => STATUS_BADGE_VARIANT[props.status] ?? 'neutral')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBadge :variant="variant">{{ label }}</BaseBadge>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
src?: string | null
|
||||
initials: string
|
||||
size?: number
|
||||
}>(),
|
||||
{ size: 72 },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="user-avatar"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.38)}px` }"
|
||||
>
|
||||
<img v-if="src" class="user-avatar__img" :src="src" alt="" />
|
||||
<span v-else>{{ initials }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-avatar {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { confirm } from '@/composables/useConfirm'
|
||||
|
||||
export function useCartPage() {
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const showFullDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const submitError = ref('')
|
||||
|
||||
const items = computed(() => cartStore.activeCart?.items ?? [])
|
||||
const isEmpty = computed(() => items.value.length === 0)
|
||||
const companyName = computed(() => cartStore.companyNamesLabel || cartStore.activeCart?.companyName || '')
|
||||
const companySummaries = computed(() => cartStore.companySummaries)
|
||||
|
||||
async function removeItem(itemId: string) {
|
||||
const item = items.value.find((entry) => entry.id === itemId)
|
||||
const ok = await confirm({
|
||||
title: 'حذف از سبد',
|
||||
message: item
|
||||
? `آیا از حذف «${item.productName}» از سبد خرید مطمئن هستید؟`
|
||||
: 'آیا از حذف این آیتم از سبد خرید مطمئن هستید؟',
|
||||
confirmLabel: 'حذف',
|
||||
cancelLabel: 'انصراف',
|
||||
tone: 'danger',
|
||||
})
|
||||
if (!ok) return
|
||||
cartStore.removeItem(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* ثبت سفارش بدون انتخاب تسویه؛ قیمتگذاری و انتخاب روش تسویه
|
||||
* بعد از بررسی مدیریت انجام میشود.
|
||||
*/
|
||||
async function submitOrder() {
|
||||
if (!cartStore.activeCart || items.value.length === 0 || submitting.value) return
|
||||
submitting.value = true
|
||||
submitError.value = ''
|
||||
try {
|
||||
await cartStore.hydrate()
|
||||
if (!cartStore.activeCart?.items.length) {
|
||||
submitError.value = 'سبد خرید خالی است یا با سرور همگام نشد.'
|
||||
return
|
||||
}
|
||||
const order = await orderStore.submitOrder(cartStore.activeCart)
|
||||
await cartStore.hydrate()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
} catch (e) {
|
||||
submitError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
await cartStore.hydrate()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function proceedToCheckout() {
|
||||
if (items.value.length === 0) return
|
||||
if (cartStore.isFull) {
|
||||
showFullDialog.value = true
|
||||
return
|
||||
}
|
||||
void submitOrder()
|
||||
}
|
||||
|
||||
function proceedAnyway() {
|
||||
showFullDialog.value = false
|
||||
void submitOrder()
|
||||
}
|
||||
|
||||
async function createNewInvoice() {
|
||||
await cartStore.createNewCartForSameCompany()
|
||||
showFullDialog.value = false
|
||||
const companyId = cartStore.selectedCompanyId || cartStore.activeCart?.companyId
|
||||
if (companyId) {
|
||||
router.push(`/companies/${companyId}/catalogs`)
|
||||
} else {
|
||||
router.push('/companies')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cartStore.ensureActiveCartForCartPage()
|
||||
})
|
||||
|
||||
return {
|
||||
cartStore,
|
||||
items,
|
||||
isEmpty,
|
||||
companyName,
|
||||
companySummaries,
|
||||
showFullDialog,
|
||||
submitting,
|
||||
submitError,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
createNewInvoice,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
tone?: 'danger' | 'primary'
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
open: boolean
|
||||
resolve: ((value: boolean) => void) | null
|
||||
}
|
||||
|
||||
const state = reactive<ConfirmState>({
|
||||
open: false,
|
||||
title: '',
|
||||
message: '',
|
||||
confirmLabel: 'حذف',
|
||||
cancelLabel: 'انصراف',
|
||||
tone: 'danger',
|
||||
resolve: null,
|
||||
})
|
||||
|
||||
export function useConfirmState() {
|
||||
return state
|
||||
}
|
||||
|
||||
export function confirm(options: ConfirmOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
Object.assign(state, {
|
||||
open: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
confirmLabel: options.confirmLabel ?? 'حذف',
|
||||
cancelLabel: options.cancelLabel ?? 'انصراف',
|
||||
tone: options.tone ?? 'danger',
|
||||
resolve,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveConfirm(value: boolean) {
|
||||
state.resolve?.(value)
|
||||
state.open = false
|
||||
state.resolve = null
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
/** بازگشت با تاریخچه؛ اگر تاریخچه نباشد به مسیر جایگزین میرود */
|
||||
export function useGoBack(fallback = '/') {
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
const hasHistory = window.history.length > 1
|
||||
if (hasHistory) {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
router.push(fallback)
|
||||
}
|
||||
|
||||
return { goBack }
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import type { Order } from '@/types'
|
||||
import {
|
||||
DELIVERED_STATUSES,
|
||||
IN_PROGRESS_STATUSES,
|
||||
REVIEW_STATUSES,
|
||||
matchesDesktopOrderFilter,
|
||||
matchesMobileOrderTab,
|
||||
type DesktopOrderFilterId,
|
||||
} from '@/utils/orderStatusGroups'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
|
||||
const DESKTOP_PAGE_SIZE = 5
|
||||
const MOBILE_PAGE_SIZE = 5
|
||||
|
||||
export function useOrderList() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const currentPage = ref(1)
|
||||
const mobilePage = ref(1)
|
||||
const submitSuccessMessage = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([orderStore.hydrate(), catalogStore.loadCompanies()])
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.submitted,
|
||||
(submitted) => {
|
||||
const count = Number(submitted)
|
||||
if (!Number.isFinite(count) || count < 1) {
|
||||
submitSuccessMessage.value = ''
|
||||
return
|
||||
}
|
||||
submitSuccessMessage.value =
|
||||
count === 1
|
||||
? 'پیشسفارش با موفقیت ثبت شد.'
|
||||
: `${toPersianDigits(String(count))} پیشسفارش (برای شرکتهای مختلف) ثبت شد.`
|
||||
const { submitted: _submitted, ...rest } = route.query
|
||||
void router.replace({ query: rest })
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const tab = computed({
|
||||
get: () => (router.currentRoute.value.query.tab as string) ?? 'all',
|
||||
set: (v: string) => router.replace({ query: { ...router.currentRoute.value.query, tab: v } }),
|
||||
})
|
||||
|
||||
const desktopFilter = computed({
|
||||
get: () => (router.currentRoute.value.query.filter as DesktopOrderFilterId) ?? 'all',
|
||||
set: (v: DesktopOrderFilterId) =>
|
||||
router.replace({
|
||||
query: { ...router.currentRoute.value.query, filter: v === 'all' ? undefined : v },
|
||||
}),
|
||||
})
|
||||
|
||||
const searchQuery = computed(() => (router.currentRoute.value.query.q as string) ?? '')
|
||||
|
||||
const orders = computed(() => {
|
||||
if (!authStore.user) return []
|
||||
return orderStore.userOrders(authStore.user.id)
|
||||
})
|
||||
|
||||
const stats = computed(() => ({
|
||||
total: orders.value.length,
|
||||
review: orders.value.filter((o) => REVIEW_STATUSES.includes(o.status)).length,
|
||||
inProgress: orders.value.filter((o) => IN_PROGRESS_STATUSES.includes(o.status)).length,
|
||||
done: orders.value.filter((o) => DELIVERED_STATUSES.includes(o.status)).length,
|
||||
}))
|
||||
|
||||
function matchesSearch(order: Order, q: string): boolean {
|
||||
if (!q.trim()) return true
|
||||
const needle = q.trim().toLowerCase()
|
||||
return (
|
||||
order.orderNumber.toLowerCase().includes(needle) ||
|
||||
order.companyName.toLowerCase().includes(needle)
|
||||
)
|
||||
}
|
||||
|
||||
const filteredOrdersMobile = computed(() =>
|
||||
orders.value
|
||||
.filter((o) => matchesSearch(o, searchQuery.value))
|
||||
.filter((o) => matchesMobileOrderTab(o.status, tab.value)),
|
||||
)
|
||||
|
||||
const filteredOrdersDesktop = computed(() =>
|
||||
orders.value
|
||||
.filter((o) => matchesSearch(o, searchQuery.value))
|
||||
.filter((o) => matchesDesktopOrderFilter(o.status, desktopFilter.value)),
|
||||
)
|
||||
|
||||
const paginatedOrders = computed(() => {
|
||||
const start = (currentPage.value - 1) * DESKTOP_PAGE_SIZE
|
||||
return filteredOrdersDesktop.value.slice(start, start + DESKTOP_PAGE_SIZE)
|
||||
})
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredOrdersDesktop.value.length / DESKTOP_PAGE_SIZE)),
|
||||
)
|
||||
|
||||
const paginationRange = computed(() => {
|
||||
if (filteredOrdersDesktop.value.length === 0) return { start: 0, end: 0 }
|
||||
const start = (currentPage.value - 1) * DESKTOP_PAGE_SIZE + 1
|
||||
const end = Math.min(currentPage.value * DESKTOP_PAGE_SIZE, filteredOrdersDesktop.value.length)
|
||||
return { start, end }
|
||||
})
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = totalPages.value
|
||||
const current = currentPage.value
|
||||
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
|
||||
|
||||
const pages: (number | 'ellipsis')[] = [1]
|
||||
if (current > 3) pages.push('ellipsis')
|
||||
|
||||
const rangeStart = Math.max(2, current - 1)
|
||||
const rangeEnd = Math.min(total - 1, current + 1)
|
||||
for (let p = rangeStart; p <= rangeEnd; p += 1) pages.push(p)
|
||||
|
||||
if (current < total - 2) pages.push('ellipsis')
|
||||
if (total > 1) pages.push(total)
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
const mobileTotalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredOrdersMobile.value.length / MOBILE_PAGE_SIZE)),
|
||||
)
|
||||
|
||||
const paginatedOrdersMobile = computed(() => {
|
||||
const start = (mobilePage.value - 1) * MOBILE_PAGE_SIZE
|
||||
return filteredOrdersMobile.value.slice(start, start + MOBILE_PAGE_SIZE)
|
||||
})
|
||||
|
||||
const mobilePaginationRange = computed(() => {
|
||||
if (filteredOrdersMobile.value.length === 0) return { start: 0, end: 0 }
|
||||
const start = (mobilePage.value - 1) * MOBILE_PAGE_SIZE + 1
|
||||
const end = Math.min(mobilePage.value * MOBILE_PAGE_SIZE, filteredOrdersMobile.value.length)
|
||||
return { start, end }
|
||||
})
|
||||
|
||||
const mobileVisiblePages = computed(() => {
|
||||
const total = mobileTotalPages.value
|
||||
const current = mobilePage.value
|
||||
if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)
|
||||
|
||||
const pages: (number | 'ellipsis')[] = [1]
|
||||
if (current > 3) pages.push('ellipsis')
|
||||
|
||||
const rangeStart = Math.max(2, current - 1)
|
||||
const rangeEnd = Math.min(total - 1, current + 1)
|
||||
for (let p = rangeStart; p <= rangeEnd; p += 1) pages.push(p)
|
||||
|
||||
if (current < total - 2) pages.push('ellipsis')
|
||||
if (total > 1) pages.push(total)
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
watch([filteredOrdersDesktop, desktopFilter, searchQuery], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
watch([filteredOrdersMobile, tab, searchQuery], () => {
|
||||
mobilePage.value = 1
|
||||
})
|
||||
|
||||
function companySlug(order: Order) {
|
||||
return catalogStore.getCompany(order.companyId)?.slug ?? 'par'
|
||||
}
|
||||
|
||||
function iconVariant(order: Order) {
|
||||
if (DELIVERED_STATUSES.includes(order.status)) return 'success'
|
||||
return companySlug(order) === 'fam-baft' ? 'fam-baft' : 'par'
|
||||
}
|
||||
|
||||
function goToOrder(id: string) {
|
||||
router.push(`/orders/${id}`)
|
||||
}
|
||||
|
||||
function setPage(page: number) {
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function setMobilePage(page: number) {
|
||||
if (page < 1 || page > mobileTotalPages.value) return
|
||||
mobilePage.value = page
|
||||
}
|
||||
|
||||
function setDesktopFilter(filter: DesktopOrderFilterId) {
|
||||
desktopFilter.value = desktopFilter.value === filter && filter !== 'all' ? 'all' : filter
|
||||
}
|
||||
|
||||
function viewStatFilter(filter: DesktopOrderFilterId) {
|
||||
desktopFilter.value = filter
|
||||
}
|
||||
|
||||
return {
|
||||
tab,
|
||||
desktopFilter,
|
||||
searchQuery,
|
||||
stats,
|
||||
filteredOrdersMobile,
|
||||
filteredOrdersDesktop,
|
||||
paginatedOrders,
|
||||
paginatedOrdersMobile,
|
||||
totalPages,
|
||||
mobileTotalPages,
|
||||
paginationRange,
|
||||
mobilePaginationRange,
|
||||
visiblePages,
|
||||
mobileVisiblePages,
|
||||
currentPage,
|
||||
mobilePage,
|
||||
companySlug,
|
||||
iconVariant,
|
||||
submitSuccessMessage,
|
||||
goToOrder,
|
||||
setPage,
|
||||
setMobilePage,
|
||||
setDesktopFilter,
|
||||
viewStatFilter,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
|
||||
export function useProductOrdering() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const catalogStore = useCatalogStore()
|
||||
const cartStore = useCartStore()
|
||||
const loading = ref(true)
|
||||
|
||||
const companyId = computed(() => route.params.companyId as string)
|
||||
const catalogId = computed(() => route.params.catalogId as string)
|
||||
const catalog = computed(() => catalogStore.getCatalog(catalogId.value))
|
||||
const company = computed(() => catalogStore.getCompany(companyId.value))
|
||||
|
||||
const colorFilter = ref('all')
|
||||
const colors = computed(() => ['all', ...catalogStore.getUniqueColors(catalogId.value)])
|
||||
const colorLabels: Record<string, string> = { all: 'همه' }
|
||||
|
||||
const products = computed(() =>
|
||||
catalogStore.fetchProducts(
|
||||
catalogId.value,
|
||||
colorFilter.value === 'all' ? undefined : colorFilter.value,
|
||||
),
|
||||
)
|
||||
|
||||
const meterages = ref<Record<string, number>>({})
|
||||
const errors = ref<Record<string, string>>({})
|
||||
const toast = ref('')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
if (!catalogStore.getCompany(companyId.value)) {
|
||||
await catalogStore.loadCompanies()
|
||||
}
|
||||
if (!catalogStore.getCatalog(catalogId.value)) {
|
||||
await catalogStore.loadCatalogs(companyId.value)
|
||||
}
|
||||
await catalogStore.loadProducts(catalogId.value)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
watch([companyId, catalogId], load)
|
||||
watch(colorFilter, async (color) => {
|
||||
await catalogStore.loadProducts(
|
||||
catalogId.value,
|
||||
color === 'all' ? undefined : color,
|
||||
)
|
||||
})
|
||||
|
||||
function getMeterage(productId: string) {
|
||||
return meterages.value[productId] ?? 0
|
||||
}
|
||||
|
||||
function adjustMeterage(productId: string, delta: number, step: number) {
|
||||
const current = getMeterage(productId)
|
||||
meterages.value[productId] = Math.max(0, current + delta * step)
|
||||
}
|
||||
|
||||
async function addProduct(productId: string) {
|
||||
const product = catalogStore.getProduct(productId)
|
||||
if (!product || !catalog.value) {
|
||||
errors.value = {
|
||||
...errors.value,
|
||||
[productId]: 'اطلاعات محصول هنوز آماده نیست. صفحه را تازه کنید.',
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const meterage = getMeterage(productId)
|
||||
if (meterage <= 0) {
|
||||
errors.value = { ...errors.value, [productId]: 'متراژ را وارد کنید.' }
|
||||
return
|
||||
}
|
||||
|
||||
const result = await cartStore.addItem(product, catalog.value.code, meterage)
|
||||
if (result.success) {
|
||||
errors.value = { ...errors.value, [productId]: '' }
|
||||
toast.value = `${product.name} به سبد اضافه شد.`
|
||||
setTimeout(() => {
|
||||
toast.value = ''
|
||||
}, 2000)
|
||||
} else {
|
||||
errors.value = { ...errors.value, [productId]: result.message ?? 'خطا' }
|
||||
}
|
||||
}
|
||||
|
||||
function setMeterage(productId: string, value: number) {
|
||||
meterages.value[productId] = Math.max(0, value)
|
||||
}
|
||||
|
||||
function goToCart() {
|
||||
router.push('/cart')
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push(`/companies/${companyId.value}/catalogs`)
|
||||
}
|
||||
|
||||
return {
|
||||
companyId,
|
||||
catalogId,
|
||||
catalog,
|
||||
company,
|
||||
colorFilter,
|
||||
colors,
|
||||
colorLabels,
|
||||
products,
|
||||
meterages,
|
||||
errors,
|
||||
toast,
|
||||
loading,
|
||||
cartStore,
|
||||
adjustMeterage,
|
||||
setMeterage,
|
||||
addProduct,
|
||||
goToCart,
|
||||
goBack,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { isInvoicePriced } from '@/utils/orderInvoice'
|
||||
import type { Order } from '@/types'
|
||||
|
||||
/**
|
||||
* مسیرهای تسویه/چک فقط بعد از قیمتگذاری ادمین مجازند.
|
||||
* در غیر این صورت به جزئیات سفارش برمیگردیم.
|
||||
*/
|
||||
export function useRequirePricedOrder() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const invoiceReady = computed(() => isInvoicePriced(order.value))
|
||||
|
||||
function redirectIfNeeded(current: Order | null | undefined) {
|
||||
if (!current) return
|
||||
if (!isInvoicePriced(current)) {
|
||||
router.replace(`/orders/${current.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
const current = orderStore.getOrderById(id) ?? (await orderStore.fetchOrder(id))
|
||||
redirectIfNeeded(current)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(order, (current) => {
|
||||
redirectIfNeeded(current)
|
||||
})
|
||||
|
||||
return { orderId, order, invoiceReady }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ProductCategory, SeasonCategory } from '@/types/crm'
|
||||
|
||||
/** دستهبندی کاربرد/فصل — تاکسونومی ثابت اپ (همتراز با سرور) */
|
||||
export const CATALOG_USAGE_CATEGORIES: ProductCategory[] = [
|
||||
{ id: 'trousers', title: 'پارچههای شلواری', subtitle: 'مشاهده محصولات' },
|
||||
{ id: 'suit', title: 'پارچههای کتشلواری', subtitle: 'مشاهده محصولات' },
|
||||
{ id: 'shirt', title: 'پارچههای پیراهنی', subtitle: 'مشاهده محصولات' },
|
||||
]
|
||||
|
||||
export const CATALOG_SEASON_CATEGORIES: SeasonCategory[] = [
|
||||
{
|
||||
id: 'summer',
|
||||
title: 'تابستانه',
|
||||
description: 'سبک، خنک و مناسب فصل گرم',
|
||||
},
|
||||
{
|
||||
id: 'winter',
|
||||
title: 'زمستانه',
|
||||
description: 'گرم و مناسب فصل سرد',
|
||||
},
|
||||
{
|
||||
id: 'all_season',
|
||||
title: 'چهارفصل',
|
||||
description: 'مناسب استفاده در بیشتر فصلهای سال',
|
||||
},
|
||||
]
|
||||
|
||||
export function getCatalogCategories() {
|
||||
return {
|
||||
usage: CATALOG_USAGE_CATEGORIES,
|
||||
seasons: CATALOG_SEASON_CATEGORIES,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BottomNav from '@/components/layout/BottomNav.vue'
|
||||
import DesktopSidebar from '@/components/layout/DesktopSidebar.vue'
|
||||
import DesktopHeader from '@/components/layout/DesktopHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const showNav = computed(() => {
|
||||
const noNavRoutes = [
|
||||
'login',
|
||||
'register',
|
||||
'otp',
|
||||
'settlement',
|
||||
'check-mode',
|
||||
'check-form',
|
||||
'check-list',
|
||||
'cash-settlement',
|
||||
'order-success',
|
||||
]
|
||||
return !noNavRoutes.includes(route.name as string) && !route.meta.hideNav
|
||||
})
|
||||
|
||||
const isWideGuest = computed(() => {
|
||||
const wideRoutes = [
|
||||
'settlement',
|
||||
'check-mode',
|
||||
'check-form',
|
||||
'check-list',
|
||||
'cash-settlement',
|
||||
]
|
||||
return wideRoutes.includes(route.name as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-shell"
|
||||
:class="{
|
||||
'app-shell--no-nav': !showNav,
|
||||
'app-shell--wide': isWideGuest,
|
||||
'app-shell--with-nav': showNav,
|
||||
}"
|
||||
>
|
||||
<div class="app-layout">
|
||||
<DesktopSidebar v-if="showNav" />
|
||||
<div class="app-frame">
|
||||
<DesktopHeader v-if="showNav" />
|
||||
<main class="app-main">
|
||||
<div class="app-main__content">
|
||||
<div class="app-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav v-if="showNav" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import 'virtual:uno.css'
|
||||
import './assets/styles/fonts.css'
|
||||
import './assets/styles/main.css'
|
||||
import './assets/styles/app-shell.css'
|
||||
import './assets/styles/responsive.css'
|
||||
import './assets/styles/desktop.css'
|
||||
import './assets/styles/desktop-pages.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
useThemeStore(pinia)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,254 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getCachedUser, isAuthenticated } from '@/services/authService'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/auth/LoginView.vue'),
|
||||
meta: { guest: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import('@/views/auth/RegisterView.vue'),
|
||||
meta: { guest: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/otp',
|
||||
name: 'otp',
|
||||
component: () => import('@/views/auth/OtpView.vue'),
|
||||
meta: { guest: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('@/views/home/HomeView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/companies',
|
||||
name: 'companies',
|
||||
component: () => import('@/views/companies/CompanySelectView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
name: 'categories',
|
||||
component: () => import('@/views/categories/CategoriesView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/products',
|
||||
name: 'products',
|
||||
component: () => import('@/views/products/ProductsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/product',
|
||||
name: 'product',
|
||||
component: () => import('@/views/products/ProductDetailView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/catalogs',
|
||||
name: 'catalogs',
|
||||
component: () => import('@/views/catalogs/CatalogsHubView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/catalog-request',
|
||||
name: 'catalog-request',
|
||||
component: () => import('@/views/catalogs/CatalogRequestView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/catalog-requests',
|
||||
name: 'catalog-requests',
|
||||
component: () => import('@/views/catalogs/CatalogRequestsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/favorites',
|
||||
name: 'favorites',
|
||||
component: () => import('@/views/products/FavoritesView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/customers',
|
||||
name: 'customers',
|
||||
component: () => import('@/views/customers/CustomersView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/customers/new',
|
||||
name: 'customer-new',
|
||||
component: () => import('@/views/customers/CustomerCreateView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/customer',
|
||||
name: 'customer',
|
||||
component: () => import('@/views/customers/CustomerDetailView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/meeting-report',
|
||||
name: 'meeting-report',
|
||||
component: () => import('@/views/customers/MeetingReportView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/reminders',
|
||||
name: 'reminders',
|
||||
component: () => import('@/views/reminders/RemindersView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/reminder',
|
||||
name: 'reminder',
|
||||
component: () => import('@/views/reminders/ReminderDetailView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/companies/:companyId/catalogs',
|
||||
name: 'company-catalogs',
|
||||
component: () => import('@/views/catalogs/CatalogListView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/companies/:companyId/catalogs/:catalogId/products',
|
||||
name: 'company-products',
|
||||
component: () => import('@/views/products/ProductListView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/cart',
|
||||
name: 'cart',
|
||||
component: () => import('@/views/cart/CartView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'orders',
|
||||
component: () => import('@/views/OrdersView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/order',
|
||||
name: 'order-query',
|
||||
redirect: (to) => {
|
||||
const id = String(to.query.id ?? '')
|
||||
if (!id) return { name: 'orders' }
|
||||
return { path: `/orders/${id}` }
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/orders/:id',
|
||||
name: 'order-detail',
|
||||
component: () => import('@/views/orders/OrderDetailView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/success',
|
||||
name: 'order-success',
|
||||
component: () => import('@/views/orders/OrderSuccessView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/settlement',
|
||||
name: 'settlement',
|
||||
component: () => import('@/views/checkout/SettlementTypeView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/cash',
|
||||
name: 'cash-settlement',
|
||||
component: () => import('@/views/checkout/CashSettlementView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-mode',
|
||||
name: 'check-mode',
|
||||
component: () => import('@/views/checkout/CheckModeSelectView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-form',
|
||||
name: 'check-form',
|
||||
component: () => import('@/views/checkout/CheckFormView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-list',
|
||||
name: 'check-list',
|
||||
component: () => import('@/views/checkout/CheckListView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: () => import('@/views/profile/ProfileView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile/edit',
|
||||
name: 'profile-edit',
|
||||
component: () => import('@/views/profile/ProfileEditView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile/security',
|
||||
name: 'profile-security',
|
||||
component: () => import('@/views/profile/ProfileSecurityView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile/notifications',
|
||||
name: 'profile-notifications',
|
||||
component: () => import('@/views/profile/ProfileNotificationsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile/display',
|
||||
name: 'profile-display',
|
||||
component: () => import('@/views/profile/ProfileDisplayView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile/checks',
|
||||
name: 'profile-checks',
|
||||
component: () => import('@/views/profile/ProfileChecksView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const authed = isAuthenticated()
|
||||
|
||||
if (to.meta.requiresAuth && !authed) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
if (to.meta.guest && authed) {
|
||||
return { name: 'home' }
|
||||
}
|
||||
|
||||
// صفحات CRM برای همه قابل مشاهدهاند (طبق طراحی موکاپ)؛ API برای غیرواسطه خطا میدهد
|
||||
if (to.meta.agentOnly) {
|
||||
const user = getCachedUser()
|
||||
if (user?.appRole !== 'agent') {
|
||||
return { name: 'home' }
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,181 @@
|
||||
import { apiRequest, ApiError } from './http'
|
||||
import { getCatalogCategories } from '@/data/catalogCategories'
|
||||
|
||||
export type AgentProfileDto = {
|
||||
id: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
code: string
|
||||
levelLabel: string
|
||||
nextLevelLabel: string
|
||||
pointsToNext: number
|
||||
levelProgress: number
|
||||
phone: string
|
||||
customersCount: number
|
||||
ordersCount: number
|
||||
salesVolumeToman: number
|
||||
profitToman: number
|
||||
periodLabel: string
|
||||
}
|
||||
|
||||
export type BrowseProductDto = {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
companyId?: string
|
||||
catalogId?: string
|
||||
catalogCode?: string
|
||||
color?: string
|
||||
minMeterage?: number
|
||||
orderMultiplier?: number
|
||||
basePrice?: number | null
|
||||
categoryId?: string
|
||||
usageCategory?: string
|
||||
season?: string
|
||||
widthCm?: number
|
||||
composition?: string
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
rollMeters?: number
|
||||
imageUrl?: string
|
||||
imageUrls?: string[]
|
||||
}
|
||||
|
||||
const AGENT_UNAVAILABLE =
|
||||
'سرویس واسطه روی سرور فعال نیست. پس از migrate و deploy سرور دوباره تلاش کنید.'
|
||||
|
||||
async function agentRequest<T>(path: string, options?: Parameters<typeof apiRequest>[1]): Promise<T> {
|
||||
try {
|
||||
return await apiRequest<T>(path, options)
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && (e.status === 404 || e.status === 501)) {
|
||||
throw new ApiError(e.status, AGENT_UNAVAILABLE)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCatalogCategories() {
|
||||
try {
|
||||
return await apiRequest<{
|
||||
usage: Array<{ id: string; title: string; subtitle: string; imageUrl?: string }>
|
||||
seasons: Array<{ id: string; title: string; description: string; imageUrl?: string }>
|
||||
}>('/catalog/categories')
|
||||
} catch {
|
||||
try {
|
||||
return await apiRequest<{
|
||||
usage: Array<{ id: string; title: string; subtitle: string; imageUrl?: string }>
|
||||
seasons: Array<{ id: string; title: string; description: string; imageUrl?: string }>
|
||||
}>('/products/meta/categories')
|
||||
} catch {
|
||||
return getCatalogCategories()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBrowseProducts(query?: {
|
||||
categoryId?: string
|
||||
season?: string
|
||||
}) {
|
||||
return apiRequest<BrowseProductDto[]>('/products', {
|
||||
query: {
|
||||
activeOnly: true,
|
||||
categoryId: query?.categoryId,
|
||||
season: query?.season,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export type AppBannerDto = {
|
||||
id: string
|
||||
slot: string
|
||||
title: string
|
||||
subtitle: string
|
||||
ctaLabel: string
|
||||
ctaLink: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
export async function fetchBanners(slot?: string) {
|
||||
return apiRequest<AppBannerDto[]>('/banners', { query: { slot } })
|
||||
}
|
||||
|
||||
export async function fetchAgentProfile() {
|
||||
return agentRequest<AgentProfileDto>('/agent/profile')
|
||||
}
|
||||
|
||||
export async function fetchAgentCustomers(query?: { q?: string; status?: string }) {
|
||||
return agentRequest<unknown[]>('/agent/customers', { query })
|
||||
}
|
||||
|
||||
export type CreateAgentCustomerPayload = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
phone: string
|
||||
businessName?: string
|
||||
}
|
||||
|
||||
export async function createAgentCustomer(body: CreateAgentCustomerPayload) {
|
||||
return agentRequest('/agent/customers', { method: 'POST', body })
|
||||
}
|
||||
|
||||
export async function fetchAgentCustomerDetail(id: string) {
|
||||
return agentRequest<{
|
||||
customer: unknown
|
||||
orders: unknown[]
|
||||
meetings: unknown[]
|
||||
}>(`/agent/customers/${id}`)
|
||||
}
|
||||
|
||||
export type CreateMeetingPayload = {
|
||||
customerId: string
|
||||
type: string
|
||||
title: string
|
||||
description?: string
|
||||
subject?: string
|
||||
content?: string
|
||||
date: string
|
||||
time?: string
|
||||
createReminder?: boolean
|
||||
reminderTitle?: string
|
||||
reminderDate?: string
|
||||
reminderTime?: string
|
||||
reminderNote?: string
|
||||
}
|
||||
|
||||
export async function createMeetingReport(body: CreateMeetingPayload) {
|
||||
return agentRequest('/agent/meetings', { method: 'POST', body })
|
||||
}
|
||||
|
||||
export async function fetchReminders(filter?: string) {
|
||||
return agentRequest<unknown[]>('/agent/reminders', {
|
||||
query: { filter },
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchReminderDetail(id: string) {
|
||||
return agentRequest(`/agent/reminders/${id}`)
|
||||
}
|
||||
|
||||
export async function completeReminder(id: string, completed = true) {
|
||||
return agentRequest(`/agent/reminders/${id}/complete`, {
|
||||
method: 'PATCH',
|
||||
body: { completed },
|
||||
})
|
||||
}
|
||||
|
||||
export async function createReminder(body: {
|
||||
customerId: string
|
||||
title: string
|
||||
date: string
|
||||
time: string
|
||||
description?: string
|
||||
note?: string
|
||||
interactionType?: string
|
||||
}) {
|
||||
return agentRequest('/agent/reminders', { method: 'POST', body })
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { extractNumericString, normalizePhone } from '@/utils/format'
|
||||
import { apiRequest, ApiError, getAccessToken, setAccessToken } from './http'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const USER_KEY = 'ft_user'
|
||||
const PENDING_OTP_KEY = 'ft_pending_otp'
|
||||
|
||||
type PendingOtp = {
|
||||
phone: string
|
||||
isRegister: boolean
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
let memoryPending: PendingOtp | null = null
|
||||
|
||||
function cacheUser(user: User | null): void {
|
||||
if (user) localStorage.setItem(USER_KEY, JSON.stringify(user))
|
||||
else localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
|
||||
export function getCachedUser(): User | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(USER_KEY)
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw) as User
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentUser(): User | null {
|
||||
if (!getAccessToken()) return null
|
||||
return getCachedUser()
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getAccessToken()
|
||||
}
|
||||
|
||||
function setPendingOtp(value: PendingOtp | null) {
|
||||
memoryPending = value
|
||||
try {
|
||||
if (value) sessionStorage.setItem(PENDING_OTP_KEY, JSON.stringify(value))
|
||||
else sessionStorage.removeItem(PENDING_OTP_KEY)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendOtp(
|
||||
phone: string,
|
||||
options: { firstName?: string; lastName?: string; isRegister: boolean },
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const normalized = normalizePhone(phone)
|
||||
if (!/^09\d{9}$/.test(normalized)) {
|
||||
return { success: false, message: 'شماره موبایل معتبر نیست.' }
|
||||
}
|
||||
|
||||
if (options.isRegister && (!options.firstName?.trim() || !options.lastName?.trim())) {
|
||||
return { success: false, message: 'نام و نام خانوادگی الزامی است.' }
|
||||
}
|
||||
|
||||
try {
|
||||
setPendingOtp({
|
||||
phone: normalized,
|
||||
isRegister: options.isRegister,
|
||||
firstName: options.firstName,
|
||||
lastName: options.lastName,
|
||||
})
|
||||
const res = await apiRequest<{ success: boolean; message: string; debugCode?: string }>(
|
||||
'/auth/otp/request',
|
||||
{
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
body: {
|
||||
phone: normalized,
|
||||
firstName: options.firstName,
|
||||
lastName: options.lastName,
|
||||
isRegister: options.isRegister,
|
||||
},
|
||||
},
|
||||
)
|
||||
const hint = res.debugCode ? ` (کد: ${res.debugCode})` : ''
|
||||
return { success: true, message: `${res.message}${hint}` }
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e instanceof ApiError ? e.message : 'ارسال کد ناموفق بود.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyOtp(
|
||||
phone: string,
|
||||
code: string,
|
||||
): Promise<{ success: boolean; message: string; user?: User }> {
|
||||
const normalized = normalizePhone(phone)
|
||||
try {
|
||||
const res = await apiRequest<{ accessToken: string; user: User }>('/auth/otp/verify', {
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
body: { phone: normalized, code: extractNumericString(code) },
|
||||
})
|
||||
setAccessToken(res.accessToken)
|
||||
cacheUser(res.user)
|
||||
setPendingOtp(null)
|
||||
return { success: true, message: 'ورود موفق.', user: res.user }
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e instanceof ApiError ? e.message : 'تأیید کد ناموفق بود.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logout(): void {
|
||||
setAccessToken(null)
|
||||
cacheUser(null)
|
||||
setPendingOtp(null)
|
||||
}
|
||||
|
||||
export async function fetchMe(): Promise<User | null> {
|
||||
const token = getAccessToken()
|
||||
if (!token) {
|
||||
cacheUser(null)
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const me = await apiRequest<User>('/auth/me')
|
||||
cacheUser(me)
|
||||
return me
|
||||
} catch {
|
||||
return getCachedUser()
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfile(
|
||||
data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId' | 'avatarUrl'>>,
|
||||
): Promise<User | null> {
|
||||
try {
|
||||
const updated = await apiRequest<User>('/auth/profile', {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
cacheUser(updated)
|
||||
return updated
|
||||
} catch (e) {
|
||||
const avatarOnly =
|
||||
data.avatarUrl !== undefined &&
|
||||
data.firstName === undefined &&
|
||||
data.lastName === undefined &&
|
||||
data.nationalId === undefined
|
||||
const current = getCachedUser()
|
||||
if (!avatarOnly || !current) throw e
|
||||
const merged: User = { ...current }
|
||||
if (data.avatarUrl) merged.avatarUrl = data.avatarUrl
|
||||
else delete merged.avatarUrl
|
||||
cacheUser(merged)
|
||||
return merged
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
const TOKEN_KEY = 'ft_access_token'
|
||||
|
||||
export function getApiBaseUrl(): string {
|
||||
return (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, '')
|
||||
|| 'https://parche.montest.ir/server/api'
|
||||
}
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function setAccessToken(token: string | null): void {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token)
|
||||
else localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOptions = {
|
||||
method?: string
|
||||
body?: unknown
|
||||
auth?: boolean
|
||||
query?: Record<string, string | number | boolean | undefined | null>
|
||||
}
|
||||
|
||||
function buildUrl(path: string, query?: RequestOptions['query']): string {
|
||||
const base = getApiBaseUrl()
|
||||
const joined = path.startsWith('http')
|
||||
? path
|
||||
: `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
||||
|
||||
const url = joined.startsWith('http')
|
||||
? new URL(joined)
|
||||
: new URL(joined, typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1')
|
||||
|
||||
if (query) {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === undefined || value === null || value === '') continue
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
function errorMessageFromBody(data: unknown, status: number): string {
|
||||
if (typeof data === 'object' && data) {
|
||||
if ('message' in data) {
|
||||
const message = (data as { message: unknown }).message
|
||||
if (Array.isArray(message)) return message.join('، ')
|
||||
if (message != null && String(message).trim()) return String(message)
|
||||
}
|
||||
if ('error' in data) {
|
||||
const error = (data as { error: unknown }).error
|
||||
if (typeof error === 'string' && error.trim()) return `${error} (${status})`
|
||||
}
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
if (/upstream error/i.test(data)) {
|
||||
return 'سرور API در دسترس نیست. کمی بعد دوباره تلاش کنید.'
|
||||
}
|
||||
if (data.trim() && !data.trimStart().startsWith('<')) {
|
||||
return data.trim().slice(0, 200)
|
||||
}
|
||||
}
|
||||
if (status === 400) return 'درخواست نامعتبر است. اطلاعات سفارش را بررسی کنید.'
|
||||
if (status === 401) return 'نشست شما منقضی شده است. دوباره وارد شوید.'
|
||||
return `خطای سرور (${status})`
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
}
|
||||
if (options.body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
if (options.auth !== false) {
|
||||
const token = getAccessToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
let res: Response
|
||||
try {
|
||||
res = await fetch(buildUrl(path, options.query), {
|
||||
method: options.method ?? (options.body !== undefined ? 'POST' : 'GET'),
|
||||
headers,
|
||||
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
})
|
||||
} catch (err) {
|
||||
const timedOut =
|
||||
(err instanceof DOMException && err.name === 'TimeoutError') ||
|
||||
(err instanceof Error && err.name === 'TimeoutError')
|
||||
throw new ApiError(
|
||||
0,
|
||||
timedOut
|
||||
? 'پاسخ سرور طول کشید. دوباره تلاش کنید.'
|
||||
: 'اتصال به سرور برقرار نشد. اینترنت یا آدرس API را بررسی کنید.',
|
||||
)
|
||||
}
|
||||
|
||||
const text = await res.text()
|
||||
let data: unknown = null
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch {
|
||||
data = text
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401 && options.auth !== false) {
|
||||
setAccessToken(null)
|
||||
try {
|
||||
localStorage.removeItem('ft_user')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
const redirect = encodeURIComponent(
|
||||
`${window.location.pathname}${window.location.search}`,
|
||||
)
|
||||
window.location.assign(`/login?redirect=${redirect}`)
|
||||
}
|
||||
}
|
||||
throw new ApiError(res.status, errorMessageFromBody(data, res.status))
|
||||
}
|
||||
|
||||
return data as T
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export function getItem<T>(key: string, fallback: T): T {
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return fallback
|
||||
return JSON.parse(raw) as T
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export function setItem<T>(key: string, value: T): void {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
export function removeItem(key: string): void {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
users: 'ft_users',
|
||||
session: 'ft_session',
|
||||
carts: 'ft_carts',
|
||||
activeCartId: 'ft_active_cart_id',
|
||||
orders: 'ft_orders',
|
||||
pendingOtp: 'ft_pending_otp',
|
||||
} as const
|
||||
@@ -0,0 +1,141 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { User } from '@/types'
|
||||
import {
|
||||
getCurrentUser,
|
||||
logout as authLogout,
|
||||
sendOtp,
|
||||
verifyOtp,
|
||||
updateProfile as authUpdateProfile,
|
||||
fetchMe,
|
||||
} from '@/services/authService'
|
||||
import {
|
||||
getPendingRegister,
|
||||
setPendingRegister,
|
||||
type PendingRegister,
|
||||
} from '@/utils/pendingRegister'
|
||||
import { useCartStore } from './cart'
|
||||
import { useOrderStore } from './order'
|
||||
import { useCatalogStore } from './catalog'
|
||||
import { useNotificationStore } from './notification'
|
||||
import { useFavoritesStore } from './favorites'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(getCurrentUser())
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const pendingRegister = ref<PendingRegister | null>(getPendingRegister())
|
||||
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const fullName = computed(() =>
|
||||
user.value ? `${user.value.firstName} ${user.value.lastName}` : '',
|
||||
)
|
||||
const isAgent = computed(() => user.value?.appRole === 'agent')
|
||||
const isCustomerOnly = computed(
|
||||
() => !!user.value && user.value.appRole !== 'agent',
|
||||
)
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
function clearPendingRegister() {
|
||||
pendingRegister.value = null
|
||||
setPendingRegister(null)
|
||||
}
|
||||
|
||||
async function requestOtp(
|
||||
phone: string,
|
||||
options: { firstName?: string; lastName?: string; isRegister: boolean },
|
||||
) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Re-read storage on every call (survives HMR / refresh / separate tabs).
|
||||
const stored = getPendingRegister()
|
||||
pendingRegister.value = stored
|
||||
|
||||
const firstName = options.firstName?.trim() || stored?.firstName
|
||||
const lastName = options.lastName?.trim() || stored?.lastName
|
||||
|
||||
if (options.isRegister && firstName && lastName) {
|
||||
pendingRegister.value = { firstName, lastName }
|
||||
setPendingRegister(pendingRegister.value)
|
||||
}
|
||||
|
||||
const result = await sendOtp(phone, {
|
||||
isRegister: options.isRegister,
|
||||
firstName,
|
||||
lastName,
|
||||
})
|
||||
if (!result.success) error.value = result.message
|
||||
return result
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmOtp(phone: string, code: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const result = await verifyOtp(phone, code)
|
||||
loading.value = false
|
||||
if (result.success && result.user) {
|
||||
user.value = result.user
|
||||
clearPendingRegister()
|
||||
await Promise.all([
|
||||
useCatalogStore().loadCompanies(),
|
||||
useCartStore().hydrate(),
|
||||
useOrderStore().hydrate(),
|
||||
useNotificationStore().hydrate(),
|
||||
useFavoritesStore().hydrate(),
|
||||
])
|
||||
} else {
|
||||
error.value = result.message
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authLogout()
|
||||
user.value = null
|
||||
clearPendingRegister()
|
||||
useCartStore().resetLocal()
|
||||
useOrderStore().resetLocal()
|
||||
useNotificationStore().resetLocal()
|
||||
useFavoritesStore().resetLocal()
|
||||
}
|
||||
|
||||
async function updateProfile(
|
||||
data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId' | 'avatarUrl'>>,
|
||||
) {
|
||||
if (!user.value) return null
|
||||
const updated = await authUpdateProfile(data)
|
||||
if (updated) user.value = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
user.value = getCurrentUser()
|
||||
if (!user.value) return
|
||||
const me = await fetchMe()
|
||||
user.value = me
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fullName,
|
||||
isAgent,
|
||||
isCustomerOnly,
|
||||
clearError,
|
||||
requestOtp,
|
||||
confirmOtp,
|
||||
logout,
|
||||
updateProfile,
|
||||
hydrate,
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user