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>
|