Edit: Add from Android phone contacts

This commit is contained in:
2026-09-21 20:51:06 +03:30
parent 9249302eac
commit be97a067f9
3 changed files with 110 additions and 41 deletions
+50 -31
View File
@@ -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;
+7 -1
View File
@@ -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);
+53 -9
View File
@@ -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;