333 lines
7.6 KiB
Vue
333 lines
7.6 KiB
Vue
<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>
|