Files
boursecu_admin/src/pages/menuPages/configuration/TemplateTextField.vue
T

333 lines
7.6 KiB
Vue
Raw Normal View History

2026-07-13 19:03:03 +03:30
<template>
<div class="tpl-field">
<div class="tpl-field__grid">
<!-- ویرایشگر قالب -->
<div class="tpl-field__editor">
<div class="tpl-field__section-title">
<BaseIcon icon="vuesax-linear:edit-2" size="15" />
<span>{{ config.editorTitle }}</span>
</div>
<textarea
ref="textareaRef"
class="tpl-field__textarea"
:value="modelValue"
:disabled="disabled"
:rows="config.rows"
spellcheck="false"
:placeholder="config.placeholder"
@input="onInput"
@blur="emit('blur')"
/>
<div class="tpl-field__hint">
<BaseIcon icon="vuesax-linear:info-circle" size="13" />
<span>
{{ config.hint }} متغیرها با <code>%</code> یا <code>٪</code> نوشته می‌شوند.
</span>
</div>
<!-- پالت متغیرها -->
<div class="tpl-field__vars">
<div v-for="group in config.variableGroups" :key="group.title" class="tpl-vargroup">
<div class="tpl-vargroup__head">
<span class="tpl-vargroup__dot" :style="{ background: group.color }" />
<span class="tpl-vargroup__title">{{ group.title }}</span>
</div>
<div class="tpl-vargroup__chips">
<button
v-for="v in group.items"
:key="v.token"
type="button"
class="tpl-chip"
:class="{ 'tpl-chip--disabled': disabled }"
:disabled="disabled"
:title="v.help"
@click="insertToken(v.token)"
>
<span class="tpl-chip__token">%{{ v.token }}</span>
<span class="tpl-chip__label">{{ v.label }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- پیش‌نمایش -->
<div class="tpl-field__preview">
<div class="tpl-field__section-title">
<BaseIcon icon="vuesax-linear:eye" size="15" />
<span>{{ config.previewTitle }}</span>
<span class="tpl-field__preview-tag">نمونه</span>
</div>
<div class="tpl-preview">
<div class="tpl-preview__bubble" dir="auto">{{ previewText }}</div>
</div>
<div class="tpl-field__legend">
{{ config.legend }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import BaseIcon from '@core/components/BaseIcon/src/Icon.vue'
import { getTemplateFieldConfig, TEMPLATE_FIELD_CONFIGS, type TemplateFieldConfig } from './templateFields'
const props = withDefaults(
defineProps<{
modelValue: string
settingKey: string
disabled?: boolean
}>(),
{ disabled: false },
)
const emit = defineEmits<{
'update:value': [value: string]
blur: []
}>()
const textareaRef = ref<HTMLTextAreaElement>()
const config = computed<TemplateFieldConfig>(
() => getTemplateFieldConfig(props.settingKey) ?? TEMPLATE_FIELD_CONFIGS.price_channel_message_text,
)
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const previewText = computed(() => {
const cfg = config.value
let out = (props.modelValue ?? '').trim() || cfg.defaultTemplate
const keys = Object.keys(cfg.sampleValues).sort((a, b) => b.length - a.length)
for (const key of keys) {
const re = new RegExp('[%٪]\\s*' + escapeRegExp(key), 'gu')
out = out.replace(re, cfg.sampleValues[key])
}
return out.replace(/\n{3,}/g, '\n\n').trim()
})
function onInput(event: Event) {
emit('update:value', (event.target as HTMLTextAreaElement).value)
}
function insertToken(token: string) {
if (props.disabled) return
const insert = `%${token}`
const current = props.modelValue ?? ''
const el = textareaRef.value
if (!el) {
emit('update:value', current ? `${current}${insert}` : insert)
return
}
const start = el.selectionStart ?? current.length
const end = el.selectionEnd ?? current.length
const next = current.slice(0, start) + insert + current.slice(end)
emit('update:value', next)
nextTick(() => {
el.focus()
const pos = start + insert.length
el.setSelectionRange(pos, pos)
})
}
</script>
<style lang="less" scoped>
.tpl-field {
width: 100%;
}
.tpl-field__grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
@media (min-width: 992px) {
grid-template-columns: 1.15fr 0.85fr;
}
}
.tpl-field__section-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--admin-text);
margin-bottom: 8px;
:deep(svg) {
color: #d4af37;
}
}
.tpl-field__preview-tag {
margin-inline-start: auto;
font-size: 10px;
font-weight: 500;
color: #b45309;
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 6px;
padding: 1px 8px;
}
.tpl-field__textarea {
width: 100%;
min-height: 160px;
resize: vertical;
padding: 12px 14px;
border: 1px solid var(--admin-border);
border-radius: 10px;
background: var(--admin-bg-elevated);
color: var(--admin-text);
font-size: 13px;
line-height: 2;
font-family: 'Vazirmatn', 'IRANSans', ui-monospace, monospace;
direction: rtl;
transition: border-color 0.2s, box-shadow 0.2s;
&:focus {
outline: none;
border-color: #d4af37;
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.15);
}
&:disabled {
background: var(--admin-bg-muted);
cursor: not-allowed;
opacity: 0.7;
}
}
.tpl-field__hint {
display: flex;
align-items: flex-start;
gap: 6px;
margin-top: 8px;
font-size: 11px;
line-height: 1.7;
color: var(--admin-text-secondary);
:deep(svg) {
flex-shrink: 0;
margin-top: 2px;
color: #3b82f6;
}
code {
background: var(--admin-bg-muted);
border-radius: 4px;
padding: 0 4px;
}
}
.tpl-field__vars {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
.tpl-vargroup__head {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.tpl-vargroup__dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.tpl-vargroup__title {
font-size: 12px;
font-weight: 600;
color: var(--admin-text-secondary);
}
.tpl-vargroup__chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tpl-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border: 1px solid var(--admin-border);
border-radius: 8px;
background: var(--admin-bg-elevated);
cursor: pointer;
transition: all 0.15s ease;
font-family: inherit;
&:hover:not(.tpl-chip--disabled) {
border-color: #d4af37;
background: #fffaf0;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(212, 175, 55, 0.15);
}
&--disabled {
cursor: not-allowed;
opacity: 0.5;
}
&__token {
font-size: 12px;
font-weight: 700;
color: #b45309;
direction: ltr;
}
&__label {
font-size: 11px;
color: var(--admin-text-secondary);
}
}
.tpl-preview {
background: linear-gradient(160deg, #e8f5e9 0%, #eef7ff 100%);
border: 1px solid var(--admin-border);
border-radius: 12px;
padding: 14px;
min-height: 160px;
}
.tpl-preview__bubble {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px 12px 12px 4px;
padding: 12px 14px;
font-size: 13px;
line-height: 2;
color: #111827;
white-space: pre-wrap;
word-break: break-word;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.tpl-field__legend {
margin-top: 8px;
font-size: 11px;
line-height: 1.7;
color: var(--admin-text-secondary);
}
</style>