Edit: Add from phone contacts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import type { CreatePersonRequest, PersonDto } from '@mento/shared';
|
||||
@@ -25,16 +25,23 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const pickerSupported = isContactPickerSupported();
|
||||
const open = ref(false);
|
||||
const fallbackOpen = ref(false);
|
||||
/** یک مودال با دو مرحله تا بعد از انتخاب فایل، لیست تأیید گم نشود */
|
||||
const phase = ref<'closed' | 'help' | 'review'>('closed');
|
||||
const importing = ref(false);
|
||||
const drafts = ref<PhoneContactDraft[]>([]);
|
||||
const selected = ref<Set<string>>(new Set());
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const modalOpen = computed(() => phase.value !== 'closed');
|
||||
const modalTitle = computed(() =>
|
||||
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
||||
);
|
||||
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,
|
||||
);
|
||||
|
||||
function apiMessage(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
@@ -44,15 +51,13 @@ function apiMessage(e: unknown, fallback: string) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function setDrafts(rows: PhoneContactDraft[]) {
|
||||
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
||||
drafts.value = rows;
|
||||
selected.value = new Set(rows.map((r) => r.key));
|
||||
open.value = rows.length > 0;
|
||||
fallbackOpen.value = false;
|
||||
if (!rows.length) {
|
||||
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
||||
emit('settled');
|
||||
}
|
||||
selected.value = opts?.selectAll ? new Set(rows.map((r) => r.key)) : new Set();
|
||||
// بعد از دیالوگ فایل، یک تیک صبر کن تا مودال مطمئن باز شود
|
||||
phase.value = 'closed';
|
||||
await nextTick();
|
||||
phase.value = 'review';
|
||||
}
|
||||
|
||||
function toggle(key: string) {
|
||||
@@ -67,8 +72,7 @@ function selectAll(on: boolean) {
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
open.value = false;
|
||||
fallbackOpen.value = false;
|
||||
phase.value = 'closed';
|
||||
drafts.value = [];
|
||||
selected.value = new Set();
|
||||
importing.value = false;
|
||||
@@ -79,32 +83,32 @@ function cancel() {
|
||||
emit('settled');
|
||||
}
|
||||
|
||||
function openFallback() {
|
||||
fallbackOpen.value = true;
|
||||
function openHelp() {
|
||||
phase.value = 'help';
|
||||
}
|
||||
|
||||
async function startPicker() {
|
||||
emit('start');
|
||||
try {
|
||||
if (!pickerSupported) {
|
||||
openFallback();
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
const rows = await pickPhoneContacts();
|
||||
setDrafts(rows);
|
||||
const rows = await pickPhoneContacts({ multiple: true });
|
||||
if (!rows.length) {
|
||||
emit('fail', 'مخاطبی انتخاب نشد.');
|
||||
return;
|
||||
}
|
||||
// از Contact Picker قبلاً انتخاب شدهاند → همه تیکخورده، ولی هنوز قابل کم/زیاد کردن
|
||||
await showReview(rows, { selectAll: true });
|
||||
} catch (e) {
|
||||
const name = e instanceof DOMException ? e.name : '';
|
||||
const message = e instanceof Error ? e.message : '';
|
||||
if (name === 'AbortError') {
|
||||
emit('settled');
|
||||
return;
|
||||
}
|
||||
if (name === 'AbortError') return;
|
||||
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
||||
openFallback();
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
emit('fail', 'انتخاب مخاطب از گوشی ناموفق بود.');
|
||||
emit('settled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,11 +119,16 @@ async function onVcfFile(ev: Event) {
|
||||
if (!file) return;
|
||||
try {
|
||||
const rows = await parseVCardFile(file);
|
||||
setDrafts(rows);
|
||||
if (!rows.length) {
|
||||
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
||||
phase.value = 'help';
|
||||
return;
|
||||
}
|
||||
// از فایل: هیچکدام پیشفرض انتخاب نشود تا کاربر خودش یک/چند/همه را بزند
|
||||
await showReview(rows, { selectAll: false });
|
||||
} catch {
|
||||
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
||||
emit('settled');
|
||||
fallbackOpen.value = false;
|
||||
phase.value = 'help';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +140,7 @@ async function importSelected() {
|
||||
const rows = drafts.value.filter((d) => selected.value.has(d.key));
|
||||
if (!rows.length || importing.value) return;
|
||||
importing.value = true;
|
||||
emit('start');
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
@@ -180,68 +190,75 @@ async function importSelected() {
|
||||
@change="onVcfFile"
|
||||
/>
|
||||
|
||||
<UiModal :open="fallbackOpen" title="ورود از مخاطبین گوشی" @close="cancel">
|
||||
<p class="lead">
|
||||
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
||||
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
||||
</p>
|
||||
<ul class="tips">
|
||||
<li>اندروید Chrome معمولاً انتخاب مستقیم مخاطب را دارد.</li>
|
||||
<li>آیفون: Contacts → Share Contact / Export → فایل vCard.</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||
<UiButton variant="primary" :disabled="importing || disabled" @click="fileInput?.click()">
|
||||
انتخاب فایل .vcf
|
||||
</UiButton>
|
||||
<UiModal :open="modalOpen" :title="modalTitle" @close="cancel">
|
||||
<template v-if="phase === 'help'">
|
||||
<p class="lead">
|
||||
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
||||
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
||||
بعد از بارگذاری میتوانید یک، چند، یا همه مخاطبین را انتخاب کنید.
|
||||
</p>
|
||||
<ul class="tips">
|
||||
<li>اندروید Chrome معمولاً انتخاب مستقیم مخاطب را دارد.</li>
|
||||
<li>آیفون: Contacts → Share Contact / Export → فایل vCard.</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-else-if="phase === 'review'">
|
||||
<p class="lead">
|
||||
{{ 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>
|
||||
<ul class="list">
|
||||
<li v-for="row in drafts" :key="row.key">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selected.has(row.key)"
|
||||
:disabled="importing"
|
||||
@change="toggle(row.key)"
|
||||
/>
|
||||
<span class="avatar" aria-hidden="true">{{ row.fullName.slice(0, 1) }}</span>
|
||||
<span class="meta">
|
||||
<strong>{{ row.fullName }}</strong>
|
||||
<small>{{ draftLine(row) }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</UiModal>
|
||||
|
||||
<UiModal :open="open" title="تأیید مخاطبین" @close="cancel">
|
||||
<p class="lead">
|
||||
{{ 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) }} انتخابشده</span>
|
||||
</div>
|
||||
<ul class="list">
|
||||
<li v-for="row in drafts" :key="row.key">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selected.has(row.key)"
|
||||
:disabled="importing"
|
||||
@change="toggle(row.key)"
|
||||
/>
|
||||
<span class="avatar" aria-hidden="true">{{ row.fullName.slice(0, 1) }}</span>
|
||||
<span class="meta">
|
||||
<strong>{{ row.fullName }}</strong>
|
||||
<small>{{ draftLine(row) }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||
<UiButton
|
||||
v-if="!pickerSupported"
|
||||
variant="ghost"
|
||||
:disabled="importing"
|
||||
@click="fileInput?.click()"
|
||||
>
|
||||
فایل دیگر
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="primary"
|
||||
:disabled="importing || !selectedCount"
|
||||
:loading="importing"
|
||||
@click="importSelected"
|
||||
>
|
||||
<UiIcon name="user-plus" :size="16" />
|
||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||
</UiButton>
|
||||
<template v-if="phase === 'help'">
|
||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||
<UiButton variant="primary" :disabled="importing" @click="fileInput?.click()">
|
||||
انتخاب فایل .vcf
|
||||
</UiButton>
|
||||
</template>
|
||||
<template v-else-if="phase === 'review'">
|
||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||
<UiButton variant="ghost" :disabled="importing" @click="fileInput?.click()">
|
||||
فایل دیگر
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="primary"
|
||||
:disabled="importing || !selectedCount"
|
||||
:loading="importing"
|
||||
@click="importSelected"
|
||||
>
|
||||
<UiIcon name="user-plus" :size="16" />
|
||||
<template v-if="allSelected && selectedCount">
|
||||
ورود همه ({{ toFaDigits(selectedCount) }})
|
||||
</template>
|
||||
<template v-else>
|
||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||
</template>
|
||||
</UiButton>
|
||||
</template>
|
||||
</template>
|
||||
</UiModal>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user