Edit: Add from phone contacts
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { CreatePersonRequest, PersonDto } from '@mento/shared';
|
import type { CreatePersonRequest, PersonDto } from '@mento/shared';
|
||||||
@@ -25,16 +25,23 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pickerSupported = isContactPickerSupported();
|
const pickerSupported = isContactPickerSupported();
|
||||||
const open = ref(false);
|
/** یک مودال با دو مرحله تا بعد از انتخاب فایل، لیست تأیید گم نشود */
|
||||||
const fallbackOpen = ref(false);
|
const phase = ref<'closed' | 'help' | 'review'>('closed');
|
||||||
const importing = ref(false);
|
const importing = ref(false);
|
||||||
const drafts = ref<PhoneContactDraft[]>([]);
|
const drafts = ref<PhoneContactDraft[]>([]);
|
||||||
const selected = ref<Set<string>>(new Set());
|
const selected = ref<Set<string>>(new Set());
|
||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const modalOpen = computed(() => phase.value !== 'closed');
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
phase.value === 'review' ? 'انتخاب مخاطبین برای ورود' : 'ورود از مخاطبین گوشی',
|
||||||
|
);
|
||||||
const selectedCount = computed(
|
const selectedCount = computed(
|
||||||
() => drafts.value.filter((d) => selected.value.has(d.key)).length,
|
() => 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) {
|
function apiMessage(e: unknown, fallback: string) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
@@ -44,15 +51,13 @@ function apiMessage(e: unknown, fallback: string) {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDrafts(rows: PhoneContactDraft[]) {
|
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
||||||
drafts.value = rows;
|
drafts.value = rows;
|
||||||
selected.value = new Set(rows.map((r) => r.key));
|
selected.value = opts?.selectAll ? new Set(rows.map((r) => r.key)) : new Set();
|
||||||
open.value = rows.length > 0;
|
// بعد از دیالوگ فایل، یک تیک صبر کن تا مودال مطمئن باز شود
|
||||||
fallbackOpen.value = false;
|
phase.value = 'closed';
|
||||||
if (!rows.length) {
|
await nextTick();
|
||||||
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
phase.value = 'review';
|
||||||
emit('settled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(key: string) {
|
function toggle(key: string) {
|
||||||
@@ -67,8 +72,7 @@ function selectAll(on: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeAll() {
|
function closeAll() {
|
||||||
open.value = false;
|
phase.value = 'closed';
|
||||||
fallbackOpen.value = false;
|
|
||||||
drafts.value = [];
|
drafts.value = [];
|
||||||
selected.value = new Set();
|
selected.value = new Set();
|
||||||
importing.value = false;
|
importing.value = false;
|
||||||
@@ -79,32 +83,32 @@ function cancel() {
|
|||||||
emit('settled');
|
emit('settled');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFallback() {
|
function openHelp() {
|
||||||
fallbackOpen.value = true;
|
phase.value = 'help';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startPicker() {
|
async function startPicker() {
|
||||||
emit('start');
|
|
||||||
try {
|
try {
|
||||||
if (!pickerSupported) {
|
if (!pickerSupported) {
|
||||||
openFallback();
|
openHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rows = await pickPhoneContacts();
|
const rows = await pickPhoneContacts({ multiple: true });
|
||||||
setDrafts(rows);
|
if (!rows.length) {
|
||||||
|
emit('fail', 'مخاطبی انتخاب نشد.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// از Contact Picker قبلاً انتخاب شدهاند → همه تیکخورده، ولی هنوز قابل کم/زیاد کردن
|
||||||
|
await showReview(rows, { selectAll: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const name = e instanceof DOMException ? e.name : '';
|
const name = e instanceof DOMException ? e.name : '';
|
||||||
const message = e instanceof Error ? e.message : '';
|
const message = e instanceof Error ? e.message : '';
|
||||||
if (name === 'AbortError') {
|
if (name === 'AbortError') return;
|
||||||
emit('settled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
||||||
openFallback();
|
openHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit('fail', 'انتخاب مخاطب از گوشی ناموفق بود.');
|
emit('fail', 'انتخاب مخاطب از گوشی ناموفق بود.');
|
||||||
emit('settled');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +119,16 @@ async function onVcfFile(ev: Event) {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
const rows = await parseVCardFile(file);
|
const rows = await parseVCardFile(file);
|
||||||
setDrafts(rows);
|
if (!rows.length) {
|
||||||
|
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
||||||
|
phase.value = 'help';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// از فایل: هیچکدام پیشفرض انتخاب نشود تا کاربر خودش یک/چند/همه را بزند
|
||||||
|
await showReview(rows, { selectAll: false });
|
||||||
} catch {
|
} catch {
|
||||||
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
||||||
emit('settled');
|
phase.value = 'help';
|
||||||
fallbackOpen.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +140,7 @@ async function importSelected() {
|
|||||||
const rows = drafts.value.filter((d) => selected.value.has(d.key));
|
const rows = drafts.value.filter((d) => selected.value.has(d.key));
|
||||||
if (!rows.length || importing.value) return;
|
if (!rows.length || importing.value) return;
|
||||||
importing.value = true;
|
importing.value = true;
|
||||||
|
emit('start');
|
||||||
let created = 0;
|
let created = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -180,68 +190,75 @@ async function importSelected() {
|
|||||||
@change="onVcfFile"
|
@change="onVcfFile"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiModal :open="fallbackOpen" title="ورود از مخاطبین گوشی" @close="cancel">
|
<UiModal :open="modalOpen" :title="modalTitle" @close="cancel">
|
||||||
<p class="lead">
|
<template v-if="phase === 'help'">
|
||||||
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
<p class="lead">
|
||||||
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
||||||
</p>
|
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
||||||
<ul class="tips">
|
بعد از بارگذاری میتوانید یک، چند، یا همه مخاطبین را انتخاب کنید.
|
||||||
<li>اندروید Chrome معمولاً انتخاب مستقیم مخاطب را دارد.</li>
|
</p>
|
||||||
<li>آیفون: Contacts → Share Contact / Export → فایل vCard.</li>
|
<ul class="tips">
|
||||||
</ul>
|
<li>اندروید Chrome معمولاً انتخاب مستقیم مخاطب را دارد.</li>
|
||||||
<template #footer>
|
<li>آیفون: Contacts → Share Contact / Export → فایل vCard.</li>
|
||||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
</ul>
|
||||||
<UiButton variant="primary" :disabled="importing || disabled" @click="fileInput?.click()">
|
</template>
|
||||||
انتخاب فایل .vcf
|
|
||||||
</UiButton>
|
<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>
|
</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>
|
<template #footer>
|
||||||
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
<template v-if="phase === 'help'">
|
||||||
<UiButton
|
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||||
v-if="!pickerSupported"
|
<UiButton variant="primary" :disabled="importing" @click="fileInput?.click()">
|
||||||
variant="ghost"
|
انتخاب فایل .vcf
|
||||||
:disabled="importing"
|
</UiButton>
|
||||||
@click="fileInput?.click()"
|
</template>
|
||||||
>
|
<template v-else-if="phase === 'review'">
|
||||||
فایل دیگر
|
<UiButton variant="ghost" :disabled="importing" @click="cancel">انصراف</UiButton>
|
||||||
</UiButton>
|
<UiButton variant="ghost" :disabled="importing" @click="fileInput?.click()">
|
||||||
<UiButton
|
فایل دیگر
|
||||||
variant="primary"
|
</UiButton>
|
||||||
:disabled="importing || !selectedCount"
|
<UiButton
|
||||||
:loading="importing"
|
variant="primary"
|
||||||
@click="importSelected"
|
:disabled="importing || !selectedCount"
|
||||||
>
|
:loading="importing"
|
||||||
<UiIcon name="user-plus" :size="16" />
|
@click="importSelected"
|
||||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
>
|
||||||
</UiButton>
|
<UiIcon name="user-plus" :size="16" />
|
||||||
|
<template v-if="allSelected && selectedCount">
|
||||||
|
ورود همه ({{ toFaDigits(selectedCount) }})
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
ورود {{ toFaDigits(selectedCount) }} نفر
|
||||||
|
</template>
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</UiModal>
|
</UiModal>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -99,12 +99,15 @@ function draftFromParts(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pickPhoneContacts(): Promise<PhoneContactDraft[]> {
|
export async function pickPhoneContacts(options?: {
|
||||||
|
multiple?: boolean;
|
||||||
|
}): Promise<PhoneContactDraft[]> {
|
||||||
const api = contactsApi();
|
const api = contactsApi();
|
||||||
if (!api) {
|
if (!api) {
|
||||||
throw new Error('CONTACT_PICKER_UNSUPPORTED');
|
throw new Error('CONTACT_PICKER_UNSUPPORTED');
|
||||||
}
|
}
|
||||||
const rows = await api.select(['name', 'email', 'tel'], { multiple: true });
|
const multiple = options?.multiple !== false;
|
||||||
|
const rows = await api.select(['name', 'email', 'tel'], { multiple });
|
||||||
const drafts: PhoneContactDraft[] = [];
|
const drafts: PhoneContactDraft[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -121,6 +124,12 @@ export async function pickPhoneContacts(): Promise<PhoneContactDraft[]> {
|
|||||||
return drafts;
|
return drafts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** یک مخاطب برای پر کردن فرم (نه ورود گروهی) */
|
||||||
|
export async function pickOnePhoneContact(): Promise<PhoneContactDraft | null> {
|
||||||
|
const rows = await pickPhoneContacts({ multiple: false });
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function unfoldVcard(text: string): string[] {
|
function unfoldVcard(text: string): string[] {
|
||||||
const raw = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
const raw = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import axios from 'axios';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
|
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
|
||||||
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
||||||
|
import {
|
||||||
|
isContactPickerSupported,
|
||||||
|
parseVCardFile,
|
||||||
|
pickOnePhoneContact,
|
||||||
|
type PhoneContactDraft,
|
||||||
|
} from '@/lib/phone-contacts';
|
||||||
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
||||||
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
||||||
import EntityTimeline from '@/components/EntityTimeline.vue';
|
import EntityTimeline from '@/components/EntityTimeline.vue';
|
||||||
@@ -56,10 +62,83 @@ const customFields = ref<Record<string, unknown>>({});
|
|||||||
const loyaltyPoints = ref(0);
|
const loyaltyPoints = ref(0);
|
||||||
const loyaltyTierName = ref('');
|
const loyaltyTierName = ref('');
|
||||||
|
|
||||||
|
const contactPickerSupported = isContactPickerSupported();
|
||||||
|
const pickingContact = ref(false);
|
||||||
|
const contactFileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const backTo = computed(() =>
|
const backTo = computed(() =>
|
||||||
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
|
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** فقط موبایل را اجباری پر میکند؛ نام و بقیه فقط اگر خالی باشند. */
|
||||||
|
function applyContactDraft(draft: PhoneContactDraft) {
|
||||||
|
if (draft.mobile) {
|
||||||
|
mobile.value = draft.mobile;
|
||||||
|
} else if (draft.phone && !mobile.value.trim()) {
|
||||||
|
mobile.value = draft.phone;
|
||||||
|
}
|
||||||
|
if (!firstName.value.trim() && draft.firstName && draft.firstName !== 'بدوننام') {
|
||||||
|
firstName.value = draft.firstName;
|
||||||
|
}
|
||||||
|
if (!lastName.value.trim() && draft.lastName) {
|
||||||
|
lastName.value = draft.lastName;
|
||||||
|
}
|
||||||
|
if (!phone.value.trim() && draft.phone && draft.phone !== draft.mobile) {
|
||||||
|
phone.value = draft.phone;
|
||||||
|
}
|
||||||
|
if (!email.value.trim() && draft.email) {
|
||||||
|
email.value = draft.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillFromPhoneContact() {
|
||||||
|
if (pickingContact.value || saving.value) return;
|
||||||
|
pickingContact.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
if (!contactPickerSupported) {
|
||||||
|
contactFileInput.value?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const draft = await pickOnePhoneContact();
|
||||||
|
if (!draft) return;
|
||||||
|
applyContactDraft(draft);
|
||||||
|
} catch (e) {
|
||||||
|
const name = e instanceof DOMException ? e.name : '';
|
||||||
|
const message = e instanceof Error ? e.message : '';
|
||||||
|
if (name === 'AbortError') return;
|
||||||
|
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
||||||
|
contactFileInput.value?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.value = 'انتخاب مخاطب از گوشی ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
pickingContact.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onContactVcfFile(ev: Event) {
|
||||||
|
const el = ev.target as HTMLInputElement;
|
||||||
|
const file = el.files?.[0];
|
||||||
|
el.value = '';
|
||||||
|
if (!file) return;
|
||||||
|
pickingContact.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const rows = await parseVCardFile(file);
|
||||||
|
const draft = rows[0];
|
||||||
|
if (!draft) {
|
||||||
|
error.value = 'مخاطبی در فایل پیدا نشد.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyContactDraft(draft);
|
||||||
|
} catch {
|
||||||
|
error.value = 'خواندن فایل مخاطب (.vcf) ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
pickingContact.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fill(p: PersonDto) {
|
function fill(p: PersonDto) {
|
||||||
firstName.value = p.firstName;
|
firstName.value = p.firstName;
|
||||||
lastName.value = p.lastName;
|
lastName.value = p.lastName;
|
||||||
@@ -205,9 +284,28 @@ onMounted(load);
|
|||||||
</UiField>
|
</UiField>
|
||||||
|
|
||||||
<UiField label="موبایل" for-id="mobile">
|
<UiField label="موبایل" for-id="mobile">
|
||||||
|
<template #label-extra>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="from-contact"
|
||||||
|
:disabled="pickingContact || saving"
|
||||||
|
@click="fillFromPhoneContact"
|
||||||
|
>
|
||||||
|
<UiIcon name="phone" :size="14" />
|
||||||
|
{{ pickingContact ? '…' : 'از مخاطبین' }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
<UiInput id="mobile" v-model="mobile" type="tel" />
|
<UiInput id="mobile" v-model="mobile" type="tel" />
|
||||||
</UiField>
|
</UiField>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="contactFileInput"
|
||||||
|
class="hidden-file"
|
||||||
|
type="file"
|
||||||
|
accept=".vcf,text/vcard,text/x-vcard"
|
||||||
|
@change="onContactVcfFile"
|
||||||
|
/>
|
||||||
|
|
||||||
<UiField label="تلفن" for-id="phone">
|
<UiField label="تلفن" for-id="phone">
|
||||||
<UiInput id="phone" v-model="phone" type="tel" />
|
<UiInput id="phone" v-model="phone" type="tel" />
|
||||||
</UiField>
|
</UiField>
|
||||||
@@ -367,6 +465,30 @@ onMounted(load);
|
|||||||
margin-top: var(--sp-4);
|
margin-top: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.from-contact {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-deep);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-contact:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 901px) {
|
@media (min-width: 901px) {
|
||||||
.mob-only {
|
.mob-only {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user