feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography - Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout - Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions - Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components - Add draggable bottom-left QuickCreate floating action button
This commit is contained in:
@@ -9,8 +9,13 @@ import {
|
||||
toFaDigits,
|
||||
} from '@/lib/jalali';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import UiButton from '@/components/ui/UiButton.vue';
|
||||
import UiModal from '@/components/ui/UiModal.vue';
|
||||
import {
|
||||
UiButton,
|
||||
UiModal,
|
||||
UiPageHeader,
|
||||
UiPagination,
|
||||
UiEmptyState,
|
||||
} from '@/components/ui';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import InvoiceFormView from '@/views/InvoiceFormView.vue';
|
||||
import type {
|
||||
@@ -41,7 +46,6 @@ type BillingRow = {
|
||||
href: { name: string; params: { id: string } };
|
||||
};
|
||||
|
||||
const PAGE_SIZES = [10, 20, 50];
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -216,12 +220,6 @@ const pageItems = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return filtered.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
const pageFrom = computed(() =>
|
||||
filtered.value.length ? (page.value - 1) * pageSize.value + 1 : 0,
|
||||
);
|
||||
const pageTo = computed(() =>
|
||||
Math.min(page.value * pageSize.value, filtered.value.length),
|
||||
);
|
||||
|
||||
function docLabel(doc: BillingRow['doc']) {
|
||||
if (doc === 'quote') return 'پیش فاکتور';
|
||||
@@ -343,35 +341,33 @@ onMounted(load);
|
||||
|
||||
<template>
|
||||
<section class="page" @click="closeMenus">
|
||||
<header class="page-head">
|
||||
<div class="title-row">
|
||||
<span class="title-ico"><UiIcon name="document" :size="22" /></span>
|
||||
<div>
|
||||
<h1>فاکتور ها</h1>
|
||||
<p class="lead">مدیریت فاکتورهای فروش، پیش فاکتور و خدمات</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-actions" @click.stop>
|
||||
<div class="add-split desk-only">
|
||||
<UiButton type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">
|
||||
<UiIcon name="plus" :size="16" />
|
||||
افزودن فاکتور
|
||||
</UiButton>
|
||||
<button type="button" class="btn primary split" aria-label="گزینههای افزودن" @click="addOpen = !addOpen">
|
||||
<UiIcon name="chevron" :size="14" />
|
||||
</button>
|
||||
<div v-if="addOpen" class="menu">
|
||||
<button type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">فاکتور جدید</button>
|
||||
<button type="button" @click="invoiceModal = { kind: 'quote' }; addOpen = false">پیش فاکتور جدید</button>
|
||||
<button type="button" @click="invoiceModal = { kind: 'credit' }; addOpen = false">فاکتور برگشتی</button>
|
||||
<UiPageHeader
|
||||
title="فاکتور ها"
|
||||
lead="مدیریت فاکتورهای فروش، پیش فاکتور و خدمات"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="head-actions" @click.stop>
|
||||
<div class="add-split desk-only">
|
||||
<UiButton type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">
|
||||
<UiIcon name="plus" :size="16" />
|
||||
افزودن فاکتور
|
||||
</UiButton>
|
||||
<button type="button" class="btn primary split" aria-label="گزینههای افزودن" @click="addOpen = !addOpen">
|
||||
<UiIcon name="chevron" :size="14" />
|
||||
</button>
|
||||
<div v-if="addOpen" class="menu">
|
||||
<button type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">فاکتور جدید</button>
|
||||
<button type="button" @click="invoiceModal = { kind: 'quote' }; addOpen = false">پیش فاکتور جدید</button>
|
||||
<button type="button" @click="invoiceModal = { kind: 'credit' }; addOpen = false">فاکتور برگشتی</button>
|
||||
</div>
|
||||
</div>
|
||||
<UiButton class="mob-only" type="button" @click="invoiceModal = { kind: 'invoice' }">
|
||||
<UiIcon name="plus" :size="16" />
|
||||
افزودن
|
||||
</UiButton>
|
||||
</div>
|
||||
<UiButton class="mob-only" type="button" @click="invoiceModal = { kind: 'invoice' }">
|
||||
<UiIcon name="plus" :size="16" />
|
||||
افزودن
|
||||
</UiButton>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
|
||||
<!-- Desktop underline tabs -->
|
||||
<div class="tabs desk-only">
|
||||
@@ -565,37 +561,21 @@ onMounted(load);
|
||||
<em :class="{ neg: r.amount < 0 }">{{ money(r.amount, r.currency) }}</em>
|
||||
</div>
|
||||
</article>
|
||||
<p v-if="!pageItems.length" class="empty">سندی یافت نشد.</p>
|
||||
<UiEmptyState
|
||||
v-if="!pageItems.length"
|
||||
title="سندی یافت نشد."
|
||||
description="فاکتور، پیشفاکتور یا سند جدیدی ثبت کنید."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer v-if="filtered.length" class="pager">
|
||||
<div class="pager-meta">
|
||||
<p class="muted">
|
||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از
|
||||
{{ toFaDigits(filtered.length) }} مورد
|
||||
</p>
|
||||
<label class="page-size desk-only">
|
||||
نمایش در هر صفحه:
|
||||
<select v-model.number="pageSize">
|
||||
<option v-for="n in PAGE_SIZES" :key="n" :value="n">{{ toFaDigits(n) }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pages">
|
||||
<button type="button" class="page-btn" :disabled="page <= 1" @click="page -= 1">‹</button>
|
||||
<button
|
||||
v-for="n in pageCount"
|
||||
:key="n"
|
||||
type="button"
|
||||
class="page-btn"
|
||||
:class="{ on: page === n }"
|
||||
@click="page = n"
|
||||
>
|
||||
{{ toFaDigits(n) }}
|
||||
</button>
|
||||
<button type="button" class="page-btn" :disabled="page >= pageCount" @click="page += 1">›</button>
|
||||
</div>
|
||||
</footer>
|
||||
<UiPagination
|
||||
v-if="filtered.length"
|
||||
:page="page"
|
||||
:total-pages="pageCount"
|
||||
:total-items="filtered.length"
|
||||
:page-size="pageSize"
|
||||
@update:page="page = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<UiModal
|
||||
@@ -682,7 +662,8 @@ onMounted(load);
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
position: relative;
|
||||
padding-bottom: 5rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
@@ -692,10 +673,10 @@ onMounted(load);
|
||||
}
|
||||
.title-row { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||||
.title-ico {
|
||||
width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
|
||||
width: 44px; height: 44px; border-radius: var(--radius); display: grid; place-items: center;
|
||||
background: var(--teal-soft); color: var(--teal);
|
||||
}
|
||||
h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
||||
h1 { margin: 0; font-size: 1.5rem; font-weight: 600; }
|
||||
.lead { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.9rem; }
|
||||
.head-actions { position: relative; }
|
||||
.add-split { display: flex; position: relative; }
|
||||
@@ -721,11 +702,11 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
||||
.add-doc { color: var(--teal); }
|
||||
.plus-badge {
|
||||
position: absolute; bottom: 6px; left: 6px;
|
||||
background: var(--teal); color: #fff; border-radius: 4px; padding: 1px;
|
||||
background: var(--teal); color: #fff; border-radius: var(--radius); padding: 1px;
|
||||
}
|
||||
.menu {
|
||||
position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; min-width: 170px;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 12px;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
|
||||
box-shadow: 0 12px 28px rgba(15,23,42,.12); padding: 0.35rem; display: grid;
|
||||
}
|
||||
.menu.left { left: auto; right: 0; }
|
||||
@@ -733,7 +714,7 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
||||
.menu button {
|
||||
display: block; width: 100%;
|
||||
border: 0; background: transparent; text-align: right; padding: 0.55rem 0.7rem;
|
||||
border-radius: 8px; color: var(--text); text-decoration: none; cursor: pointer;
|
||||
border-radius: var(--radius); color: var(--text); text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.menu a:hover,
|
||||
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
|
||||
@@ -764,21 +745,30 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
||||
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||
.search {
|
||||
flex: 1 1 220px; display: flex; align-items: center; gap: 0.45rem;
|
||||
flex: 1 1 220px; max-width: 380px; display: flex; align-items: center; gap: 0.45rem;
|
||||
border: 1px solid var(--line); border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
||||
padding: 0 0.75rem; background: #fff; color: var(--muted); box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.search input { border: 0; outline: 0; width: 100%; background: transparent; color: var(--text); }
|
||||
.search:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
.search input { border: 0; outline: 0; width: 100%; background: transparent; color: var(--ink); font-size: var(--fs-sm); padding: 0; }
|
||||
.search .mob-ph { display: none; }
|
||||
.filters { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; }
|
||||
.filter-chip {
|
||||
display: inline-flex; align-items: center; gap: 0.3rem; flex-wrap: nowrap;
|
||||
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.55rem;
|
||||
background: #fff; color: #94a3b8; min-height: var(--control-h); height: var(--control-h);
|
||||
box-sizing: border-box; white-space: nowrap;
|
||||
background: #fff; color: var(--muted); min-height: var(--control-h); height: var(--control-h);
|
||||
box-sizing: border-box; white-space: nowrap; font-size: var(--fs-sm);
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
.filter-chip:focus-within {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.filter-chip select, .filter-chip :deep(button), .filter-chip :deep(input) {
|
||||
border: 0; background: transparent; outline: 0; min-height: 0; height: 32px; color: var(--text);
|
||||
border: 0; background: transparent; outline: 0; min-height: 0; height: 32px; color: var(--ink); font-size: var(--fs-sm);
|
||||
}
|
||||
.filter-chip :deep(.wrap) { width: auto; flex: 0 1 auto; }
|
||||
.filter-chip :deep(.trigger) {
|
||||
@@ -790,7 +780,7 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
||||
.filter-chip .sep { color: #cbd5e1; flex-shrink: 0; }
|
||||
|
||||
.table-wrap {
|
||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px; overflow: auto;
|
||||
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius); overflow: auto;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,.04);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; min-width: 980px; }
|
||||
@@ -802,7 +792,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
.cust { display: flex; gap: 0.55rem; align-items: center; }
|
||||
.avatar {
|
||||
width: 36px; height: 36px; border-radius: 999px; background: var(--teal-soft); color: var(--teal);
|
||||
display: grid; place-items: center; font-weight: 800; flex-shrink: 0;
|
||||
display: grid; place-items: center; font-weight: 600; flex-shrink: 0;
|
||||
}
|
||||
.cust strong { display: block; }
|
||||
.cust small { color: var(--muted); }
|
||||
@@ -832,7 +822,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
.mob-list { display: none; gap: 0.65rem; }
|
||||
.mob-card {
|
||||
position: relative;
|
||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px; padding: 0.9rem;
|
||||
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius); padding: 0.9rem;
|
||||
box-shadow: 0 6px 16px rgba(15,23,42,.04);
|
||||
display: grid; gap: 0.65rem;
|
||||
}
|
||||
@@ -847,7 +837,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
.mob-aside { display: grid; justify-items: center; gap: 0.35rem; }
|
||||
.mob-menu { top: 2.2rem; }
|
||||
.mob-card-bottom { display: flex; justify-content: flex-start; }
|
||||
.mob-card-bottom em { font-style: normal; font-weight: 800; font-size: 0.92rem; }
|
||||
.mob-card-bottom em { font-style: normal; font-weight: 600; font-size: 0.92rem; }
|
||||
|
||||
.pager {
|
||||
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center;
|
||||
@@ -857,11 +847,11 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); font-size: 0.88rem;
|
||||
}
|
||||
.page-size select {
|
||||
border: 1px solid var(--line); border-radius: 10px; padding: 0.3rem 0.5rem; background: #fff;
|
||||
border: 1px solid var(--line); border-radius: var(--radius); padding: 0.3rem 0.5rem; background: #fff;
|
||||
}
|
||||
.pages { display: flex; gap: 0.3rem; }
|
||||
.page-btn {
|
||||
min-width: 34px; height: 34px; border-radius: 10px; border: 1px solid var(--line);
|
||||
min-width: 34px; height: 34px; border-radius: var(--radius); border: 1px solid var(--line);
|
||||
background: #fff; cursor: pointer;
|
||||
}
|
||||
.page-btn.on { background: var(--teal); color: #fff; border-color: transparent; }
|
||||
@@ -869,7 +859,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
.fab {
|
||||
display: none; position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 40;
|
||||
background: var(--teal); color: #fff; text-align: center; text-decoration: none;
|
||||
border-radius: 14px; padding: 0.9rem; font-weight: 800;
|
||||
border-radius: var(--radius); padding: 0.9rem; font-weight: 600;
|
||||
box-shadow: 0 12px 28px rgba(13,148,136,.35);
|
||||
}
|
||||
|
||||
@@ -878,12 +868,12 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
display: grid; align-items: end;
|
||||
}
|
||||
.sheet {
|
||||
background: #fff; border-radius: 20px 20px 0 0; padding: 1rem; max-height: 85vh; overflow: auto;
|
||||
background: #fff; border-radius: var(--radius, 10px) var(--radius, 10px) 0 0; padding: 16px; max-height: 85vh; overflow: auto;
|
||||
}
|
||||
.sheet header {
|
||||
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;
|
||||
}
|
||||
.sheet header strong { justify-self: end; }
|
||||
.sheet header strong { justify-self: end; font-size: 16px; font-weight: 500; color: var(--ink); }
|
||||
.link { border: 0; background: transparent; color: var(--teal); font-weight: 700; cursor: pointer; }
|
||||
.lbl { margin: 0.85rem 0 0.4rem; font-weight: 700; color: #334155; }
|
||||
.pill-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||
@@ -894,7 +884,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
||||
.pill-row button.on { background: var(--teal); color: #fff; border-color: transparent; }
|
||||
.field { display: grid; gap: 0.3rem; color: var(--muted); font-size: 0.85rem; margin-top: 0.35rem; }
|
||||
.field input, .field select, .control select {
|
||||
border: 1px solid var(--line); border-radius: 12px; padding: 0.55rem 0.7rem; background: #fff; color: var(--text);
|
||||
border: 1px solid var(--line); border-radius: var(--radius); padding: 0.55rem 0.7rem; background: #fff; color: var(--text);
|
||||
}
|
||||
.control { position: relative; display: flex; align-items: center; }
|
||||
.control :deep(.ico) {
|
||||
|
||||
Reference in New Issue
Block a user