Edit from phone contacts
This commit is contained in:
@@ -7,9 +7,11 @@ import { toFaDigits } from '@/lib/jalali';
|
||||
import { UiButton, UiModal } from '@/components/ui';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import {
|
||||
filterPhoneContacts,
|
||||
isContactPickerSupported,
|
||||
parseVCardFile,
|
||||
pickPhoneContacts,
|
||||
sortPhoneContacts,
|
||||
type PhoneContactDraft,
|
||||
} from '@/lib/phone-contacts';
|
||||
|
||||
@@ -30,18 +32,27 @@ const phase = ref<'closed' | 'help' | 'review'>('closed');
|
||||
const importing = ref(false);
|
||||
const drafts = ref<PhoneContactDraft[]>([]);
|
||||
const selected = ref<Set<string>>(new Set());
|
||||
const listQuery = ref('');
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const modalOpen = computed(() => phase.value !== 'closed');
|
||||
const modalTitle = computed(() =>
|
||||
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
||||
);
|
||||
const visibleDrafts = computed(() =>
|
||||
filterPhoneContacts(drafts.value, listQuery.value),
|
||||
);
|
||||
const selectedCount = computed(
|
||||
() => drafts.value.filter((d) => selected.value.has(d.key)).length,
|
||||
);
|
||||
const allSelected = computed(
|
||||
() => drafts.value.length > 0 && selectedCount.value === drafts.value.length,
|
||||
);
|
||||
const visibleAllSelected = computed(
|
||||
() =>
|
||||
visibleDrafts.value.length > 0 &&
|
||||
visibleDrafts.value.every((d) => selected.value.has(d.key)),
|
||||
);
|
||||
|
||||
function apiMessage(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
@@ -52,8 +63,9 @@ function apiMessage(e: unknown, fallback: string) {
|
||||
}
|
||||
|
||||
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
||||
drafts.value = rows;
|
||||
selected.value = opts?.selectAll ? new Set(rows.map((r) => r.key)) : new Set();
|
||||
drafts.value = sortPhoneContacts(rows);
|
||||
listQuery.value = '';
|
||||
selected.value = opts?.selectAll ? new Set(drafts.value.map((r) => r.key)) : new Set();
|
||||
// بعد از دیالوگ فایل، یک تیک صبر کن تا مودال مطمئن باز شود
|
||||
phase.value = 'closed';
|
||||
await nextTick();
|
||||
@@ -68,13 +80,25 @@ function toggle(key: string) {
|
||||
}
|
||||
|
||||
function selectAll(on: boolean) {
|
||||
selected.value = on ? new Set(drafts.value.map((d) => d.key)) : new Set();
|
||||
if (!on) {
|
||||
selected.value = new Set();
|
||||
return;
|
||||
}
|
||||
// اگر جستجو فعال است فقط نتایج را انتخاب کن، وگرنه همه
|
||||
if (listQuery.value.trim()) {
|
||||
const next = new Set(selected.value);
|
||||
for (const row of visibleDrafts.value) next.add(row.key);
|
||||
selected.value = next;
|
||||
} else {
|
||||
selected.value = new Set(drafts.value.map((d) => d.key));
|
||||
}
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
phase.value = 'closed';
|
||||
drafts.value = [];
|
||||
selected.value = new Set();
|
||||
listQuery.value = '';
|
||||
importing.value = false;
|
||||
}
|
||||
|
||||
@@ -98,7 +122,6 @@ async function startPicker() {
|
||||
emit('fail', 'مخاطبی انتخاب نشد.');
|
||||
return;
|
||||
}
|
||||
// از Contact Picker قبلاً انتخاب شدهاند → همه تیکخورده، ولی هنوز قابل کم/زیاد کردن
|
||||
await showReview(rows, { selectAll: true });
|
||||
} catch (e) {
|
||||
const name = e instanceof DOMException ? e.name : '';
|
||||
@@ -124,7 +147,6 @@ async function onVcfFile(ev: Event) {
|
||||
phase.value = 'help';
|
||||
return;
|
||||
}
|
||||
// از فایل: هیچکدام پیشفرض انتخاب نشود تا کاربر خودش یک/چند/همه را بزند
|
||||
await showReview(rows, { selectAll: false });
|
||||
} catch {
|
||||
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
||||
@@ -205,16 +227,35 @@ async function importSelected() {
|
||||
|
||||
<template v-else-if="phase === 'review'">
|
||||
<p class="lead">
|
||||
{{ toFaDigits(drafts.length) }} مخاطب از فایل/گوشی خوانده شد.
|
||||
{{ toFaDigits(drafts.length) }} مخاطب از فایل/گوشی خوانده شد (مرتبشده الفبایی).
|
||||
موارد دلخواه را تیک بزنید؛ فقط انتخابشدهها وارد میشوند.
|
||||
</p>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="link" @click="selectAll(true)">انتخاب همه</button>
|
||||
<button type="button" class="link" @click="selectAll(false)">هیچکدام</button>
|
||||
<span class="count">{{ toFaDigits(selectedCount) }} از {{ toFaDigits(drafts.length) }}</span>
|
||||
<div class="search">
|
||||
<UiIcon name="search" :size="15" />
|
||||
<input
|
||||
v-model="listQuery"
|
||||
type="search"
|
||||
placeholder="جستجو نام یا موبایل…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<ul class="list">
|
||||
<li v-for="row in drafts" :key="row.key">
|
||||
<div class="toolbar">
|
||||
<button type="button" class="link" @click="selectAll(true)">
|
||||
{{ listQuery.trim() ? 'انتخاب نتایج' : 'انتخاب همه' }}
|
||||
</button>
|
||||
<button type="button" class="link" @click="selectAll(false)">هیچکدام</button>
|
||||
<span class="count">
|
||||
{{ toFaDigits(selectedCount) }} انتخاب
|
||||
<template v-if="listQuery.trim()">
|
||||
· {{ toFaDigits(visibleDrafts.length) }} نتیجه
|
||||
</template>
|
||||
<template v-else>
|
||||
از {{ toFaDigits(drafts.length) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<ul v-if="visibleDrafts.length" class="list">
|
||||
<li v-for="row in visibleDrafts" :key="row.key">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -230,6 +271,7 @@ async function importSelected() {
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="empty">مخاطبی با این جستجو پیدا نشد.</p>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
@@ -251,9 +293,12 @@ async function importSelected() {
|
||||
@click="importSelected"
|
||||
>
|
||||
<UiIcon name="user-plus" :size="16" />
|
||||
<template v-if="allSelected && selectedCount">
|
||||
<template v-if="allSelected && selectedCount && !listQuery.trim()">
|
||||
ورود همه ({{ toFaDigits(selectedCount) }})
|
||||
</template>
|
||||
<template v-else-if="visibleAllSelected && listQuery.trim() && selectedCount">
|
||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||
</template>
|
||||
<template v-else>
|
||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||
</template>
|
||||
@@ -292,6 +337,28 @@ async function importSelected() {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.65rem;
|
||||
padding: 0.45rem 0.65rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 0.84rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -367,4 +434,11 @@ async function importSelected() {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin: 0.75rem 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.84rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user