Edit: Add from Android phone contacts
This commit is contained in:
@@ -28,7 +28,7 @@ const emit = defineEmits<{
|
||||
|
||||
const pickerSupported = isContactPickerSupported();
|
||||
/** یک مودال با دو مرحله تا بعد از انتخاب فایل، لیست تأیید گم نشود */
|
||||
const phase = ref<'closed' | 'help' | 'review'>('closed');
|
||||
const phase = ref<'closed' | 'choose' | 'help' | 'review'>('closed');
|
||||
const importing = ref(false);
|
||||
const drafts = ref<PhoneContactDraft[]>([]);
|
||||
const selected = ref<Set<string>>(new Set());
|
||||
@@ -36,23 +36,17 @@ const listQuery = ref('');
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const modalOpen = computed(() => phase.value !== 'closed');
|
||||
const modalTitle = computed(() =>
|
||||
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
||||
);
|
||||
const modalTitle = computed(() => {
|
||||
if (phase.value === 'review') return 'انتخاب مخاطبین برای ورود';
|
||||
if (phase.value === 'choose') return 'ورود از مخاطبین گوشی';
|
||||
return 'ورود از فایل مخاطبین';
|
||||
});
|
||||
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)) {
|
||||
@@ -111,18 +105,19 @@ function openHelp() {
|
||||
phase.value = 'help';
|
||||
}
|
||||
|
||||
async function startPicker() {
|
||||
function openChoose() {
|
||||
phase.value = 'choose';
|
||||
}
|
||||
|
||||
async function runDevicePicker() {
|
||||
try {
|
||||
if (!pickerSupported) {
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
const rows = await pickPhoneContacts({ multiple: true });
|
||||
if (!rows.length) {
|
||||
emit('fail', 'مخاطبی انتخاب نشد.');
|
||||
return;
|
||||
}
|
||||
await showReview(rows, { selectAll: true });
|
||||
// مثل vcf: جستجو/انتخاب دستی، نه ورود خودکار همه
|
||||
await showReview(rows, { selectAll: false });
|
||||
} catch (e) {
|
||||
const name = e instanceof DOMException ? e.name : '';
|
||||
const message = e instanceof Error ? e.message : '';
|
||||
@@ -135,6 +130,14 @@ async function startPicker() {
|
||||
}
|
||||
}
|
||||
|
||||
function startPicker() {
|
||||
if (pickerSupported) {
|
||||
openChoose();
|
||||
return;
|
||||
}
|
||||
openHelp();
|
||||
}
|
||||
|
||||
async function onVcfFile(ev: Event) {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
const file = el.files?.[0];
|
||||
@@ -144,13 +147,13 @@ async function onVcfFile(ev: Event) {
|
||||
const rows = await parseVCardFile(file);
|
||||
if (!rows.length) {
|
||||
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
||||
phase.value = 'help';
|
||||
phase.value = pickerSupported ? 'choose' : 'help';
|
||||
return;
|
||||
}
|
||||
await showReview(rows, { selectAll: false });
|
||||
} catch {
|
||||
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
||||
phase.value = 'help';
|
||||
phase.value = pickerSupported ? 'choose' : 'help';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +216,23 @@ async function importSelected() {
|
||||
/>
|
||||
|
||||
<UiModal :open="modalOpen" :title="modalTitle" @close="cancel">
|
||||
<template v-if="phase === 'help'">
|
||||
<template v-if="phase === 'choose'">
|
||||
<p class="lead">
|
||||
مخاطبین را از دفترچه تلفن یا فایل <strong>.vcf</strong> بگیرید.
|
||||
بعد در لیست الفبایی جستجو کنید و یک، چند، یا همه را برای ورود انتخاب کنید.
|
||||
</p>
|
||||
<div class="choose-actions">
|
||||
<UiButton variant="primary" block @click="runDevicePicker">
|
||||
<UiIcon name="phone" :size="16" />
|
||||
از دفترچه تلفن
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" block @click="fileInput?.click()">
|
||||
از فایل .vcf
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="phase === 'help'">
|
||||
<p class="lead">
|
||||
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
||||
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
||||
@@ -275,7 +294,10 @@ async function importSelected() {
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="phase === 'help'">
|
||||
<template v-if="phase === 'choose'">
|
||||
<UiButton variant="ghost" @click="cancel">انصراف</UiButton>
|
||||
</template>
|
||||
<template v-else-if="phase === 'help'">
|
||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||
<UiButton variant="primary" :disabled="importing" @click="fileInput?.click()">
|
||||
انتخاب فایل .vcf
|
||||
@@ -293,15 +315,7 @@ async function importSelected() {
|
||||
@click="importSelected"
|
||||
>
|
||||
<UiIcon name="user-plus" :size="16" />
|
||||
<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>
|
||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||
</UiButton>
|
||||
</template>
|
||||
</template>
|
||||
@@ -329,6 +343,11 @@ async function importSelected() {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.choose-actions {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin: 0;
|
||||
padding: 0 1.1rem 0 0;
|
||||
|
||||
@@ -5,6 +5,8 @@ import UiIcon from '@/components/UiIcon.vue';
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
/** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */
|
||||
nest?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -29,7 +31,7 @@ function onBackdrop(event: MouseEvent) {
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="backdrop" @click="onBackdrop">
|
||||
<div v-if="open" class="backdrop" :class="{ nest }" @click="onBackdrop">
|
||||
<section
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
@@ -65,6 +67,10 @@ function onBackdrop(event: MouseEvent) {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.backdrop.nest {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: min(880px, 100%);
|
||||
max-height: min(88dvh, 88vh, 880px);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
filterPhoneContacts,
|
||||
isContactPickerSupported,
|
||||
parseVCardFile,
|
||||
pickOnePhoneContact,
|
||||
pickPhoneContacts,
|
||||
sortPhoneContacts,
|
||||
type PhoneContactDraft,
|
||||
} from '@/lib/phone-contacts';
|
||||
@@ -69,6 +69,7 @@ const loyaltyTierName = ref('');
|
||||
const contactPickerSupported = isContactPickerSupported();
|
||||
const pickingContact = ref(false);
|
||||
const contactFileInput = ref<HTMLInputElement | null>(null);
|
||||
const sourceOpen = ref(false);
|
||||
const pickOpen = ref(false);
|
||||
const pickDrafts = ref<PhoneContactDraft[]>([]);
|
||||
const pickQuery = ref('');
|
||||
@@ -112,6 +113,10 @@ function closePick() {
|
||||
pickQuery.value = '';
|
||||
}
|
||||
|
||||
function closeSource() {
|
||||
sourceOpen.value = false;
|
||||
}
|
||||
|
||||
function choosePick(row: PhoneContactDraft) {
|
||||
applyContactDraft(row);
|
||||
closePick();
|
||||
@@ -120,23 +125,34 @@ function choosePick(row: PhoneContactDraft) {
|
||||
async function openPickList(rows: PhoneContactDraft[]) {
|
||||
pickDrafts.value = sortPhoneContacts(rows);
|
||||
pickQuery.value = '';
|
||||
sourceOpen.value = false;
|
||||
pickOpen.value = false;
|
||||
await nextTick();
|
||||
pickOpen.value = true;
|
||||
}
|
||||
|
||||
async function fillFromPhoneContact() {
|
||||
function fillFromPhoneContact() {
|
||||
if (pickingContact.value || saving.value) return;
|
||||
error.value = '';
|
||||
if (contactPickerSupported) {
|
||||
sourceOpen.value = true;
|
||||
return;
|
||||
}
|
||||
contactFileInput.value?.click();
|
||||
}
|
||||
|
||||
async function pickFromDevice() {
|
||||
if (pickingContact.value || saving.value) return;
|
||||
pickingContact.value = true;
|
||||
error.value = '';
|
||||
sourceOpen.value = false;
|
||||
try {
|
||||
if (!contactPickerSupported) {
|
||||
contactFileInput.value?.click();
|
||||
const rows = await pickPhoneContacts({ multiple: true });
|
||||
if (!rows.length) {
|
||||
error.value = 'مخاطبی انتخاب نشد.';
|
||||
return;
|
||||
}
|
||||
const draft = await pickOnePhoneContact();
|
||||
if (!draft) return;
|
||||
applyContactDraft(draft);
|
||||
await openPickList(rows);
|
||||
} catch (e) {
|
||||
const name = e instanceof DOMException ? e.name : '';
|
||||
const message = e instanceof Error ? e.message : '';
|
||||
@@ -151,6 +167,11 @@ async function fillFromPhoneContact() {
|
||||
}
|
||||
}
|
||||
|
||||
function pickFromVcf() {
|
||||
sourceOpen.value = false;
|
||||
contactFileInput.value?.click();
|
||||
}
|
||||
|
||||
async function onContactVcfFile(ev: Event) {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
const file = el.files?.[0];
|
||||
@@ -339,9 +360,27 @@ onMounted(load);
|
||||
@change="onContactVcfFile"
|
||||
/>
|
||||
|
||||
<UiModal :open="pickOpen" title="انتخاب مخاطب" @close="closePick">
|
||||
<UiModal nest :open="sourceOpen" title="از مخاطبین" @close="closeSource">
|
||||
<p class="pick-lead">
|
||||
{{ toFaDigits(pickDrafts.length) }} مخاطب در فایل (مرتبشده الفبایی).
|
||||
چند مخاطب را از دفترچه یا فایل بگیرید؛ بعد در لیست الفبایی جستجو کنید و یکی را برای پر کردن موبایل انتخاب کنید.
|
||||
</p>
|
||||
<div class="pick-choose">
|
||||
<UiButton variant="primary" block :disabled="pickingContact" @click="pickFromDevice">
|
||||
<UiIcon name="phone" :size="16" />
|
||||
از دفترچه تلفن
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" block :disabled="pickingContact" @click="pickFromVcf">
|
||||
از فایل .vcf
|
||||
</UiButton>
|
||||
</div>
|
||||
<template #footer>
|
||||
<UiButton variant="ghost" @click="closeSource">انصراف</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
|
||||
<UiModal nest :open="pickOpen" title="انتخاب مخاطب" @close="closePick">
|
||||
<p class="pick-lead">
|
||||
{{ toFaDigits(pickDrafts.length) }} مخاطب (مرتبشده الفبایی).
|
||||
یکی را انتخاب کنید تا فقط موبایل (و در صورت خالی بودن، نام) پر شود.
|
||||
</p>
|
||||
<div class="pick-search">
|
||||
@@ -561,6 +600,11 @@ onMounted(load);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.pick-choose {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.pick-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user