feat: add html editor for telegram messages
This commit is contained in:
133
frontend/admin/src/components/RichTextEditor.vue
Normal file
133
frontend/admin/src/components/RichTextEditor.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="tw:space-y-2">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 240,
|
||||
},
|
||||
});
|
||||
|
||||
const model = defineModel({
|
||||
type: String,
|
||||
default: "",
|
||||
});
|
||||
|
||||
const textareaRef = ref(null);
|
||||
const summernoteInstance = ref(null);
|
||||
|
||||
const getJQuery = () => window.$ || window.jQuery;
|
||||
|
||||
const normalizeTelegramHtml = (html = "") => {
|
||||
const withoutEmptyParagraphs = html.replace(/<p><br><\/p>/gi, "<br>");
|
||||
const withoutParagraphs = withoutEmptyParagraphs
|
||||
.replace(/<p>/gi, "")
|
||||
.replace(/<\/p>/gi, "<br>");
|
||||
|
||||
return withoutParagraphs.replace(/(?:<br>\s*)+$/i, "").trim();
|
||||
};
|
||||
|
||||
const makeSpoilerButton = ($) => (context) => {
|
||||
const ui = $.summernote.ui;
|
||||
return ui
|
||||
.button({
|
||||
contents: '<i class="fa fa-eye-slash"></i>',
|
||||
tooltip: "Спойлер (Telegram)",
|
||||
click() {
|
||||
const selectedText = context.invoke("editor.getSelectedText") || "";
|
||||
const content = selectedText || "spoiler";
|
||||
context.invoke(
|
||||
"editor.pasteHTML",
|
||||
`<span class="tg-spoiler">${content}</span>`
|
||||
);
|
||||
},
|
||||
})
|
||||
.render();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const $ = getJQuery();
|
||||
if (!$ || !textareaRef.value) {
|
||||
console.warn("[RichTextEditor] jQuery или textarea недоступны");
|
||||
return;
|
||||
}
|
||||
|
||||
const $el = $(textareaRef.value);
|
||||
|
||||
$el.summernote({
|
||||
height: props.height,
|
||||
placeholder: props.placeholder,
|
||||
shortcuts: false,
|
||||
dialogsInBody: true,
|
||||
disableResizeEditor: true,
|
||||
buttons: {
|
||||
spoiler: makeSpoilerButton($),
|
||||
},
|
||||
toolbar: [
|
||||
["font", ["bold", "underline", "italic", "strikethrough", "clear"]],
|
||||
["para", ["ul", "ol", "paragraph"]],
|
||||
["insert", ["link", "spoiler"]],
|
||||
["view", ["fullscreen", "codeview", "help"]],
|
||||
],
|
||||
callbacks: {
|
||||
onChange(contents) {
|
||||
const normalized = normalizeTelegramHtml(contents ?? "");
|
||||
if (normalized !== contents) {
|
||||
$el.summernote("code", normalized);
|
||||
return;
|
||||
}
|
||||
model.value = normalized;
|
||||
},
|
||||
onKeydown(e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
$el.summernote("pasteHTML", "<br>");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (model.value) {
|
||||
$el.summernote("code", normalizeTelegramHtml(model.value));
|
||||
}
|
||||
|
||||
summernoteInstance.value = $el;
|
||||
});
|
||||
|
||||
watch(
|
||||
model,
|
||||
(value) => {
|
||||
const instance = summernoteInstance.value;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeTelegramHtml(value || "");
|
||||
const current = normalizeTelegramHtml(instance.summernote("code"));
|
||||
if (current !== normalized) {
|
||||
instance.summernote("code", normalized);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (summernoteInstance.value) {
|
||||
summernoteInstance.value.summernote("destroy");
|
||||
summernoteInstance.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<SettingsItem :label="label">
|
||||
<template #default>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<textarea
|
||||
<Codemirror
|
||||
v-model="model"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
:extensions="extensions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -37,7 +36,12 @@
|
||||
|
||||
<div class="collapse" :id="collapseId" style="margin-top: 15px">
|
||||
<div class="well">
|
||||
<p>Вы можете использовать переменные:</p>
|
||||
<p>
|
||||
Для формирования сообщения используется HTML разметка.
|
||||
Telegram поддерживает только часть HTML тегов, которые описаны в их
|
||||
<a href="https://core.telegram.org/bots/api#html-style" target="_blank">документации <i class="fa fa-external-link"></i></a>.
|
||||
</p>
|
||||
<p>Дополнительно к этому TeleCart добавляет переменные, которые вы можете использовать, чтобы сделать сообщения динамическими.</p>
|
||||
<ul>
|
||||
<li><code>{store_name}</code> — название магазина</li>
|
||||
<li><code>{order_id}</code> — номер заказа</li>
|
||||
@@ -50,19 +54,7 @@
|
||||
<li><code>{ip}</code> — IP покупателя</li>
|
||||
<li><code>{created_at}</code> — дата и время создания заказа</li>
|
||||
</ul>
|
||||
<p>
|
||||
Форматирование: поддерживается
|
||||
<a href="https://core.telegram.org/bots/api#markdownv2-style" target="_blank">
|
||||
*MarkdownV2*
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>.
|
||||
</p>
|
||||
<p>Символы, которые нужно экранировать в тексте:</p>
|
||||
<pre>_ * [ ] ( ) ~ ` > # + - = | { } . !</pre>
|
||||
<p>
|
||||
Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования.
|
||||
Например вместо <code>Заказ #123</code> нужно писать <code>Заказ \#123</code>.
|
||||
</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -78,11 +70,15 @@ import {useSettingsStore} from "@/stores/settings.js";
|
||||
import {ref, toRaw, useId} from "vue";
|
||||
import SettingsItem from "@/components/SettingsItem.vue";
|
||||
import {apiPost} from "@/utils/http.js";
|
||||
import {Codemirror} from "vue-codemirror";
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
|
||||
const model = defineModel();
|
||||
const settings = useSettingsStore();
|
||||
const isSending = ref(false);
|
||||
const collapseId = useId();
|
||||
const extensions = [html(), oneDark];
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
|
||||
Reference in New Issue
Block a user