feat(settings): allow adding/removing dynamic variables in SMS templates

This commit is contained in:
2026-08-15 21:58:19 +03:30
parent dc867883f2
commit 28b75664ab
+143 -42
View File
@@ -1,6 +1,6 @@
<!-- /src/views/settings/SettingsView.vue -->
<template>
<div class="settings-view w-full max-w-3xl mx-auto">
<div class="settings-view w-full max-w-4xl mx-auto">
<PageHeader
title="تنظیمات سیستم"
subtitle="قالب‌های پیامک، راه‌اندازی پایگاه داده و وارد کردن داده‌ها"
@@ -14,7 +14,7 @@
<div class="flex-grow-1">
<h2 class="text-lg font-bold text-color m-0 mb-1">قالبهای پیامک</h2>
<p class="text-sm text-muted m-0 line-height-3">
شناسه قالب و نام متغیرهای sms.ir را وارد کنید. نام متغیر باید دقیقاً با قالب پنل یکی باشد. اگر قالبی در پایگاه داده نباشد، با ذخیره ایجاد میشود.
شناسه قالب و تعداد و نام متغیرهای sms.ir را بر اساس قالب تعریف شده در پنل پیامک تنظیم کنید. میتوانید هر تعداد متغیر که میخواهید تعریف یا حذف نمایید.
</p>
</div>
</div>
@@ -28,50 +28,107 @@
<div
v-for="template in smsTemplates"
:key="template.key"
class="p-3 border-round surface-ground border-1 border-color flex flex-column gap-3"
class="p-4 border-round-lg surface-ground border-1 border-color flex flex-column gap-3"
>
<template v-if="templateForm[template.key]">
<label class="font-semibold text-sm text-color" :for="`sms-template-${template.key}`">
{{ template.label }}
</label>
<InputText
:id="`sms-template-${template.key}`"
v-model.trim="templateForm[template.key].templateId"
class="w-full text-sm"
dir="ltr"
inputmode="numeric"
placeholder="شناسه قالب sms.ir"
/>
<div class="flex flex-column gap-2">
<span class="text-xs text-muted">نام متغیرها در قالب sms.ir</span>
<div
v-for="variable in template.variables"
:key="variable.slot"
class="flex flex-column sm:flex-row sm:align-items-center gap-2"
>
<div class="flex flex-column sm:flex-row sm:align-items-center justify-content-between gap-2 border-bottom-1 border-color pb-2">
<div class="flex align-items-center gap-2">
<i class="pi pi-bookmark text-primary"></i>
<label class="font-bold text-base text-color" :for="`sms-template-${template.key}`">
{{ template.label }}
</label>
</div>
<span class="text-xs text-muted font-mono">{{ template.key }}</span>
</div>
<div class="flex flex-column sm:flex-row sm:align-items-center gap-2">
<label
class="text-sm text-color sm:w-10rem flex-shrink-0"
:for="`sms-var-${template.key}-${variable.slot}`"
class="text-sm font-semibold text-color sm:w-10rem flex-shrink-0"
:for="`sms-template-${template.key}`"
>
{{ variable.label }}
شناسه قالب sms.ir:
</label>
<div class="flex-grow-1 flex align-items-center gap-2">
<div class="flex-grow-1">
<InputText
:id="`sms-var-${template.key}-${variable.slot}`"
v-model.trim="templateForm[template.key].variables[variable.slot]"
:id="`sms-template-${template.key}`"
v-model.trim="templateForm[template.key].templateId"
class="w-full text-sm"
dir="ltr"
:placeholder="variable.defaultName"
autocomplete="off"
/>
<Tag
:value="`#${templateForm[template.key].variables[variable.slot] || variable.defaultName}#`"
severity="secondary"
class="text-xs flex-shrink-0"
inputmode="numeric"
placeholder="مثلاً: 123456"
/>
</div>
</div>
</div>
<div class="flex flex-column gap-3 pt-2">
<div class="flex align-items-center justify-content-between">
<div>
<span class="text-sm font-semibold text-color">متغیرهای ارسالی در قالب</span>
<span class="text-xs text-muted block mt-1">متغیرهایی که در پنل sms.ir در متن قالب قرار دادهاید</span>
</div>
<Button
label="افزودن متغیر"
icon="pi pi-plus"
size="small"
outlined
class="text-xs font-semibold"
@click="addVariable(template.key)"
/>
</div>
<div
v-if="!templateForm[template.key].variables || templateForm[template.key].variables.length === 0"
class="p-3 text-center border-1 border-dashed border-round surface-card text-muted text-xs line-height-3"
>
هیچ متغیری برای این قالب تعریف نشده است. پیامک بدون متغیر ارسال خواهد شد.
</div>
<div v-else class="flex flex-column gap-2">
<div
v-for="(variable, vIdx) in templateForm[template.key].variables"
:key="vIdx"
class="flex flex-column sm:flex-row sm:align-items-center gap-2 p-2 border-round surface-card border-1 border-color"
>
<div class="flex align-items-center gap-2 sm:w-14rem flex-shrink-0">
<span class="text-xs text-muted font-bold ml-1">{{ vIdx + 1 }}.</span>
<Select
v-model="variable.slot"
:options="template.availableSlots || []"
optionLabel="label"
optionValue="slot"
placeholder="انتخاب مقدار داده"
class="w-full text-sm"
@change="onSlotChange(template.key, vIdx)"
/>
</div>
<div class="flex-grow-1 flex align-items-center gap-2">
<InputText
v-model.trim="variable.name"
class="w-full text-sm"
dir="ltr"
:placeholder="getSlotDefaultName(template, variable.slot) || 'نام متغیر در sms.ir'"
autocomplete="off"
/>
<Tag
:value="`#${variable.name || getSlotDefaultName(template, variable.slot) || '...'}#`"
severity="secondary"
class="text-xs flex-shrink-0 font-mono"
/>
<Button
icon="pi pi-trash"
severity="danger"
text
rounded
size="small"
class="p-button-sm flex-shrink-0"
title="حذف متغیر"
@click="removeVariable(template.key, vIdx)"
/>
</div>
</div>
</div>
</div>
</template>
</div>
@@ -195,6 +252,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
import { useConfirm } from 'primevue/useconfirm';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import Tag from 'primevue/tag';
import PageHeader from '@/components/common/PageHeader.vue';
import { seedApi } from '@/api/seedApi';
@@ -227,6 +285,44 @@ const lockedAtLabel = computed(() => {
return `قفل شده در ${date.toLocaleString('fa-IR')}`;
});
const getSlotDefaultName = (template, slotKey) => {
const slot = (template?.availableSlots || []).find((s) => s.slot === slotKey);
return slot?.defaultName || '';
};
const onSlotChange = (templateKey, vIdx) => {
const form = templateForm[templateKey];
if (!form || !form.variables[vIdx]) return;
const template = smsTemplates.value.find((t) => t.key === templateKey);
const currentVar = form.variables[vIdx];
if (!currentVar.name) {
currentVar.name = getSlotDefaultName(template, currentVar.slot);
}
};
const addVariable = (templateKey) => {
const form = templateForm[templateKey];
if (!form) return;
const template = smsTemplates.value.find((t) => t.key === templateKey);
const availableSlots = template?.availableSlots || [];
const existingSlots = new Set((form.variables || []).map((v) => v.slot));
const unusedSlot = availableSlots.find((s) => !existingSlots.has(s.slot));
const selectedSlot = unusedSlot || availableSlots[0] || { slot: 'param', defaultName: 'param' };
form.variables.push({
slot: selectedSlot.slot,
name: selectedSlot.defaultName || ''
});
};
const removeVariable = (templateKey, index) => {
const form = templateForm[templateKey];
if (form && form.variables) {
form.variables.splice(index, 1);
}
};
const applyStatus = (payload) => {
const status = payload?.data || payload || {};
isLocked.value = Boolean(status.locked);
@@ -313,12 +409,12 @@ const confirmImport = () => {
const emptyFormEntry = (template) => ({
templateId: template?.templateId || '',
variables: Object.fromEntries(
(template?.variables || []).map((variable) => [
variable.slot,
variable.name || variable.defaultName || ''
])
)
variables: Array.isArray(template?.variables)
? template.variables.map((variable) => ({
slot: variable.slot,
name: variable.name || ''
}))
: []
});
const applySmsTemplates = (payload) => {
@@ -353,7 +449,12 @@ const saveSmsTemplates = async () => {
const entry = templateForm[template.key] || emptyFormEntry(template);
smsTemplatesPayload[template.key] = {
templateId: entry.templateId || '',
variables: { ...(entry.variables || {}) }
variables: (entry.variables || [])
.filter((v) => v.slot)
.map((v) => ({
slot: v.slot,
name: v.name || getSlotDefaultName(template, v.slot) || v.slot
}))
};
});
const response = await settingsApi.save({ smsTemplates: smsTemplatesPayload });