Files
boursecu_admin/src/pages/menuPages/configuration/TemplateTextField.vue
T
2026-07-13 19:03:03 +03:30

333 lines
7.6 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>