2503 lines
59 KiB
Vue
2503 lines
59 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||
import { useEscClose } from '@/lib/modal-stack';
|
||
import { matchesSearch } from '@/lib/search';
|
||
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
||
import axios from 'axios';
|
||
import { api } from '@/lib/api';
|
||
import UiIcon from '@/components/UiIcon.vue';
|
||
import { UiDropdownMenu } from '@/components/ui';
|
||
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||
import type {
|
||
CreateMessageAutomationRuleRequest,
|
||
MessageAutomationChannel,
|
||
MessageAutomationDelayUnit,
|
||
MessageAutomationRuleDto,
|
||
MessageAutomationTarget,
|
||
MessageAutomationTrigger,
|
||
UpdateMessageAutomationRuleRequest,
|
||
} from '@mento/shared';
|
||
import {
|
||
MESSAGE_AUTOMATION_CHANNEL_LABELS,
|
||
MESSAGE_AUTOMATION_FIELDS,
|
||
MESSAGE_AUTOMATION_SMS_SEGMENT,
|
||
MESSAGE_AUTOMATION_TARGET_LABELS,
|
||
MESSAGE_AUTOMATION_TRIGGER_LABELS,
|
||
MESSAGE_AUTOMATION_TRIGGERS,
|
||
MESSAGE_AUTOMATION_UNIT_LABELS,
|
||
} from '@mento/shared';
|
||
|
||
const MOBILE_MQ = '(max-width: 979px)';
|
||
const DELAY_UNITS: MessageAutomationDelayUnit[] = ['hours', 'days', 'weeks', 'months'];
|
||
const DELAY_MAX: Record<MessageAutomationDelayUnit, number> = {
|
||
minutes: 1,
|
||
hours: 23,
|
||
days: 6,
|
||
weeks: 3,
|
||
months: 11,
|
||
};
|
||
const EMOJIS = ['🙏', '😊', '❤️', '👍', '🎉', '🌸', '✨', '👋', '💐', '🌟', '😍', '🤝'];
|
||
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const error = ref('');
|
||
const ok = ref('');
|
||
const items = ref<MessageAutomationRuleDto[]>([]);
|
||
const editingId = ref<string | null>(null);
|
||
|
||
const name = ref('');
|
||
const description = ref('');
|
||
const trigger = ref<MessageAutomationTrigger>('invoice_issued');
|
||
const delayLater = ref(false);
|
||
const startDelayValue = ref(1);
|
||
const startDelayUnit = ref<MessageAutomationDelayUnit>('hours');
|
||
const repeatCount = ref(1);
|
||
const intervalValue = ref(2);
|
||
const intervalUnit = ref<MessageAutomationDelayUnit>('days');
|
||
const notifyTarget = ref<MessageAutomationTarget>('person');
|
||
const channel = ref<MessageAutomationChannel>('sms');
|
||
const body = ref('');
|
||
|
||
const bodyEl = ref<HTMLTextAreaElement | null>(null);
|
||
const fieldsOpen = ref(false);
|
||
const emojiOpen = ref(false);
|
||
const fieldsSheet = ref(false);
|
||
const isMobile = ref(typeof window !== 'undefined' && window.matchMedia(MOBILE_MQ).matches);
|
||
const mobileView = ref<'list' | 'wizard'>('list');
|
||
const mobileStep = ref(1);
|
||
const query = ref('');
|
||
const statusFilter = ref<'all' | 'active' | 'inactive'>('all');
|
||
const menuId = ref<string | null>(null);
|
||
const MOBILE_STEPS = [
|
||
{ n: 1, label: 'اطلاعات' },
|
||
{ n: 2, label: 'شرط اجرا' },
|
||
{ n: 3, label: 'پیام' },
|
||
{ n: 4, label: 'تکمیل' },
|
||
] as const;
|
||
|
||
const visibleItems = computed(() => {
|
||
const q = query.value.trim();
|
||
return items.value.filter((row) => {
|
||
if (statusFilter.value === 'active' && !row.isActive) return false;
|
||
if (statusFilter.value === 'inactive' && row.isActive) return false;
|
||
if (!q) return true;
|
||
return matchesSearch(q, [row.name, row.description, row.body]);
|
||
});
|
||
});
|
||
|
||
const delayMax = computed(() => DELAY_MAX[startDelayUnit.value] || 23);
|
||
const intervalMax = computed(() => DELAY_MAX[intervalUnit.value] || 6);
|
||
const bodyCount = computed(() => body.value.length);
|
||
const bodyOver = computed(() => bodyCount.value > MESSAGE_AUTOMATION_SMS_SEGMENT);
|
||
const smsParts = computed(() =>
|
||
bodyCount.value === 0 ? 0 : Math.ceil(bodyCount.value / MESSAGE_AUTOMATION_SMS_SEGMENT),
|
||
);
|
||
const previewBody = computed(() => {
|
||
const sample = {
|
||
'{نام}': 'علی',
|
||
'{شماره}': '۰۹۱۲۱۲۳۴۵۶۷',
|
||
'{تاریخ}': formatJalaliNumeric(new Date()),
|
||
'{عنوان}': name.value.trim() || 'عنوان',
|
||
'{شرکت}': 'شرکت نمونه',
|
||
'{title}': name.value.trim() || 'عنوان',
|
||
};
|
||
let text = body.value.trim();
|
||
for (const [token, value] of Object.entries(sample)) {
|
||
text = text.split(token).join(value);
|
||
}
|
||
return text;
|
||
});
|
||
const previewClock = computed(() => formatClockFa(new Date()));
|
||
|
||
watch(startDelayUnit, () => {
|
||
if (startDelayValue.value > delayMax.value) startDelayValue.value = delayMax.value;
|
||
});
|
||
|
||
watch(intervalUnit, () => {
|
||
if (intervalValue.value > intervalMax.value) intervalValue.value = intervalMax.value;
|
||
});
|
||
|
||
function resetForm() {
|
||
editingId.value = null;
|
||
name.value = '';
|
||
description.value = '';
|
||
trigger.value = 'invoice_issued';
|
||
delayLater.value = false;
|
||
startDelayValue.value = 1;
|
||
startDelayUnit.value = 'hours';
|
||
repeatCount.value = 1;
|
||
intervalValue.value = 2;
|
||
intervalUnit.value = 'days';
|
||
notifyTarget.value = 'person';
|
||
channel.value = 'sms';
|
||
body.value = '';
|
||
fieldsOpen.value = false;
|
||
emojiOpen.value = false;
|
||
fieldsSheet.value = false;
|
||
menuId.value = null;
|
||
ok.value = '';
|
||
}
|
||
|
||
function ruleDate(row: MessageAutomationRuleDto) {
|
||
return formatJalaliNumeric(new Date(row.createdAt));
|
||
}
|
||
|
||
function openMobileCreate() {
|
||
resetForm();
|
||
mobileStep.value = 1;
|
||
mobileView.value = 'wizard';
|
||
}
|
||
|
||
function openMobileEdit(row: MessageAutomationRuleDto) {
|
||
startEdit(row);
|
||
menuId.value = null;
|
||
mobileStep.value = 1;
|
||
mobileView.value = 'wizard';
|
||
}
|
||
|
||
function closeMobileWizard() {
|
||
resetForm();
|
||
mobileStep.value = 1;
|
||
mobileView.value = 'list';
|
||
}
|
||
|
||
function nextMobileStep() {
|
||
if (mobileStep.value === 1 && !name.value.trim()) {
|
||
error.value = 'عنوان قانون را وارد کنید.';
|
||
return;
|
||
}
|
||
if (mobileStep.value === 3 && !body.value.trim()) {
|
||
error.value = 'متن پیام را وارد کنید.';
|
||
return;
|
||
}
|
||
error.value = '';
|
||
if (mobileStep.value < 4) mobileStep.value += 1;
|
||
}
|
||
|
||
function prevMobileStep() {
|
||
error.value = '';
|
||
if (mobileStep.value <= 1) {
|
||
closeMobileWizard();
|
||
return;
|
||
}
|
||
mobileStep.value -= 1;
|
||
}
|
||
|
||
function startEdit(row: MessageAutomationRuleDto) {
|
||
editingId.value = row.id;
|
||
name.value = row.name;
|
||
description.value = row.description ?? '';
|
||
trigger.value = row.trigger;
|
||
delayLater.value = row.startDelayValue > 0;
|
||
startDelayValue.value = Math.max(1, row.startDelayValue || 1);
|
||
startDelayUnit.value =
|
||
row.startDelayUnit === 'minutes' ? 'hours' : row.startDelayUnit;
|
||
repeatCount.value = row.repeatCount;
|
||
intervalValue.value = row.intervalValue;
|
||
intervalUnit.value = row.intervalUnit;
|
||
notifyTarget.value = row.notifyTarget;
|
||
channel.value = row.channel;
|
||
body.value = row.body;
|
||
ok.value = '';
|
||
}
|
||
|
||
function scheduleLabel(row: MessageAutomationRuleDto) {
|
||
const start =
|
||
row.startDelayValue === 0
|
||
? 'بلافاصله'
|
||
: `${toFaDigits(row.startDelayValue)} ${MESSAGE_AUTOMATION_UNIT_LABELS[row.startDelayUnit]} بعد`;
|
||
if (row.repeatCount <= 1) return start;
|
||
return `${start} · ${toFaDigits(row.repeatCount)} بار، هر ${toFaDigits(row.intervalValue)} ${MESSAGE_AUTOMATION_UNIT_LABELS[row.intervalUnit]}`;
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true;
|
||
error.value = '';
|
||
try {
|
||
const { data } = await api.get<MessageAutomationRuleDto[]>('/message-automations');
|
||
items.value = data;
|
||
} catch (e) {
|
||
error.value = axios.isAxiosError(e) && !e.response
|
||
? 'اتصال به API برقرار نشد.'
|
||
: 'بارگذاری قوانین پیام ناموفق بود.';
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function payload(): CreateMessageAutomationRuleRequest {
|
||
const named = name.value.trim();
|
||
return {
|
||
name: named,
|
||
isActive: true,
|
||
trigger: trigger.value,
|
||
startDelayValue: delayLater.value ? Math.max(1, startDelayValue.value || 1) : 0,
|
||
startDelayUnit: delayLater.value ? startDelayUnit.value : 'minutes',
|
||
repeatCount: Math.min(10, Math.max(1, Number(repeatCount.value) || 1)),
|
||
intervalValue: Math.max(1, intervalValue.value || 1),
|
||
intervalUnit: intervalUnit.value,
|
||
notifyTarget: notifyTarget.value,
|
||
channel: channel.value,
|
||
title: named,
|
||
description: description.value.trim() || null,
|
||
body: body.value.trim(),
|
||
};
|
||
}
|
||
|
||
async function save() {
|
||
if (!name.value.trim() || !body.value.trim()) {
|
||
error.value = 'عنوان قانون و متن پیام را وارد کنید.';
|
||
return;
|
||
}
|
||
saving.value = true;
|
||
error.value = '';
|
||
ok.value = '';
|
||
try {
|
||
if (editingId.value) {
|
||
const { isActive: _ignore, ...rest } = payload();
|
||
const bodyDto: UpdateMessageAutomationRuleRequest = rest;
|
||
await api.patch(`/message-automations/${editingId.value}`, bodyDto);
|
||
ok.value = 'قانون بهروز شد.';
|
||
} else {
|
||
await api.post('/message-automations', payload());
|
||
ok.value = 'قانون ساخته شد.';
|
||
}
|
||
resetForm();
|
||
if (isMobile.value) {
|
||
mobileStep.value = 1;
|
||
mobileView.value = 'list';
|
||
}
|
||
await load();
|
||
} catch {
|
||
error.value = 'ذخیره قانون ناموفق بود.';
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
async function toggle(row: MessageAutomationRuleDto) {
|
||
try {
|
||
await api.patch(`/message-automations/${row.id}`, { isActive: !row.isActive });
|
||
await load();
|
||
} catch {
|
||
error.value = 'تغییر وضعیت ناموفق بود.';
|
||
}
|
||
}
|
||
|
||
const pendingDelete = ref<MessageAutomationRuleDto | null>(null);
|
||
|
||
function askRemove(row: MessageAutomationRuleDto) {
|
||
menuId.value = null;
|
||
pendingDelete.value = row;
|
||
}
|
||
|
||
useEscClose(() => fieldsSheet.value, () => { fieldsSheet.value = false; });
|
||
useEscClose(() => pendingDelete.value, () => cancelRemove());
|
||
|
||
function cancelRemove() {
|
||
pendingDelete.value = null;
|
||
}
|
||
|
||
async function confirmRemove() {
|
||
const row = pendingDelete.value;
|
||
if (!row) return;
|
||
pendingDelete.value = null;
|
||
try {
|
||
await api.delete(`/message-automations/${row.id}`);
|
||
if (editingId.value === row.id) resetForm();
|
||
await load();
|
||
} catch {
|
||
error.value = 'حذف قانون ناموفق بود.';
|
||
}
|
||
}
|
||
|
||
function insertAtCursor(text: string) {
|
||
const el = bodyEl.value;
|
||
const start = el?.selectionStart ?? body.value.length;
|
||
const end = el?.selectionEnd ?? start;
|
||
body.value = `${body.value.slice(0, start)}${text}${body.value.slice(end)}`;
|
||
void nextTick(() => {
|
||
if (!el) return;
|
||
const pos = start + text.length;
|
||
el.focus();
|
||
el.setSelectionRange(pos, pos);
|
||
});
|
||
}
|
||
|
||
function insertField(token: string) {
|
||
insertAtCursor(token);
|
||
fieldsOpen.value = false;
|
||
fieldsSheet.value = false;
|
||
}
|
||
|
||
function insertEmoji(mark: string) {
|
||
insertAtCursor(mark);
|
||
emojiOpen.value = false;
|
||
}
|
||
|
||
let mq: MediaQueryList | null = null;
|
||
function syncViewport() {
|
||
isMobile.value = !!mq?.matches;
|
||
}
|
||
|
||
onMounted(() => {
|
||
mq = window.matchMedia(MOBILE_MQ);
|
||
syncViewport();
|
||
mq.addEventListener('change', syncViewport);
|
||
void load();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
mq?.removeEventListener('change', syncViewport);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page">
|
||
<div v-if="isMobile && mobileView === 'list'" class="m-home">
|
||
<header class="m-title">
|
||
<h1>اتوماسیون پیامکها</h1>
|
||
<p>با قوانین هوشمند، در زمان مناسب پیام مناسب را ارسال کنید.</p>
|
||
</header>
|
||
<div class="m-hero">
|
||
<img
|
||
class="hero-art"
|
||
src="/brand/message-automation-hero.png"
|
||
alt=""
|
||
width="1600"
|
||
height="983"
|
||
/>
|
||
<h2>ارتباط هوشمند با مشتریان</h2>
|
||
<p class="sub">برای هر رویداد در سیستم، به صورت خودکار پیامک ارسال کنید.</p>
|
||
<button class="btn primary wide" type="button" @click="openMobileCreate">
|
||
<UiIcon name="plus" :size="18" />
|
||
قانون جدید
|
||
</button>
|
||
</div>
|
||
|
||
<div class="m-tools">
|
||
<label class="m-search">
|
||
<UiIcon name="search" :size="16" />
|
||
<input v-model="query" type="search" placeholder="جستجوی قانون..." />
|
||
<UiIcon name="funnel" :size="16" />
|
||
</label>
|
||
<div class="m-tabs" role="tablist">
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
:class="{ on: statusFilter === 'all' }"
|
||
@click="statusFilter = 'all'"
|
||
>
|
||
همه
|
||
</button>
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
:class="{ on: statusFilter === 'active' }"
|
||
@click="statusFilter = 'active'"
|
||
>
|
||
فعال
|
||
</button>
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
:class="{ on: statusFilter === 'inactive' }"
|
||
@click="statusFilter = 'inactive'"
|
||
>
|
||
غیرفعال
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="error" class="error">{{ error }}</p>
|
||
<p v-if="ok" class="ok">{{ ok }}</p>
|
||
|
||
<div class="m-cards">
|
||
<article
|
||
v-for="row in visibleItems"
|
||
:key="row.id"
|
||
class="m-card"
|
||
:class="{ off: !row.isActive }"
|
||
>
|
||
<header>
|
||
<div class="m-menu">
|
||
<UiDropdownMenu
|
||
:open="menuId === row.id"
|
||
align="end"
|
||
@update:open="(v) => (menuId = v ? row.id : null)"
|
||
>
|
||
<template #trigger="{ toggle }">
|
||
<button
|
||
type="button"
|
||
aria-label="عملیات"
|
||
@click.stop="toggle"
|
||
>
|
||
<UiIcon name="dots-v" :size="18" />
|
||
</button>
|
||
</template>
|
||
<button type="button" @click="openMobileEdit(row)">ویرایش</button>
|
||
<button type="button" class="danger" @click="askRemove(row)">حذف</button>
|
||
</UiDropdownMenu>
|
||
</div>
|
||
<div class="m-copy">
|
||
<strong>{{ row.name }}</strong>
|
||
<p>{{ row.description || scheduleLabel(row) }}</p>
|
||
</div>
|
||
<button
|
||
class="toggle"
|
||
type="button"
|
||
:class="{ on: row.isActive }"
|
||
:aria-pressed="row.isActive"
|
||
:aria-label="row.isActive ? 'فعال' : 'غیرفعال'"
|
||
@click="toggle(row)"
|
||
/>
|
||
</header>
|
||
<footer>
|
||
<span class="m-status" :class="{ on: row.isActive }">
|
||
<i />
|
||
{{ row.isActive ? 'فعال' : 'غیرفعال' }}
|
||
</span>
|
||
<time>{{ ruleDate(row) }}</time>
|
||
</footer>
|
||
</article>
|
||
<p v-if="!loading && !visibleItems.length" class="empty">هنوز قانون پیامی نیست.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form
|
||
v-else-if="isMobile"
|
||
class="m-wiz"
|
||
@submit.prevent="mobileStep === 4 ? save() : nextMobileStep()"
|
||
>
|
||
<header class="m-wiz-head">
|
||
<button class="m-back" type="button" aria-label="بازگشت" @click="prevMobileStep">
|
||
<UiIcon name="chevron" :size="20" />
|
||
</button>
|
||
<div>
|
||
<h1>{{ editingId ? 'ویرایش قانون' : 'قانون جدید' }}</h1>
|
||
<p>ایجاد یک قانون پیامک خودکار</p>
|
||
</div>
|
||
<span class="block-ico" aria-hidden="true">
|
||
<UiIcon name="document" :size="18" />
|
||
</span>
|
||
</header>
|
||
|
||
<ol class="m-steps">
|
||
<li
|
||
v-for="step in MOBILE_STEPS"
|
||
:key="step.n"
|
||
:class="{ on: mobileStep === step.n, done: mobileStep > step.n }"
|
||
>
|
||
<b>{{ toFaDigits(step.n) }}</b>
|
||
<span>{{ step.label }}</span>
|
||
</li>
|
||
</ol>
|
||
|
||
<p v-if="error" class="error">{{ error }}</p>
|
||
<p v-if="ok" class="ok">{{ ok }}</p>
|
||
|
||
<section v-if="mobileStep === 1" class="stack">
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>اطلاعات کلی</h2>
|
||
</div>
|
||
<span class="block-ico" aria-hidden="true">
|
||
<UiIcon name="document" :size="18" />
|
||
</span>
|
||
</header>
|
||
<label class="field">
|
||
<span>عنوان قانون <em>*</em></span>
|
||
<input v-model="name" maxlength="120" placeholder="مثلاً تشکر بعد از فاکتور" />
|
||
</label>
|
||
<label class="field">
|
||
توضیحات
|
||
<textarea
|
||
v-model="description"
|
||
rows="2"
|
||
maxlength="500"
|
||
placeholder="توضیح کوتاهی درباره هدف این قانون بنویسید..."
|
||
/>
|
||
</label>
|
||
</section>
|
||
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>زمان و تعداد</h2>
|
||
<p>چه زمانی و چه تعداد باید پیامک ارسال شود؟</p>
|
||
</div>
|
||
<span class="block-ico clock" aria-hidden="true">
|
||
<UiIcon name="clock" :size="18" />
|
||
</span>
|
||
</header>
|
||
<div class="m-schedule">
|
||
<label class="m-opt" :class="{ on: !delayLater }">
|
||
<input v-model="delayLater" type="radio" :value="false" />
|
||
<i />
|
||
<span>
|
||
<strong>بلافاصله</strong>
|
||
<small>بلافاصله بعد از وقوع رویداد</small>
|
||
</span>
|
||
</label>
|
||
<div class="m-opt-box" :class="{ on: delayLater }">
|
||
<label class="m-opt tight">
|
||
<input v-model="delayLater" type="radio" :value="true" />
|
||
<i />
|
||
<span>
|
||
<strong>با تأخیر زمانی</strong>
|
||
</span>
|
||
</label>
|
||
<div v-if="delayLater" class="m-delay">
|
||
<em>بعد از</em>
|
||
<input
|
||
v-model.number="startDelayValue"
|
||
type="number"
|
||
min="1"
|
||
:max="delayMax"
|
||
aria-label="تأخیر"
|
||
/>
|
||
<select v-model="startDelayUnit" aria-label="واحد تأخیر">
|
||
<option v-for="unit in DELAY_UNITS" :key="unit" :value="unit">
|
||
{{ MESSAGE_AUTOMATION_UNIT_LABELS[unit] }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="m-repeat">
|
||
<div class="m-delay">
|
||
<span class="cell-label">تعداد</span>
|
||
<input
|
||
v-model.number="repeatCount"
|
||
type="number"
|
||
min="1"
|
||
max="10"
|
||
aria-label="تعداد پیام"
|
||
/>
|
||
</div>
|
||
<div class="m-delay" :class="{ off: repeatCount <= 1 }">
|
||
<span class="cell-label">فاصله بین پیامها</span>
|
||
<em>هر</em>
|
||
<input
|
||
v-model.number="intervalValue"
|
||
type="number"
|
||
min="1"
|
||
:max="intervalMax"
|
||
:disabled="repeatCount <= 1"
|
||
aria-label="فاصله تکرار"
|
||
/>
|
||
<select v-model="intervalUnit" :disabled="repeatCount <= 1" aria-label="واحد فاصله">
|
||
<option v-for="unit in DELAY_UNITS" :key="unit" :value="unit">
|
||
{{ MESSAGE_AUTOMATION_UNIT_LABELS[unit] }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>برای چه کسی؟</h2>
|
||
</div>
|
||
<span class="block-ico users" aria-hidden="true">
|
||
<UiIcon name="users" :size="18" />
|
||
</span>
|
||
</header>
|
||
<label class="field">
|
||
برای چه کسی
|
||
<select v-model="notifyTarget">
|
||
<option value="person">مشتری</option>
|
||
<option value="staff">مسئول پرونده</option>
|
||
<option value="both">مشتری و مسئول</option>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
از چه راهی؟
|
||
<select v-model="channel">
|
||
<option value="sms">پیامک</option>
|
||
<option value="app">ناتیف داخل برنامه</option>
|
||
<option value="both">پیامک و ناتیف</option>
|
||
</select>
|
||
</label>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-else-if="mobileStep === 2" class="stack">
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>رویداد/شرط اجرا</h2>
|
||
<p>این قانون در چه شرایطی فعال شود؟</p>
|
||
</div>
|
||
<span class="block-ico funnel" aria-hidden="true">
|
||
<UiIcon name="funnel" :size="18" />
|
||
</span>
|
||
</header>
|
||
<label class="field">
|
||
نوع رویداد
|
||
<select v-model="trigger">
|
||
<option
|
||
v-for="item in MESSAGE_AUTOMATION_TRIGGERS"
|
||
:key="item"
|
||
:value="item"
|
||
>
|
||
{{ MESSAGE_AUTOMATION_TRIGGER_LABELS[item] }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-else-if="mobileStep === 3" class="stack">
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>متن پیام</h2>
|
||
<p>متن پیامک خود را بنویسید. میتوانید از فیلدهای پویا استفاده کنید.</p>
|
||
</div>
|
||
<span class="block-ico chat" aria-hidden="true">
|
||
<UiIcon name="chat" :size="18" />
|
||
</span>
|
||
</header>
|
||
<div class="msg-box">
|
||
<div class="msg-tools">
|
||
<div class="pop-wrap">
|
||
<button class="pill" type="button" @click="fieldsOpen = !fieldsOpen; emojiOpen = false">
|
||
<b>{ }</b>
|
||
فیلدهای پویا
|
||
</button>
|
||
<div v-if="fieldsOpen" class="pop">
|
||
<button
|
||
v-for="item in MESSAGE_AUTOMATION_FIELDS"
|
||
:key="item.token"
|
||
type="button"
|
||
@click="insertField(item.token)"
|
||
>
|
||
<code>{{ item.token }}</code>
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="pop-wrap">
|
||
<button class="icon-pill" type="button" aria-label="ایموجی" @click="emojiOpen = !emojiOpen; fieldsOpen = false">
|
||
😊
|
||
</button>
|
||
<div v-if="emojiOpen" class="pop emojis">
|
||
<button
|
||
v-for="mark in EMOJIS"
|
||
:key="mark"
|
||
type="button"
|
||
@click="insertEmoji(mark)"
|
||
>
|
||
{{ mark }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<textarea
|
||
ref="bodyEl"
|
||
v-model="body"
|
||
maxlength="500"
|
||
rows="6"
|
||
placeholder="سلام {نام} عزیز"
|
||
/>
|
||
<span class="counter" :class="{ over: bodyOver }">
|
||
{{ toFaDigits(bodyCount) }}/{{ toFaDigits(MESSAGE_AUTOMATION_SMS_SEGMENT) }}
|
||
</span>
|
||
</div>
|
||
</section>
|
||
<div class="tip">
|
||
<strong>
|
||
<UiIcon name="lightbulb" :size="16" />
|
||
نکته
|
||
</strong>
|
||
<p>
|
||
میتوانید از فیلدهای پویا مثل
|
||
<bdi class="token">{نام}</bdi>،
|
||
<bdi class="token">{شماره}</bdi>
|
||
و
|
||
<bdi class="token">{تاریخ}</bdi>
|
||
در متن پیام استفاده کنید.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-else class="stack">
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2>پیشنمایش پیام</h2>
|
||
</div>
|
||
<span class="block-ico eye" aria-hidden="true">
|
||
<UiIcon name="eye" :size="18" />
|
||
</span>
|
||
</header>
|
||
<div class="m-preview">
|
||
<div class="m-bubble">
|
||
<p>{{ previewBody || 'متن پیام هنوز وارد نشده است.' }}</p>
|
||
<time>{{ previewClock }}</time>
|
||
</div>
|
||
<div class="m-sms-note">
|
||
<strong>
|
||
<i>i</i>
|
||
تعداد کاراکتر: {{ toFaDigits(bodyCount) }} ({{ toFaDigits(smsParts) }} پیامک)
|
||
</strong>
|
||
<p>ممکن است بسته به نوع سامانه پیامکی تعداد کاراکتر متفاوت باشد.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<footer class="m-wiz-foot">
|
||
<button v-if="mobileStep < 4" class="btn primary" type="submit">
|
||
مرحله بعد
|
||
<span>←</span>
|
||
</button>
|
||
<button v-else class="btn primary" type="submit" :disabled="saving">
|
||
{{ saving ? 'در حال ذخیره…' : editingId ? 'ذخیره' : 'ثبت قانون' }}
|
||
</button>
|
||
<button class="btn ghost" type="button" @click="closeMobileWizard">انصراف</button>
|
||
</footer>
|
||
</form>
|
||
|
||
<header v-show="!isMobile" class="page-head">
|
||
<div class="head-copy">
|
||
<h1>اتوماسیون پیامکها</h1>
|
||
<p class="lead">
|
||
پیامک و ناتیف بعد از رویداد، با تأخیر یا همان لحظه.
|
||
</p>
|
||
</div>
|
||
<div class="head-actions">
|
||
<button class="btn ghost" type="button" :disabled="loading" @click="load">
|
||
بروزرسانی
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<p v-show="!isMobile && error" class="error">{{ error }}</p>
|
||
<p v-show="!isMobile && ok" class="ok">{{ ok }}</p>
|
||
|
||
<form v-show="!isMobile" class="composer" @submit.prevent="save">
|
||
<div class="stack">
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2><b>{{ toFaDigits(1) }}</b> اطلاعات کلی</h2>
|
||
<p>ابتدا یک نام و توضیح برای این قانون مشخص کنید.</p>
|
||
</div>
|
||
<span class="block-ico" aria-hidden="true">
|
||
<UiIcon name="document" :size="18" />
|
||
</span>
|
||
</header>
|
||
<label class="field">
|
||
<span>عنوان قانون <em>*</em></span>
|
||
<input v-model="name" maxlength="120" placeholder="مثلاً تشکر بعد از فاکتور" />
|
||
</label>
|
||
<label class="field">
|
||
توضیحات
|
||
<textarea
|
||
v-model="description"
|
||
rows="2"
|
||
maxlength="500"
|
||
placeholder="توضیح کوتاهی درباره هدف این قانون بنویسید..."
|
||
/>
|
||
</label>
|
||
</section>
|
||
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2><b>{{ toFaDigits(2) }}</b> زمان و تعداد</h2>
|
||
<p>چه زمانی و چه تعداد باید پیامک ارسال شود؟</p>
|
||
</div>
|
||
<span class="block-ico clock" aria-hidden="true">
|
||
<UiIcon name="clock" :size="18" />
|
||
</span>
|
||
</header>
|
||
<div class="schedule">
|
||
<div class="schedule-row">
|
||
<label class="choice" :class="{ on: !delayLater }">
|
||
<input v-model="delayLater" type="radio" :value="false" />
|
||
<i />
|
||
<span>
|
||
<strong>بلافاصله</strong>
|
||
<small>بلافاصله بعد از وقوع رویداد</small>
|
||
</span>
|
||
</label>
|
||
<div class="split flush" :class="{ off: !delayLater }">
|
||
<label class="choice" :class="{ on: delayLater }">
|
||
<input v-model="delayLater" type="radio" :value="true" />
|
||
<i />
|
||
<span>
|
||
<strong>با تأخیر زمانی</strong>
|
||
<small>با فاصله از وقوع رویداد</small>
|
||
</span>
|
||
</label>
|
||
<span class="delay-controls">
|
||
<em>بعد از</em>
|
||
<input
|
||
v-model.number="startDelayValue"
|
||
type="number"
|
||
min="1"
|
||
:max="delayMax"
|
||
:disabled="!delayLater"
|
||
aria-label="تأخیر"
|
||
/>
|
||
<select v-model="startDelayUnit" :disabled="!delayLater" aria-label="واحد تأخیر">
|
||
<option v-for="unit in DELAY_UNITS" :key="unit" :value="unit">
|
||
{{ MESSAGE_AUTOMATION_UNIT_LABELS[unit] }}
|
||
</option>
|
||
</select>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="schedule-row">
|
||
<label class="split">
|
||
<span class="cell-label">تعداد</span>
|
||
<span class="delay-controls">
|
||
<input
|
||
v-model.number="repeatCount"
|
||
type="number"
|
||
min="1"
|
||
max="10"
|
||
aria-label="تعداد پیام"
|
||
/>
|
||
</span>
|
||
</label>
|
||
<label class="split flush" :class="{ off: repeatCount <= 1 }">
|
||
<span class="cell-label">فاصله بین پیامها</span>
|
||
<span class="delay-controls">
|
||
<em>هر</em>
|
||
<input
|
||
v-model.number="intervalValue"
|
||
type="number"
|
||
min="1"
|
||
:max="intervalMax"
|
||
:disabled="repeatCount <= 1"
|
||
aria-label="فاصله تکرار"
|
||
/>
|
||
<select
|
||
v-model="intervalUnit"
|
||
:disabled="repeatCount <= 1"
|
||
aria-label="واحد فاصله"
|
||
>
|
||
<option v-for="unit in DELAY_UNITS" :key="unit" :value="unit">
|
||
{{ MESSAGE_AUTOMATION_UNIT_LABELS[unit] }}
|
||
</option>
|
||
</select>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2><b>{{ toFaDigits(3) }}</b> رویداد/شرط اجرا</h2>
|
||
<p>این قانون در چه شرایطی فعال شود؟</p>
|
||
</div>
|
||
<span class="block-ico funnel" aria-hidden="true">
|
||
<UiIcon name="funnel" :size="18" />
|
||
</span>
|
||
</header>
|
||
<label class="field">
|
||
نوع رویداد
|
||
<select v-model="trigger">
|
||
<option
|
||
v-for="item in MESSAGE_AUTOMATION_TRIGGERS"
|
||
:key="item"
|
||
:value="item"
|
||
>
|
||
{{ MESSAGE_AUTOMATION_TRIGGER_LABELS[item] }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
برای چه کسی
|
||
<select v-model="notifyTarget">
|
||
<option value="person">مشتری</option>
|
||
<option value="staff">مسئول پرونده</option>
|
||
<option value="both">مشتری و مسئول</option>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
از چه راهی
|
||
<select v-model="channel">
|
||
<option value="sms">پیامک</option>
|
||
<option value="app">ناتیف داخل برنامه</option>
|
||
<option value="both">پیامک و ناتیف</option>
|
||
</select>
|
||
</label>
|
||
</section>
|
||
|
||
<section class="block">
|
||
<header class="block-head">
|
||
<div>
|
||
<h2><b>{{ toFaDigits(4) }}</b> متن پیام</h2>
|
||
<p>متن پیامک خود را بنویسید. میتوانید از فیلدهای پویا استفاده کنید.</p>
|
||
</div>
|
||
<span class="block-ico chat" aria-hidden="true">
|
||
<UiIcon name="chat" :size="18" />
|
||
</span>
|
||
</header>
|
||
<div class="msg-box">
|
||
<div class="msg-tools">
|
||
<div class="pop-wrap">
|
||
<button class="pill" type="button" @click="fieldsOpen = !fieldsOpen; emojiOpen = false">
|
||
<b>{ }</b>
|
||
فیلدهای پویا
|
||
</button>
|
||
<div v-if="fieldsOpen" class="pop">
|
||
<button
|
||
v-for="item in MESSAGE_AUTOMATION_FIELDS"
|
||
:key="item.token"
|
||
type="button"
|
||
@click="insertField(item.token)"
|
||
>
|
||
<code>{{ item.token }}</code>
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="pop-wrap">
|
||
<button class="icon-pill" type="button" aria-label="ایموجی" @click="emojiOpen = !emojiOpen; fieldsOpen = false">
|
||
😊
|
||
</button>
|
||
<div v-if="emojiOpen" class="pop emojis">
|
||
<button
|
||
v-for="mark in EMOJIS"
|
||
:key="mark"
|
||
type="button"
|
||
@click="insertEmoji(mark)"
|
||
>
|
||
{{ mark }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<textarea
|
||
ref="bodyEl"
|
||
v-model="body"
|
||
maxlength="500"
|
||
rows="6"
|
||
placeholder="سلام {نام} عزیز"
|
||
/>
|
||
<span class="counter" :class="{ over: bodyOver }">
|
||
{{ toFaDigits(bodyCount) }}/{{ toFaDigits(MESSAGE_AUTOMATION_SMS_SEGMENT) }}
|
||
</span>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" :disabled="saving">
|
||
{{ saving ? 'در حال ذخیره…' : editingId ? 'ذخیره' : 'ساخت' }}
|
||
</button>
|
||
<button v-if="editingId" class="btn ghost" type="button" @click="resetForm">
|
||
انصراف
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="side">
|
||
<aside class="intro">
|
||
<div class="hero">
|
||
<img
|
||
class="hero-art"
|
||
src="/brand/message-automation-hero.png"
|
||
alt=""
|
||
width="1600"
|
||
height="983"
|
||
/>
|
||
<h3>ارتباط هوشمند با مشتریان</h3>
|
||
<p>با اتوماسیون پیامک، در زمان مناسب، پیام مناسب را به افراد مناسب ارسال کنید.</p>
|
||
</div>
|
||
<ul>
|
||
<li>
|
||
<UiIcon name="pulse" :size="20" />
|
||
افزایش فروش
|
||
</li>
|
||
<li>
|
||
<UiIcon name="clock" :size="20" />
|
||
صرفهجویی در زمان
|
||
</li>
|
||
<li>
|
||
<UiIcon name="users" :size="20" />
|
||
بهبود ارتباط با مشتریان
|
||
</li>
|
||
<li>
|
||
<UiIcon name="sliders" :size="20" />
|
||
کاملاً قابل شخصیسازی
|
||
</li>
|
||
</ul>
|
||
</aside>
|
||
<div class="tip">
|
||
<strong>
|
||
<UiIcon name="lightbulb" :size="16" />
|
||
نکته
|
||
</strong>
|
||
<p>
|
||
میتوانید از فیلدهای پویا مثل
|
||
<bdi class="token">{نام}</bdi>،
|
||
<bdi class="token">{شماره}</bdi>
|
||
و
|
||
<bdi class="token">{تاریخ}</bdi>
|
||
در متن پیام استفاده کنید.
|
||
</p>
|
||
<button type="button" @click="fieldsSheet = true">
|
||
مشاهده فهرست فیلدها
|
||
<span>←</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<div v-if="fieldsSheet" class="sheet-layer">
|
||
<div class="dim" aria-hidden="true" />
|
||
<section class="sheet" role="dialog" aria-modal="true" aria-label="فهرست فیلدها">
|
||
<header>
|
||
<h3>فهرست فیلدهای پویا</h3>
|
||
<button type="button" aria-label="بستن" @click="fieldsSheet = false">
|
||
<UiIcon name="x" :size="16" />
|
||
</button>
|
||
</header>
|
||
<button
|
||
v-for="item in MESSAGE_AUTOMATION_FIELDS"
|
||
:key="item.token"
|
||
type="button"
|
||
@click="insertField(item.token)"
|
||
>
|
||
<code>{{ item.token }}</code>
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
</section>
|
||
</div>
|
||
|
||
<div v-show="!isMobile" class="list-wrap">
|
||
<div v-if="items.length" class="tc-row">
|
||
<TableColumnsMenu table-key="message-automations" align="end" />
|
||
</div>
|
||
<table v-if="items.length" v-table-cols="'message-automations'" class="desk-table">
|
||
<thead>
|
||
<tr>
|
||
<th>عنوان</th>
|
||
<th>رویداد</th>
|
||
<th>زمانبندی</th>
|
||
<th>مخاطب</th>
|
||
<th>کانال</th>
|
||
<th>عملیات</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in items" :key="row.id" :class="{ off: !row.isActive }">
|
||
<td>
|
||
<strong>{{ row.name }}</strong>
|
||
<span class="preview">{{ row.description || row.body }}</span>
|
||
</td>
|
||
<td>{{ MESSAGE_AUTOMATION_TRIGGER_LABELS[row.trigger] }}</td>
|
||
<td>{{ scheduleLabel(row) }}</td>
|
||
<td>{{ MESSAGE_AUTOMATION_TARGET_LABELS[row.notifyTarget] }}</td>
|
||
<td>{{ MESSAGE_AUTOMATION_CHANNEL_LABELS[row.channel] }}</td>
|
||
<td class="ops">
|
||
<div class="ops-row">
|
||
<button
|
||
class="icon-btn status"
|
||
type="button"
|
||
:class="{ on: row.isActive }"
|
||
:aria-pressed="row.isActive"
|
||
:aria-label="row.isActive ? 'فعال' : 'غیرفعال'"
|
||
:title="row.isActive ? 'فعال' : 'غیرفعال'"
|
||
@click="toggle(row)"
|
||
>
|
||
<UiIcon :name="row.isActive ? 'check' : 'x-circle'" :size="24" />
|
||
</button>
|
||
<button class="icon-btn edit" type="button" title="ویرایش" aria-label="ویرایش" @click="startEdit(row)">
|
||
<UiIcon name="edit" :size="24" />
|
||
</button>
|
||
<button class="icon-btn danger" type="button" title="حذف" aria-label="حذف" @click="askRemove(row)">
|
||
<UiIcon name="trash" :size="24" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="mobile-cards">
|
||
<article
|
||
v-for="row in items"
|
||
:key="row.id"
|
||
class="card"
|
||
:class="{ off: !row.isActive }"
|
||
>
|
||
<header>
|
||
<div>
|
||
<strong>{{ row.name }}</strong>
|
||
<p class="muted">{{ MESSAGE_AUTOMATION_TRIGGER_LABELS[row.trigger] }}</p>
|
||
</div>
|
||
<button
|
||
class="toggle"
|
||
type="button"
|
||
:class="{ on: row.isActive }"
|
||
:aria-pressed="row.isActive"
|
||
:aria-label="row.isActive ? 'فعال' : 'غیرفعال'"
|
||
@click="toggle(row)"
|
||
/>
|
||
</header>
|
||
<p>{{ scheduleLabel(row) }}</p>
|
||
<p class="muted">
|
||
{{ MESSAGE_AUTOMATION_TARGET_LABELS[row.notifyTarget] }}
|
||
·
|
||
{{ MESSAGE_AUTOMATION_CHANNEL_LABELS[row.channel] }}
|
||
</p>
|
||
<p class="preview">{{ row.description || row.body }}</p>
|
||
<div class="card-actions">
|
||
<button class="btn ghost" type="button" @click="startEdit(row)">ویرایش</button>
|
||
<button class="btn danger" type="button" @click="askRemove(row)">حذف</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<p v-if="!loading && !items.length" class="empty">هنوز قانون پیامی نیست.</p>
|
||
</div>
|
||
|
||
<div v-if="pendingDelete" class="sheet-layer">
|
||
<div class="dim" aria-hidden="true" />
|
||
<section class="confirm-box" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title">
|
||
<h3 id="delete-confirm-title">حذف قانون</h3>
|
||
<p>آیا از حذف مطمئن هستید؟</p>
|
||
<div class="confirm-actions">
|
||
<button class="btn danger" type="button" @click="confirmRemove">حذف</button>
|
||
<button class="btn ghost" type="button" @click="cancelRemove">انصراف</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page {
|
||
display: grid;
|
||
gap: 1rem;
|
||
min-width: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.page-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.head-copy {
|
||
min-width: 0;
|
||
}
|
||
|
||
.head-actions,
|
||
.form-actions,
|
||
.card-actions,
|
||
.ops {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0 0 0.3rem;
|
||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.lead,
|
||
.muted {
|
||
margin: 0;
|
||
color: #64748b;
|
||
}
|
||
|
||
.composer {
|
||
display: grid;
|
||
gap: 1rem;
|
||
align-items: start;
|
||
}
|
||
|
||
.stack {
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.block,
|
||
.intro,
|
||
.list-wrap {
|
||
background: #fff;
|
||
border: 1px solid #e8eef3;
|
||
border-radius: 22px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.block {
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
padding: 1.15rem 1.2rem 1.2rem;
|
||
}
|
||
|
||
.block-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 0.8rem;
|
||
}
|
||
|
||
.block-head h2 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
margin: 0;
|
||
font-size: 1rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.block-head h2 b {
|
||
font-weight: 800;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.block-head p {
|
||
margin: 0.2rem 0 0;
|
||
color: #94a3b8;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.block-ico {
|
||
width: 2.15rem;
|
||
height: 2.15rem;
|
||
border-radius: 999px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: #ecfdf5;
|
||
color: #0f766e;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.block-ico.clock {
|
||
background: #ecfeff;
|
||
color: #0e7490;
|
||
}
|
||
|
||
.block-ico.funnel {
|
||
background: #f5f3ff;
|
||
color: #6d28d9;
|
||
}
|
||
|
||
.block-ico.chat {
|
||
background: #ecfeff;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.block-ico.users {
|
||
background: #ecfdf5;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.block-ico.eye {
|
||
background: #ecfeff;
|
||
color: #0e7490;
|
||
}
|
||
|
||
.field {
|
||
display: grid;
|
||
gap: 0.35rem;
|
||
color: #64748b;
|
||
font-size: 0.86rem;
|
||
}
|
||
|
||
.field em {
|
||
color: #ef4444;
|
||
font-style: normal;
|
||
}
|
||
|
||
input,
|
||
select,
|
||
textarea {
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
min-height: var(--control-h);
|
||
padding: 0 0.85rem;
|
||
font: inherit;
|
||
background: #fff;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
color: #0f172a;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 4.4rem;
|
||
padding: 0.65rem 0.85rem;
|
||
resize: vertical;
|
||
}
|
||
|
||
.choice {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.65rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.choice input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.choice i {
|
||
width: 1.15rem;
|
||
height: 1.15rem;
|
||
margin-top: 0.1rem;
|
||
border: 2px solid #cbd5e1;
|
||
border-radius: 999px;
|
||
background: #fff;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.choice.on i {
|
||
border-color: #14b8a6;
|
||
}
|
||
|
||
.choice.on i::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 3px;
|
||
border-radius: 999px;
|
||
background: #14b8a6;
|
||
}
|
||
|
||
.choice span {
|
||
display: grid;
|
||
gap: 0.1rem;
|
||
}
|
||
|
||
.choice strong {
|
||
color: #0f172a;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
.choice small {
|
||
color: #94a3b8;
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.schedule {
|
||
--choice-indent: calc(1.15rem + 0.65rem);
|
||
display: grid;
|
||
gap: 0.9rem;
|
||
}
|
||
|
||
.schedule-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
|
||
align-items: start;
|
||
gap: 0.75rem 1.2rem;
|
||
width: 100%;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.schedule-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.split {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.65rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.schedule-row > .split:not(.flush) {
|
||
padding-inline-start: var(--choice-indent);
|
||
}
|
||
|
||
.split.flush {
|
||
display: grid;
|
||
grid-template-columns: 9rem 12.5rem;
|
||
justify-content: end;
|
||
column-gap: 1.35rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.split.flush .delay-controls {
|
||
justify-self: end;
|
||
}
|
||
|
||
.split.flush .cell-label {
|
||
justify-self: start;
|
||
margin-inline-start: var(--choice-indent);
|
||
}
|
||
|
||
.schedule-row:first-child .split.flush {
|
||
align-items: start;
|
||
}
|
||
|
||
.split.off .delay-controls {
|
||
opacity: 0.45;
|
||
}
|
||
|
||
.cell-label {
|
||
color: #0f172a;
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.delay-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
color: #64748b;
|
||
font-size: 0.86rem;
|
||
}
|
||
|
||
.delay-controls em {
|
||
font-style: normal;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.delay-controls input,
|
||
.delay-controls select {
|
||
width: auto;
|
||
min-height: 2.15rem;
|
||
}
|
||
|
||
.delay-controls input {
|
||
width: 3.4rem;
|
||
text-align: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.delay-controls select {
|
||
width: 5.8rem;
|
||
}
|
||
|
||
.msg-box {
|
||
position: relative;
|
||
display: grid;
|
||
gap: 0.55rem;
|
||
padding: 0.75rem;
|
||
border: 1px solid #e8eef3;
|
||
border-radius: 16px;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.msg-tools {
|
||
display: flex;
|
||
gap: 0.45rem;
|
||
}
|
||
|
||
.pop-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.pill,
|
||
.icon-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
min-height: 2rem;
|
||
padding: 0 0.7rem;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
color: #334155;
|
||
font: inherit;
|
||
font-size: 0.82rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.pill b {
|
||
color: #0f766e;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.icon-pill {
|
||
width: 2rem;
|
||
padding: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.pop {
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
inset-inline-start: 0;
|
||
z-index: 5;
|
||
min-width: 12rem;
|
||
padding: 0.35rem;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
|
||
display: grid;
|
||
gap: 0.15rem;
|
||
}
|
||
|
||
.pop button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.6rem;
|
||
width: 100%;
|
||
border: 0;
|
||
background: transparent;
|
||
padding: 0.45rem 0.5rem;
|
||
border-radius: 8px;
|
||
font: inherit;
|
||
cursor: pointer;
|
||
color: #334155;
|
||
}
|
||
|
||
.pop button:hover {
|
||
background: #f1f5f9;
|
||
}
|
||
|
||
.pop code,
|
||
.tip code,
|
||
.sheet code {
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.pop.emojis {
|
||
grid-template-columns: repeat(6, 1fr);
|
||
min-width: 0;
|
||
}
|
||
|
||
.pop.emojis button {
|
||
justify-content: center;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.counter {
|
||
justify-self: start;
|
||
color: #94a3b8;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.counter.over {
|
||
color: #b45309;
|
||
}
|
||
|
||
.side {
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.intro {
|
||
display: grid;
|
||
gap: 1.15rem;
|
||
padding: 1.35rem 1.15rem 1.35rem;
|
||
border: 0;
|
||
background: #eef4fa;
|
||
box-shadow: 0 0 0 1px #eef2f6;
|
||
}
|
||
|
||
.hero {
|
||
text-align: start;
|
||
padding-bottom: 1.15rem;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.hero-art {
|
||
display: block;
|
||
width: min(100%, 15.5rem);
|
||
height: auto;
|
||
margin: 0.15rem auto 1.65rem;
|
||
}
|
||
|
||
.hero h3 {
|
||
margin: 0 0 0.45rem;
|
||
color: #0f172a;
|
||
font-size: 1.02rem;
|
||
font-weight: 800;
|
||
text-align: center;
|
||
}
|
||
|
||
.hero p {
|
||
margin: 0;
|
||
color: #94a3b8;
|
||
font-size: 0.8rem;
|
||
line-height: 1.85;
|
||
}
|
||
|
||
.intro ul {
|
||
list-style: none;
|
||
width: 100%;
|
||
margin: 0.2rem 0 0;
|
||
padding: 0;
|
||
display: grid;
|
||
gap: 1.55rem;
|
||
}
|
||
|
||
.intro li {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.6rem;
|
||
color: #334155;
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.intro li :deep(.ico) {
|
||
color: #14b8a6;
|
||
}
|
||
|
||
.tip {
|
||
padding: 1rem 1.05rem 1rem;
|
||
border-radius: 22px;
|
||
background: #ecfdf5;
|
||
color: #475569;
|
||
}
|
||
|
||
.tip strong {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.35rem;
|
||
margin-bottom: 0.45rem;
|
||
color: #0f766e;
|
||
font-size: 1.02rem;
|
||
}
|
||
|
||
.tip p {
|
||
margin: 0;
|
||
font-size: 0.88rem;
|
||
line-height: 1.9;
|
||
text-align: start;
|
||
}
|
||
|
||
.token {
|
||
display: inline-block;
|
||
padding: 0 0.15rem;
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
direction: ltr;
|
||
unicode-bidi: isolate;
|
||
}
|
||
|
||
.tip button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.35rem;
|
||
width: 100%;
|
||
margin-top: 0.75rem;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #0f766e;
|
||
font: inherit;
|
||
font-weight: 700;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sheet-layer {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 90;
|
||
display: grid;
|
||
place-items: center;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.sheet-layer .dim {
|
||
position: absolute;
|
||
inset: 0;
|
||
border: 0;
|
||
background: rgba(15, 23, 42, 0.32);
|
||
}
|
||
|
||
.confirm-box {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: min(22rem, 100%);
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
padding: 1.15rem 1.2rem 1.1rem;
|
||
border-radius: 18px;
|
||
background: #fff;
|
||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16);
|
||
}
|
||
|
||
.confirm-box h3 {
|
||
margin: 0;
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.confirm-box p {
|
||
margin: 0;
|
||
color: #475569;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.confirm-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.sheet {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: min(22rem, 100%);
|
||
display: grid;
|
||
gap: 0.35rem;
|
||
padding: 1rem;
|
||
border-radius: 18px;
|
||
background: #fff;
|
||
}
|
||
|
||
.sheet header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.sheet header h3 {
|
||
margin: 0;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.sheet header button,
|
||
.sheet > button {
|
||
border: 0;
|
||
background: #f8fafc;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
}
|
||
|
||
.sheet header button {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.sheet > button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.65rem 0.7rem;
|
||
}
|
||
|
||
.list-wrap {
|
||
padding: 0.85rem 1.1rem;
|
||
}
|
||
|
||
.empty {
|
||
margin: 0;
|
||
padding: 1.4rem 0.2rem;
|
||
text-align: center;
|
||
color: #64748b;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.35rem;
|
||
min-height: var(--control-h);
|
||
padding: 0 0.95rem;
|
||
border-radius: var(--radius);
|
||
font-weight: 700;
|
||
border: 1px solid transparent;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
}
|
||
|
||
.btn.primary {
|
||
background: #14b8a6;
|
||
color: #fff;
|
||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
||
}
|
||
|
||
.btn.ghost {
|
||
background: #fff;
|
||
color: #0f766e;
|
||
border-color: #99f6e4;
|
||
}
|
||
|
||
.btn.danger {
|
||
background: #fff;
|
||
color: #b91c1c;
|
||
border-color: #fecaca;
|
||
}
|
||
|
||
.desk-table {
|
||
display: none;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.desk-table th,
|
||
.desk-table td {
|
||
padding: 0.85rem 0.9rem;
|
||
text-align: start;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.desk-table th {
|
||
color: #94a3b8;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.desk-table td {
|
||
font-size: 0.88rem;
|
||
color: #334155;
|
||
}
|
||
|
||
.desk-table td strong {
|
||
display: block;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.desk-table tr.off {
|
||
opacity: 0.62;
|
||
}
|
||
|
||
.desk-table .ops {
|
||
display: table-cell;
|
||
width: 1%;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.ops-row {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.icon-btn {
|
||
display: inline-grid;
|
||
place-items: center;
|
||
flex: 0 0 2.75rem;
|
||
width: 2.75rem;
|
||
height: 2.75rem;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.icon-btn:hover {
|
||
background: transparent;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.icon-btn.edit {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.icon-btn.edit:hover {
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.icon-btn.danger {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.icon-btn.danger:hover {
|
||
color: #b91c1c;
|
||
background: transparent;
|
||
}
|
||
|
||
.icon-btn.status {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.icon-btn.status.on {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.mobile-cards {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.card {
|
||
display: grid;
|
||
gap: 0.4rem;
|
||
padding: 0.95rem 1rem;
|
||
border: 1px solid #eef2f7;
|
||
border-radius: 16px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.card.off {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.card header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.card p {
|
||
margin: 0;
|
||
}
|
||
|
||
.card .preview {
|
||
color: #334155;
|
||
font-size: 0.88rem;
|
||
}
|
||
|
||
.toggle {
|
||
position: relative;
|
||
width: 42px;
|
||
height: 24px;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: #cbd5e1;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.toggle::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 2px;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.16);
|
||
transition: left 0.16s ease;
|
||
}
|
||
|
||
.toggle.on {
|
||
background: #14b8a6;
|
||
}
|
||
|
||
.toggle.on::after {
|
||
left: 20px;
|
||
}
|
||
|
||
.error {
|
||
margin: 0;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.ok {
|
||
margin: 0;
|
||
color: #0f766e;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.desk {
|
||
display: grid;
|
||
gap: 1rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.btn.wide {
|
||
width: 100%;
|
||
}
|
||
|
||
.m-home,
|
||
.m-wiz {
|
||
display: grid;
|
||
gap: 1rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.m-title {
|
||
text-align: start;
|
||
}
|
||
|
||
.m-title h1,
|
||
.m-wiz-head h1 {
|
||
margin: 0 0 0.35rem;
|
||
font-size: 1.45rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.m-title p,
|
||
.m-hero p,
|
||
.m-wiz-head p {
|
||
margin: 0;
|
||
color: #94a3b8;
|
||
font-size: 0.86rem;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.m-hero {
|
||
text-align: center;
|
||
padding: 0.15rem 0 0.1rem;
|
||
}
|
||
|
||
.m-hero .hero-art {
|
||
width: min(100%, 18rem);
|
||
margin: 0.15rem auto 1rem;
|
||
}
|
||
|
||
.m-hero h2 {
|
||
margin: 0 0 0.35rem;
|
||
font-size: 1.08rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.m-hero .sub {
|
||
margin-bottom: 1.05rem;
|
||
}
|
||
|
||
.m-hero .btn {
|
||
background: #0f766e;
|
||
box-shadow: 0 10px 22px rgba(15, 118, 110, 0.22);
|
||
}
|
||
|
||
.m-tools {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.m-search {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
min-height: var(--control-h);
|
||
padding: 0 0.85rem;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 999px;
|
||
background: #fff;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.m-search input {
|
||
border: 0;
|
||
min-height: 0;
|
||
padding: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.m-tabs {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 0.35rem;
|
||
padding: 0.25rem;
|
||
border-radius: 999px;
|
||
background: #eef2f6;
|
||
}
|
||
|
||
.m-tabs button {
|
||
min-height: 2.2rem;
|
||
border: 0;
|
||
border-radius: 999px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
font: inherit;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.m-tabs button.on {
|
||
background: #fff;
|
||
color: #0f172a;
|
||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.m-cards {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.m-card {
|
||
padding: 0.95rem 1rem 0.85rem;
|
||
border-radius: 20px;
|
||
background: #fff;
|
||
box-shadow: 0 0 0 1px #eef2f7;
|
||
}
|
||
|
||
.m-card.off {
|
||
opacity: 0.78;
|
||
}
|
||
|
||
.m-card header {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
align-items: start;
|
||
gap: 0.65rem;
|
||
}
|
||
|
||
.m-copy strong {
|
||
display: block;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.m-copy p {
|
||
margin: 0.25rem 0 0;
|
||
color: #94a3b8;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.m-menu {
|
||
position: relative;
|
||
}
|
||
|
||
.m-menu :deep(button),
|
||
.m-back {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border: 0;
|
||
background: transparent;
|
||
color: #64748b;
|
||
display: grid;
|
||
place-items: center;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
}
|
||
|
||
.m-card footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 0.85rem;
|
||
color: #94a3b8;
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.m-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.m-status i {
|
||
width: 0.5rem;
|
||
height: 0.5rem;
|
||
border-radius: 999px;
|
||
background: #cbd5e1;
|
||
}
|
||
|
||
.m-status.on {
|
||
color: #0f766e;
|
||
}
|
||
|
||
.m-status.on i {
|
||
background: #14b8a6;
|
||
}
|
||
|
||
.m-wiz-head {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 0.65rem;
|
||
}
|
||
|
||
.m-wiz-head h1 {
|
||
font-size: 1.15rem;
|
||
}
|
||
|
||
.m-wiz-head > div {
|
||
text-align: start;
|
||
}
|
||
|
||
.m-back {
|
||
transform: scaleX(-1);
|
||
}
|
||
|
||
.m-schedule {
|
||
display: grid;
|
||
gap: 0.7rem;
|
||
}
|
||
|
||
.m-opt {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.65rem;
|
||
padding: 0.85rem 0.9rem;
|
||
border: 1px solid #e8eef3;
|
||
border-radius: 16px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.m-opt.tight {
|
||
align-items: center;
|
||
border: 0;
|
||
padding: 0.85rem 0.9rem 0.55rem;
|
||
}
|
||
|
||
.m-opt input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.m-opt i {
|
||
width: 1.15rem;
|
||
height: 1.15rem;
|
||
margin-top: 0.15rem;
|
||
border: 2px solid #cbd5e1;
|
||
border-radius: 999px;
|
||
background: #fff;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.m-opt.tight i {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.m-opt.on i,
|
||
.m-opt-box.on .m-opt i {
|
||
border-color: #14b8a6;
|
||
}
|
||
|
||
.m-opt.on i::after,
|
||
.m-opt-box.on .m-opt i::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 3px;
|
||
border-radius: 999px;
|
||
background: #14b8a6;
|
||
}
|
||
|
||
.m-opt span {
|
||
display: grid;
|
||
gap: 0.12rem;
|
||
}
|
||
|
||
.m-opt strong {
|
||
color: #0f172a;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
.m-opt small {
|
||
color: #94a3b8;
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.m-opt.on,
|
||
.m-opt-box.on {
|
||
border-color: #99f6e4;
|
||
background: #f0fdfa;
|
||
}
|
||
|
||
.m-opt-box {
|
||
display: grid;
|
||
border: 1px solid #e8eef3;
|
||
border-radius: 16px;
|
||
background: #fff;
|
||
}
|
||
|
||
.m-opt-box .m-delay {
|
||
padding: 0 0.9rem 0.85rem;
|
||
}
|
||
|
||
.m-repeat {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
margin-top: 0.25rem;
|
||
padding-top: 0.85rem;
|
||
border-top: 1px solid #eef2f6;
|
||
}
|
||
|
||
.m-delay {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.45rem;
|
||
color: #64748b;
|
||
font-size: 0.86rem;
|
||
}
|
||
|
||
.m-delay.off {
|
||
opacity: 0.45;
|
||
}
|
||
|
||
.m-delay em {
|
||
font-style: normal;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.m-delay input,
|
||
.m-delay select {
|
||
width: auto;
|
||
min-height: 2.15rem;
|
||
}
|
||
|
||
.m-delay input[type='number'] {
|
||
width: 3.4rem;
|
||
text-align: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.m-delay select {
|
||
width: 5.8rem;
|
||
}
|
||
|
||
.m-steps {
|
||
list-style: none;
|
||
margin: 0.2rem 0 0.3rem;
|
||
padding: 0.4rem 0 0;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
position: relative;
|
||
}
|
||
|
||
.m-steps::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 1.25rem;
|
||
inset-inline: 12%;
|
||
height: 2px;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.m-steps li {
|
||
display: grid;
|
||
justify-items: center;
|
||
gap: 0.35rem;
|
||
position: relative;
|
||
z-index: 1;
|
||
color: #94a3b8;
|
||
font-size: 0.72rem;
|
||
}
|
||
|
||
.m-steps li b {
|
||
width: 1.85rem;
|
||
height: 1.85rem;
|
||
border-radius: 999px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: #e2e8f0;
|
||
color: #64748b;
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.m-steps li.on {
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.m-steps li.on b,
|
||
.m-steps li.done b {
|
||
background: #14b8a6;
|
||
color: #fff;
|
||
}
|
||
|
||
.m-preview {
|
||
display: grid;
|
||
gap: 0.85rem;
|
||
}
|
||
|
||
.m-bubble {
|
||
padding: 0.95rem 1rem 0.7rem;
|
||
border-radius: 18px;
|
||
background: #e8f7f8;
|
||
color: #334155;
|
||
}
|
||
|
||
.m-bubble p {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
line-height: 1.9;
|
||
text-align: start;
|
||
}
|
||
|
||
.m-bubble time {
|
||
display: block;
|
||
margin-top: 0.7rem;
|
||
color: #94a3b8;
|
||
font-size: 0.75rem;
|
||
text-align: end;
|
||
}
|
||
|
||
.m-sms-note {
|
||
padding: 0.85rem 0.95rem;
|
||
border-radius: 16px;
|
||
background: #eef8fb;
|
||
color: #64748b;
|
||
}
|
||
|
||
.m-sms-note strong {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.4rem;
|
||
margin-bottom: 0.35rem;
|
||
color: #0f172a;
|
||
font-size: 0.86rem;
|
||
}
|
||
|
||
.m-sms-note strong i {
|
||
width: 1.05rem;
|
||
height: 1.05rem;
|
||
border-radius: 999px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: #bae6fd;
|
||
color: #0369a1;
|
||
font-size: 0.7rem;
|
||
font-style: normal;
|
||
font-weight: 800;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.m-sms-note p {
|
||
margin: 0;
|
||
font-size: 0.8rem;
|
||
line-height: 1.75;
|
||
text-align: start;
|
||
}
|
||
|
||
.m-wiz-foot {
|
||
position: sticky;
|
||
bottom: var(--mobile-tabbar-h);
|
||
display: grid;
|
||
grid-template-columns: 1.25fr 1fr;
|
||
gap: 0.65rem;
|
||
padding: 0.85rem 0 0.35rem;
|
||
background: linear-gradient(180deg, transparent, #fff 18%);
|
||
}
|
||
|
||
.m-wiz-foot .btn.ghost {
|
||
color: #334155;
|
||
border-color: #e2e8f0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
@media (min-width: 980px) {
|
||
.composer {
|
||
grid-template-columns: minmax(0, 1fr) 18rem;
|
||
}
|
||
|
||
.desk-table {
|
||
display: table;
|
||
}
|
||
|
||
.mobile-cards {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|