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:
@@ -5,8 +5,13 @@ import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import CustomerMoodFace from '@/components/CustomerMoodFace.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 PersonFormView from '@/views/PersonFormView.vue';
|
||||
import CompanyFormView from '@/views/CompanyFormView.vue';
|
||||
@@ -57,18 +62,6 @@ const total = computed(() =>
|
||||
tab.value === 'people' ? people.value?.total ?? 0 : companies.value?.total ?? 0,
|
||||
);
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
||||
const pageFrom = computed(() => (total.value ? (page.value - 1) * PAGE_SIZE + 1 : 0));
|
||||
const pageTo = computed(() => Math.min(page.value * PAGE_SIZE, total.value));
|
||||
const pageList = computed(() => {
|
||||
const count = pageCount.value;
|
||||
const current = page.value;
|
||||
const pages: number[] = [];
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
if (i === 1 || i === count || Math.abs(i - current) <= 2) pages.push(i);
|
||||
else if (pages[pages.length - 1] !== -1) pages.push(-1);
|
||||
}
|
||||
return pages;
|
||||
});
|
||||
|
||||
function moodOf(person: PersonDto): PersonMood {
|
||||
return person.mood || 'newcomer';
|
||||
@@ -249,20 +242,19 @@ onMounted(load);
|
||||
|
||||
<template>
|
||||
<section class="page" @click="closePopups">
|
||||
<header class="page-head">
|
||||
<div class="head-copy">
|
||||
<h1>مخاطبین</h1>
|
||||
<p class="lead">مدیریت و مشاهده اشخاص و شرکتها</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="exportExcel">
|
||||
<UiPageHeader
|
||||
title="مخاطبین"
|
||||
lead="مدیریت و مشاهده اشخاص و شرکتها"
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="exportExcel">
|
||||
<span class="btn-full">خروجی اکسل</span>
|
||||
<span class="btn-short">خروجی</span>
|
||||
</button>
|
||||
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="pickImportFile">
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="pickImportFile">
|
||||
<span class="btn-full">ورود از اکسل</span>
|
||||
<span class="btn-short">ورود</span>
|
||||
</button>
|
||||
</UiButton>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
@@ -271,14 +263,14 @@ onMounted(load);
|
||||
@change="onImportFile"
|
||||
/>
|
||||
<UiButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
@click="contactModal = tab === 'companies' ? 'company' : 'person'"
|
||||
>
|
||||
<UiIcon name="plus" :size="16" />
|
||||
{{ tab === 'companies' ? 'شرکت جدید' : 'شخص جدید' }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
|
||||
<nav class="tabs">
|
||||
<button type="button" :class="{ on: tab === 'people' }" @click.stop="setTab('people')">
|
||||
@@ -442,7 +434,11 @@ onMounted(load);
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="empty">شخصی ثبت نشده. اولین نفر را اضافه کنید.</p>
|
||||
<UiEmptyState
|
||||
v-else
|
||||
title="شخصی ثبت نشده است."
|
||||
description="اولین مخاطب یا مشتری را اضافه کنید."
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@@ -491,24 +487,20 @@ onMounted(load);
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="empty">شرکتی ثبت نشده. اولین شرکت را اضافه کنید.</p>
|
||||
<UiEmptyState
|
||||
v-else
|
||||
title="شرکتی ثبت نشده است."
|
||||
description="اولین شرکت یا سازمان را اضافه کنید."
|
||||
/>
|
||||
</template>
|
||||
|
||||
<footer v-if="total" class="pager">
|
||||
<span>
|
||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از {{ toFaDigits(total) }} مورد
|
||||
</span>
|
||||
<div class="pages">
|
||||
<button type="button" :disabled="page <= 1" @click="setPage(page - 1)">‹</button>
|
||||
<template v-for="(n, idx) in pageList" :key="`${n}-${idx}`">
|
||||
<span v-if="n < 0">…</span>
|
||||
<button v-else type="button" :class="{ on: n === page }" @click="setPage(n)">
|
||||
{{ toFaDigits(n) }}
|
||||
</button>
|
||||
</template>
|
||||
<button type="button" :disabled="page >= pageCount" @click="setPage(page + 1)">›</button>
|
||||
</div>
|
||||
</footer>
|
||||
<UiPagination
|
||||
v-if="total"
|
||||
:page="page"
|
||||
:total-pages="pageCount"
|
||||
:total-items="total"
|
||||
@update:page="setPage"
|
||||
/>
|
||||
|
||||
<UiModal
|
||||
:open="Boolean(contactModal)"
|
||||
@@ -633,7 +625,7 @@ h1 {
|
||||
|
||||
.tabs .on {
|
||||
color: #0f766e;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 -2px 0 #14b8a6;
|
||||
}
|
||||
|
||||
@@ -658,34 +650,44 @@ h1 {
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1 1 12rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 14rem;
|
||||
min-width: 180px;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0 0.75rem;
|
||||
min-height: var(--control-h);
|
||||
height: var(--control-h);
|
||||
color: #94a3b8;
|
||||
color: var(--muted);
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.search:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
|
||||
.search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
color: #0f172a;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--ink);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tool {
|
||||
min-height: var(--control-h);
|
||||
height: var(--control-h);
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
padding: 0 0.75rem;
|
||||
@@ -694,9 +696,16 @@ h1 {
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font: inherit;
|
||||
color: #475569;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--ink-secondary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.tool:hover {
|
||||
background: var(--surface-subtle);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.pop {
|
||||
@@ -712,7 +721,7 @@ h1 {
|
||||
padding: 0.35rem;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
|
||||
min-width: min(12rem, calc(100vw - 2rem));
|
||||
max-width: calc(100vw - 1.5rem);
|
||||
@@ -725,7 +734,7 @@ h1 {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
color: inherit;
|
||||
@@ -751,7 +760,7 @@ h1 {
|
||||
|
||||
.table-wrap {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -789,7 +798,7 @@ th {
|
||||
|
||||
.name {
|
||||
color: #0f766e;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -814,7 +823,7 @@ th {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.dot {
|
||||
@@ -869,7 +878,7 @@ strong.newcomer {
|
||||
gap: 0.7rem;
|
||||
align-items: start;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.85rem;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
|
||||
min-width: 0;
|
||||
@@ -922,7 +931,7 @@ strong.newcomer {
|
||||
border-radius: 999px;
|
||||
padding: 0.28rem 0.6rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
margin-top: 0.25rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -972,7 +981,7 @@ strong.newcomer {
|
||||
padding: 1.4rem;
|
||||
color: #64748b;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.pager {
|
||||
@@ -997,7 +1006,7 @@ strong.newcomer {
|
||||
height: 32px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user