Fix contacts page overlapping and squeeze on mobile layouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 01:49:19 +03:30
co-authored by Cursor
parent 66dfef5d53
commit 32dfb01b4d
+219 -62
View File
@@ -244,16 +244,18 @@ onMounted(load);
<template> <template>
<section class="page" @click="closePopups"> <section class="page" @click="closePopups">
<header class="page-head"> <header class="page-head">
<div> <div class="head-copy">
<h1>مخاطبین</h1> <h1>مخاطبین</h1>
<p class="lead">مدیریت و مشاهده اشخاص و شرکت‌ها</p> <p class="lead">مدیریت و مشاهده اشخاص و شرکت‌ها</p>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-ghost" type="button" :disabled="busy" @click.stop="exportExcel"> <button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="exportExcel">
خروجی اکسل <span class="btn-full">خروجی اکسل</span>
<span class="btn-short">خروجی</span>
</button> </button>
<button class="btn btn-ghost" type="button" :disabled="busy" @click.stop="pickImportFile"> <button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="pickImportFile">
ورود از اکسل <span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</button> </button>
<input <input
ref="fileInput" ref="fileInput"
@@ -286,16 +288,6 @@ onMounted(load);
</nav> </nav>
<div class="toolbar"> <div class="toolbar">
<div class="pop">
<button class="tool" type="button" @click.stop="sortOpen = !sortOpen; filterOpen = false">
<UiIcon name="trend" :size="16" />
مرتب‌سازی
</button>
<ul v-if="sortOpen" class="menu">
<li><button type="button" :class="{ on: sort === 'name' }" @click.stop="setSort('name')">نام</button></li>
<li><button type="button" :class="{ on: sort === 'newest' }" @click.stop="setSort('newest')">جدیدترین</button></li>
</ul>
</div>
<form class="search" @submit.prevent="onSearch"> <form class="search" @submit.prevent="onSearch">
<UiIcon name="search" :size="16" /> <UiIcon name="search" :size="16" />
<input <input
@@ -304,21 +296,33 @@ onMounted(load);
:placeholder="tab === 'people' ? 'جستجو در اشخاص…' : 'جستجو در شرکت‌ها…'" :placeholder="tab === 'people' ? 'جستجو در اشخاص…' : 'جستجو در شرکت‌ها…'"
/> />
</form> </form>
<div v-if="tab === 'people'" class="pop"> <div class="toolbar-tools">
<button class="tool" type="button" @click.stop="filterOpen = !filterOpen; sortOpen = false"> <div class="pop">
<UiIcon name="filter" :size="16" /> <button class="tool" type="button" @click.stop="sortOpen = !sortOpen; filterOpen = false">
فیلتر <UiIcon name="trend" :size="16" />
</button> <span class="tool-label">مرتب‌سازی</span>
<ul v-if="filterOpen" class="menu"> </button>
<li> <ul v-if="sortOpen" class="menu">
<button type="button" :class="{ on: moodFilter === 'all' }" @click.stop="setMood('all')">همه</button> <li><button type="button" :class="{ on: sort === 'name' }" @click.stop="setSort('name')">نام</button></li>
</li> <li><button type="button" :class="{ on: sort === 'newest' }" @click.stop="setSort('newest')">جدیدترین</button></li>
<li v-for="mood in PERSON_MOODS" :key="mood"> </ul>
<button type="button" :class="{ on: moodFilter === mood }" @click.stop="setMood(mood)"> </div>
{{ moodLabel(mood) }} <div v-if="tab === 'people'" class="pop">
</button> <button class="tool" type="button" @click.stop="filterOpen = !filterOpen; sortOpen = false">
</li> <UiIcon name="filter" :size="16" />
</ul> <span class="tool-label">فیلتر</span>
</button>
<ul v-if="filterOpen" class="menu">
<li>
<button type="button" :class="{ on: moodFilter === 'all' }" @click.stop="setMood('all')">همه</button>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" :class="{ on: moodFilter === mood }" @click.stop="setMood(mood)">
{{ moodLabel(mood) }}
</button>
</li>
</ul>
</div>
</div> </div>
</div> </div>
@@ -329,13 +333,12 @@ onMounted(load);
<template v-else-if="tab === 'people'"> <template v-else-if="tab === 'people'">
<div v-if="people?.items.length" class="cards"> <div v-if="people?.items.length" class="cards">
<article v-for="p in people.items" :key="`m-${p.id}`" class="m-card"> <article v-for="p in people.items" :key="`m-${p.id}`" class="m-card">
<CustomerMoodFace :mood="moodOf(p)" :size="52" /> <CustomerMoodFace :mood="moodOf(p)" :size="48" />
<div class="m-body"> <div class="m-body">
<div class="m-top"> <div class="m-top">
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }"> <RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
{{ p.fullName }} {{ p.fullName }}
</RouterLink> </RouterLink>
<i class="dot" :class="moodOf(p)" />
<button class="more" type="button" @click.stop="menuId = menuId === p.id ? null : p.id"> <button class="more" type="button" @click.stop="menuId = menuId === p.id ? null : p.id">
<UiIcon name="dots-v" :size="16" /> <UiIcon name="dots-v" :size="16" />
</button> </button>
@@ -343,16 +346,16 @@ onMounted(load);
<p class="role"> <p class="role">
{{ [p.position, p.company?.name].filter(Boolean).join(' • ') || 'بدون سمت' }} {{ [p.position, p.company?.name].filter(Boolean).join(' • ') || 'بدون سمت' }}
</p> </p>
<p v-if="p.mobile || p.phone" class="line"> <p v-if="p.mobile || p.phone" class="line" dir="ltr">
<UiIcon name="phone" :size="13" /> <UiIcon name="phone" :size="13" />
{{ p.mobile || p.phone }} <span>{{ p.mobile || p.phone }}</span>
</p> </p>
<p v-if="p.email" class="line"> <p v-if="p.email" class="line" dir="ltr">
<UiIcon name="mail" :size="13" /> <UiIcon name="mail" :size="13" />
{{ p.email }} <span>{{ p.email }}</span>
</p> </p>
<span class="badge" :class="moodOf(p)">{{ moodLabel(moodOf(p)) }}</span>
</div> </div>
<span class="badge" :class="moodOf(p)">{{ moodLabel(moodOf(p)) }}</span>
<ul v-if="menuId === p.id" class="menu row-menu"> <ul v-if="menuId === p.id" class="menu row-menu">
<li> <li>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink> <RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
@@ -441,11 +444,19 @@ onMounted(load);
<article v-for="c in companies.items" :key="`mc-${c.id}`" class="m-card"> <article v-for="c in companies.items" :key="`mc-${c.id}`" class="m-card">
<span class="co-ico"><UiIcon name="building" :size="22" /></span> <span class="co-ico"><UiIcon name="building" :size="22" /></span>
<div class="m-body"> <div class="m-body">
<RouterLink class="name" :to="{ name: 'company-detail', params: { id: c.id } }"> <div class="m-top">
{{ c.name }} <RouterLink class="name" :to="{ name: 'company-detail', params: { id: c.id } }">
</RouterLink> {{ c.name }}
<p v-if="c.phone" class="line"><UiIcon name="phone" :size="13" /> {{ c.phone }}</p> </RouterLink>
<p v-if="c.email" class="line"><UiIcon name="mail" :size="13" /> {{ c.email }}</p> </div>
<p v-if="c.phone" class="line" dir="ltr">
<UiIcon name="phone" :size="13" />
<span>{{ c.phone }}</span>
</p>
<p v-if="c.email" class="line" dir="ltr">
<UiIcon name="mail" :size="13" />
<span>{{ c.email }}</span>
</p>
<p class="role">افراد: {{ toFaDigits(c.peopleCount) }}</p> <p class="role">افراد: {{ toFaDigits(c.peopleCount) }}</p>
</div> </div>
</article> </article>
@@ -499,19 +510,34 @@ onMounted(load);
.page { .page {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
width: 100%;
min-width: 0; min-width: 0;
max-width: 100%;
overflow-x: clip;
box-sizing: border-box;
}
.page > * {
min-width: 0;
max-width: 100%;
} }
.page-head { .page-head {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 0.75rem 1rem;
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap;
}
.head-copy {
min-width: 0;
flex: 1 1 12rem;
} }
h1 { h1 {
margin: 0; margin: 0;
font-size: 1.7rem; font-size: clamp(1.3rem, 4vw, 1.7rem);
} }
.lead, .lead,
@@ -519,6 +545,8 @@ h1 {
.role, .role,
.line { .line {
color: #64748b; color: #64748b;
overflow-wrap: anywhere;
word-break: break-word;
} }
.lead { .lead {
@@ -527,9 +555,16 @@ h1 {
.actions { .actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.45rem;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end;
flex: 0 1 auto;
min-width: 0;
}
.btn-short {
display: none;
} }
.add-btn { .add-btn {
@@ -540,25 +575,35 @@ h1 {
color: #fff; color: #fff;
display: grid; display: grid;
place-items: center; place-items: center;
flex-shrink: 0;
box-shadow: 0 8px 18px rgba(13, 148, 136, 0.28); box-shadow: 0 8px 18px rgba(13, 148, 136, 0.28);
} }
.tabs { .tabs {
display: flex; display: flex;
gap: 1.2rem; gap: 0.35rem 1.2rem;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar {
display: none;
} }
.tabs button { .tabs button {
border: 0; border: 0;
background: none; background: none;
padding: 0.55rem 0; padding: 0.55rem 0.1rem;
font: inherit; font: inherit;
color: #94a3b8; color: #94a3b8;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
cursor: pointer; cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
} }
.tabs .on { .tabs .on {
@@ -577,10 +622,18 @@ h1 {
display: flex; display: flex;
gap: 0.55rem; gap: 0.55rem;
align-items: center; align-items: center;
flex-wrap: wrap;
}
.toolbar-tools {
display: flex;
gap: 0.45rem;
align-items: center;
flex-shrink: 0;
} }
.search { .search {
flex: 1; flex: 1 1 12rem;
min-width: 0; min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -632,7 +685,8 @@ h1 {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14); box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
min-width: 12rem; min-width: min(12rem, calc(100vw - 2rem));
max-width: calc(100vw - 1.5rem);
} }
.menu button, .menu button,
@@ -671,11 +725,13 @@ h1 {
border-radius: 18px; border-radius: 18px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04); box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch;
} }
.data-table { .data-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
min-width: 640px;
} }
th, th,
@@ -705,6 +761,8 @@ th {
.name { .name {
color: #0f766e; color: #0f766e;
font-weight: 800; font-weight: 800;
min-width: 0;
overflow-wrap: anywhere;
} }
.mail { .mail {
@@ -724,6 +782,10 @@ th {
cursor: pointer; cursor: pointer;
display: grid; display: grid;
place-items: center; place-items: center;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 8px;
} }
.dot { .dot {
@@ -774,40 +836,68 @@ strong.newcomer {
.m-card { .m-card {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto minmax(0, 1fr);
gap: 0.7rem; gap: 0.7rem;
align-items: start; align-items: start;
background: #fff; background: #fff;
border-radius: 18px; border-radius: 18px;
padding: 0.85rem; padding: 0.85rem;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04); box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
min-width: 0;
overflow: hidden;
} }
.m-top { .m-top {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
min-width: 0;
}
.m-top .name {
flex: 1 1 auto;
min-width: 0;
line-height: 1.35;
} }
.m-body { .m-body {
min-width: 0; min-width: 0;
display: grid;
gap: 0.28rem;
} }
.role, .role,
.line { .line {
margin: 0.2rem 0 0; margin: 0;
font-size: 0.8rem; font-size: 0.8rem;
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 0.3rem; gap: 0.3rem;
min-width: 0;
}
.line :deep(svg),
.role :deep(svg) {
flex-shrink: 0;
margin-top: 0.15rem;
}
.line span {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
} }
.badge { .badge {
align-self: start; justify-self: start;
border-radius: 999px; border-radius: 999px;
padding: 0.28rem 0.6rem; padding: 0.28rem 0.6rem;
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 800; font-weight: 800;
margin-top: 0.25rem;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -832,18 +922,20 @@ strong.newcomer {
} }
.co-ico { .co-ico {
width: 52px; width: 48px;
height: 52px; height: 48px;
border-radius: 50%; border-radius: 50%;
display: grid; display: grid;
place-items: center; place-items: center;
background: #ecfdf5; background: #ecfdf5;
color: #0f766e; color: #0f766e;
flex-shrink: 0;
} }
.row-menu { .row-menu {
inset-inline-start: auto; inset-inline-start: auto;
inset-inline-end: 0; inset-inline-end: 0;
top: 2.4rem;
} }
.empty { .empty {
@@ -868,6 +960,7 @@ strong.newcomer {
display: flex; display: flex;
gap: 0.3rem; gap: 0.3rem;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.pages button { .pages button {
@@ -889,32 +982,96 @@ strong.newcomer {
} }
@media (max-width: 900px) { @media (max-width: 900px) {
h1 { .page {
font-size: 1.35rem; gap: 0.85rem;
} }
.page-head { .page-head {
align-items: center; align-items: center;
} }
.actions .btn-ghost, .actions .excel-btn .btn-full {
.table-wrap,
.pager {
display: none; display: none;
} }
.actions .excel-btn .btn-short {
display: inline;
}
.actions .excel-btn {
padding-inline: 0.7rem;
font-size: 0.82rem;
}
.add-btn { .add-btn {
width: 40px; width: 40px;
height: 40px; height: 40px;
} }
.toolbar { .toolbar {
display: grid; flex-direction: column;
grid-template-columns: auto 1fr auto; align-items: stretch;
}
.search {
flex: 0 0 auto;
width: 100%;
}
.toolbar-tools {
width: 100%;
}
.toolbar-tools .pop {
flex: 1 1 auto;
}
.toolbar-tools .tool {
width: 100%;
justify-content: center;
}
.table-wrap {
display: none;
} }
.cards { .cards {
display: grid; display: grid;
} }
.pager {
flex-direction: column;
align-items: stretch;
text-align: center;
gap: 0.65rem;
}
.pages {
justify-content: center;
}
}
@media (max-width: 520px) {
.actions .excel-btn {
display: none;
}
.tool-label {
display: none;
}
.toolbar-tools .tool {
width: auto;
flex: 1 1 auto;
min-width: 44px;
justify-content: center;
padding-inline: 0.65rem;
}
.m-card {
padding: 0.75rem;
gap: 0.6rem;
}
} }
</style> </style>