+ Каждый из них нужно экранировать обратным слэшем \, если он не используется для форматирования.
+ Например вместо Заказ #123 нужно писать Заказ \#123.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue b/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
new file mode 100644
index 0000000..f239fe7
--- /dev/null
+++ b/frontend/admin/src/components/Settings/ItemTgMiniAppLink.vue
@@ -0,0 +1,31 @@
+
+
+ Токен, полученный при создании бота через @BotFather.
+ Он используется для взаимодействия модуля с Telegram API.
+ Подробная инструкция доступна в
+
+ документации
+ .
+
+
+
+
+
+
diff --git a/frontend/admin/src/components/Slider/Slider.vue b/frontend/admin/src/components/Slider/Slider.vue
index 7c382e7..6b178a8 100644
--- a/frontend/admin/src/components/Slider/Slider.vue
+++ b/frontend/admin/src/components/Slider/Slider.vue
@@ -157,7 +157,7 @@ import LinkSelector from "@/components/Slider/LinkSelector.vue";
import SettingsItem from "@/components/SettingsItem.vue";
import Switcher from "@/components/Switcher.vue";
-const slider = ref({});
+const slider = defineModel();
function removeSlide(index) {
slider.value.slides.splice(index, 1);
@@ -173,10 +173,6 @@ function addSlide() {
image: '',
});
}
-
-onMounted(() => {
- slider.value = JSON.parse(window.TeleCart.mainpage_slider);
-});
diff --git a/frontend/admin/src/main.js b/frontend/admin/src/main.js
index c83f111..4097131 100644
--- a/frontend/admin/src/main.js
+++ b/frontend/admin/src/main.js
@@ -3,6 +3,15 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
+import {useSettingsStore} from "@/stores/settings.js";
+import PrimeVue from 'primevue/config';
+import Aura from '@primeuix/themes/aura';
+import ToastService from 'primevue/toastservice';
+import {definePreset} from "@primeuix/themes";
+
+const MyPreset = definePreset(Aura, {
+
+});
function onReady(fn) {
if (document.readyState === 'loading') {
@@ -12,9 +21,20 @@ function onReady(fn) {
}
}
-onReady(() => {
+onReady(async () => {
const app = createApp(App);
app.use(createPinia());
app.use(router);
+ app.use(PrimeVue, {
+ theme: {
+ preset: MyPreset,
+ options: {
+ cssLayer: false, // если используешь Tailwind, отключает layering
+ },
+ }
+ });
+ app.use(ToastService);
+
app.mount('#app');
+ await useSettingsStore().fetchSettings();
});
diff --git a/frontend/admin/src/router/index.js b/frontend/admin/src/router/index.js
index 4569e2f..5bbba6a 100644
--- a/frontend/admin/src/router/index.js
+++ b/frontend/admin/src/router/index.js
@@ -1,15 +1,23 @@
-import {createMemoryHistory, createRouter} from 'vue-router'
-import HomeView from '../views/HomeView.vue'
+import {createMemoryHistory, createRouter} from 'vue-router';
+import SliderView from "@/views/SliderView.vue";
+import GeneralView from "@/views/GeneralView.vue";
+import TextsView from "@/views/TextsView.vue";
+import OrdersView from "@/views/OrdersView.vue";
+import TelegramView from "@/views/TelegramView.vue";
+import MetricsView from "@/views/MetricsView.vue";
+import StoreView from "@/views/StoreView.vue";
const router = createRouter({
history: createMemoryHistory(),
routes: [
- {
- path: '/',
- name: 'home',
- component: HomeView,
- },
+ {path: '/', name: 'general', component: GeneralView},
+ {path: '/slider', name: 'slider', component: SliderView},
+ {path: '/orders', name: 'orders', component: OrdersView},
+ {path: '/texts', name: 'texts', component: TextsView},
+ {path: '/telegram', name: 'telegram', component: TelegramView},
+ {path: '/metrics', name: 'metrics', component: MetricsView},
+ {path: '/store', name: 'store', component: StoreView},
],
-})
+});
-export default router
+export default router;
diff --git a/frontend/admin/src/stores/settings.js b/frontend/admin/src/stores/settings.js
new file mode 100644
index 0000000..1c77814
--- /dev/null
+++ b/frontend/admin/src/stores/settings.js
@@ -0,0 +1,125 @@
+import {defineStore} from "pinia";
+import {apiGet, apiPost} from "@/utils/http.js";
+import {toastBus} from "@/utils/toastHelper.js";
+
+export const useSettingsStore = defineStore('settings', {
+ state: () => ({
+ isLoading: false,
+ error: null,
+
+ items: {
+ app: {
+ app_enabled: true,
+ app_name: '',
+ app_icon: null,
+ theme_light: 'light',
+ theme_dark: 'dark',
+ app_debug: false,
+ },
+
+ telegram: {
+ mini_app_url: '',
+ bot_token: '',
+ chat_id: '',
+ owner_notification_template: '',
+ customer_notification_template: '',
+ },
+
+ metrics: {
+ yandex_metrika_enabled: false,
+ yandex_metrika_counter: '',
+ },
+
+ store: {
+ enable_store: true,
+ mainpage_products: 'most_viewed',
+ featured_products: [],
+ mainpage_categories: 'latest10',
+ featured_categories: [],
+ feature_coupons: true,
+ feature_vouchers: true,
+ },
+
+ orders: {
+ order_default_status_id: 1,
+ },
+
+ texts: {
+ text_no_more_products: '',
+ text_empty_cart: '',
+ text_order_created_success: '',
+ },
+
+ sliders: {
+ mainpage_slider: {
+ is_enabled: false,
+ effect: "slide",
+ pagination: true,
+ scrollbar: false,
+ free_mode: false,
+ space_between: 30,
+ autoplay: false,
+ loop: false,
+ slides: [],
+ },
+ },
+ },
+ }),
+
+ getters: {
+ app_icon_preview: (state) => {
+ if (!state.items.app.app_icon) return '/image/cache/no_image-100x100.png';
+ const extIndex = state.items.app.app_icon.lastIndexOf('.');
+ const ext = state.items.app.app_icon.substring(extIndex);
+ const filename = state.items.app.app_icon.substring(0, extIndex);
+ return `/image/cache/${filename}-100x100${ext}`;
+ },
+ },
+
+ actions: {
+ async fetchSettings() {
+ this.isLoading = true;
+ this.error = null;
+ const response = await apiGet('getSettingsForm');
+ if (response.success) {
+ this.items = {
+ ...this.items,
+ ...response.data,
+ };
+ } else {
+ this.error = 'Возникли проблемы при загрузке настроек.';
+ }
+ this.isLoading = false;
+ },
+
+ async saveSettings() {
+ this.isLoading = true;
+ const settings = this.transformSettingsToStore(this.items);
+ const response = await apiPost('saveSettingsForm', settings);
+
+ if (response.success === true) {
+ toastBus.emit('show', {
+ severity: 'success',
+ summary: 'Готово!',
+ detail: 'Настройки сохранены.',
+ life: 2000,
+ });
+ } else {
+ toastBus.emit('show', {
+ severity: 'error',
+ summary: 'Ошибка',
+ detail: 'Возникли проблемы при сохранении настроек на сервере.',
+ life: 2000,
+ });
+ }
+
+
+
+ this.isLoading = false;
+ },
+
+ transformSettingsToStore(items) {
+ return items;
+ },
+ },
+});
diff --git a/frontend/admin/src/stores/stats.js b/frontend/admin/src/stores/stats.js
new file mode 100644
index 0000000..2afac9a
--- /dev/null
+++ b/frontend/admin/src/stores/stats.js
@@ -0,0 +1,22 @@
+import {defineStore} from "pinia";
+import {apiGet, apiPost} from "@/utils/http.js";
+
+export const useStatsStore = defineStore('stats', {
+ state: () => ({
+ items: {
+ orders_count: null,
+ orders_total_amount: null,
+ order_products_count: null,
+ }
+ }),
+
+ actions: {
+ async fetchStats() {
+ const response = await apiPost('getDashboardStats');
+ this.items.orders_count = response.data?.data?.orders_count;
+ this.items.orders_total_amount = response.data?.data?.orders_total_amount;
+ this.items.order_products_count = response.data?.data?.order_products_count;
+ }
+ },
+
+});
diff --git a/frontend/admin/src/utils/http.js b/frontend/admin/src/utils/http.js
new file mode 100644
index 0000000..d68a0e1
--- /dev/null
+++ b/frontend/admin/src/utils/http.js
@@ -0,0 +1,142 @@
+import axios from 'axios';
+
+/**
+ * Получает user_token из глобального объекта TeleCart
+ */
+function getUserToken() {
+ if (typeof window !== 'undefined' && window.TeleCart?.user_token) {
+ return window.TeleCart.user_token;
+ }
+
+ // Fallback: пытаемся получить из URL как запасной вариант
+ if (typeof window !== 'undefined') {
+ const urlParams = new URLSearchParams(window.location.search);
+ return urlParams.get('user_token') || '';
+ }
+
+ return '';
+}
+
+/**
+ * Базовый URL для API запросов
+ */
+function getBaseUrl() {
+ return '/admin/index.php';
+}
+
+/**
+ * Создает URL для API запроса
+ * @param {string} apiAction - действие API (например, 'configureBotToken')
+ * @returns {string} полный URL
+ */
+function buildApiUrl(apiAction) {
+ const baseUrl = getBaseUrl();
+ const userToken = getUserToken();
+ return `${baseUrl}?route=extension/module/tgshop/handle&api_action=${apiAction}&user_token=${userToken}`;
+}
+
+/**
+ * HTTP клиент для работы с API
+ */
+const httpClient = axios.create({
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+/**
+ * Выполняет POST запрос к API
+ * @param {string} apiAction - действие API
+ * @param {object} data - данные для отправки
+ * @returns {Promise} результат запроса
+ */
+export async function apiPost(apiAction, data = {}) {
+ const url = buildApiUrl(apiAction);
+
+ try {
+ const response = await httpClient.post(url, data);
+ return {
+ success: true,
+ data: response.data,
+ status: response.status,
+ };
+ } catch (error) {
+ // Обработка ошибок axios
+ if (error.response) {
+ // Сервер вернул ошибку
+ const status = error.response.status;
+ const errorData = error.response.data;
+
+ return {
+ success: false,
+ error: errorData?.error || error.response.statusText,
+ status,
+ data: errorData,
+ };
+ } else if (error.request) {
+ // Запрос был отправлен, но ответа не получено
+ return {
+ success: false,
+ error: 'Не удалось получить ответ от сервера',
+ status: 0,
+ };
+ } else {
+ // Ошибка при настройке запроса
+ return {
+ success: false,
+ error: error.message || 'Произошла неизвестная ошибка',
+ status: 0,
+ };
+ }
+ }
+}
+
+/**
+ * Выполняет GET запрос к API
+ * @param {string} apiAction - действие API
+ * @param {object} params - query параметры
+ * @returns {Promise} результат запроса
+ */
+export async function apiGet(apiAction, params = {}) {
+ const url = buildApiUrl(apiAction);
+
+ try {
+ const response = await httpClient.get(url, { params: params });
+ return {
+ success: true,
+ data: response.data.data,
+ status: response.status,
+ };
+ } catch (error) {
+ if (error.response) {
+ const status = error.response.status;
+ const errorData = error.response.data;
+
+ return {
+ success: false,
+ error: errorData?.error || error.response.statusText,
+ status,
+ data: errorData,
+ };
+ } else if (error.request) {
+ return {
+ success: false,
+ error: 'Не удалось получить ответ от сервера',
+ status: 0,
+ };
+ } else {
+ return {
+ success: false,
+ error: error.message || 'Произошла неизвестная ошибка',
+ status: 0,
+ };
+ }
+ }
+}
+
+export default {
+ apiPost,
+ apiGet,
+ getUserToken,
+};
+
diff --git a/frontend/admin/src/utils/toastHelper.js b/frontend/admin/src/utils/toastHelper.js
new file mode 100644
index 0000000..96ca792
--- /dev/null
+++ b/frontend/admin/src/utils/toastHelper.js
@@ -0,0 +1,2 @@
+import mitt from 'mitt';
+export const toastBus = mitt();
diff --git a/frontend/admin/src/views/GeneralView.vue b/frontend/admin/src/views/GeneralView.vue
new file mode 100644
index 0000000..93af1c3
--- /dev/null
+++ b/frontend/admin/src/views/GeneralView.vue
@@ -0,0 +1,56 @@
+
+
+ Если выключено, покупатели в Telegram увидят сообщение, что магазин временно закрыт.
+ Заказы и просмотр товаров будут недоступны.
+
+
+
+ Отображается в заголовке Telegram Mini App при запуске, а также используется как подпись
+ под иконкой, если пользователь добавит приложение на главный экран своего устройства.
+ Рекомендуется короткое и понятное название (до 20 символов).
+ Если оставить пустым, то название выводиться не будет.
+
+
+
+ Изображение, которое будет отображаться в Telegram Mini App.
+
+
+
+ Выберите стиль, который будет использоваться при отображении вашего магазина
+ в Telegram для дневного режима.
+
+ Посмотреть как выглядят темы
+
+
+
+
+ Выберите стиль, который будет использоваться при отображении вашего магазина
+ в Telegram для ночного режима.
+
+ Посмотреть как выглядят темы
+
+
+
+
+ Режим разработчика. Рекомендуется включать только по необходимости.
+ В остальных случаях, для нормальной работы магазина, должен быть выключен.
+
+
+
+
+
+
diff --git a/frontend/admin/src/views/HomeView.vue b/frontend/admin/src/views/HomeView.vue
deleted file mode 100644
index 9000062..0000000
--- a/frontend/admin/src/views/HomeView.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/frontend/admin/src/views/MetricsView.vue b/frontend/admin/src/views/MetricsView.vue
new file mode 100644
index 0000000..85dcc60
--- /dev/null
+++ b/frontend/admin/src/views/MetricsView.vue
@@ -0,0 +1,29 @@
+
+
+ Задействовать Яндекс.Метрику для Telegram магазина.
+
+
+
+
Для проверки интеграции через кнопку "Проверить" в интерфейсе Яндекс Метрики,
+ необходимо сначала включить "Режим разработчика" на вкладке "Общие".
+
+
+
+
diff --git a/frontend/admin/src/views/OrdersView.vue b/frontend/admin/src/views/OrdersView.vue
new file mode 100644
index 0000000..b778039
--- /dev/null
+++ b/frontend/admin/src/views/OrdersView.vue
@@ -0,0 +1,17 @@
+
+
+ Статус, с которым будут создаваться заказы через Telegram по умолчанию.
+
+
+
+
diff --git a/frontend/admin/src/views/SliderView.vue b/frontend/admin/src/views/SliderView.vue
new file mode 100644
index 0000000..152fce9
--- /dev/null
+++ b/frontend/admin/src/views/SliderView.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/frontend/admin/src/views/StoreView.vue b/frontend/admin/src/views/StoreView.vue
new file mode 100644
index 0000000..6e07e75
--- /dev/null
+++ b/frontend/admin/src/views/StoreView.vue
@@ -0,0 +1,80 @@
+
+
+
Если опция включена — пользователи смогут оформлять
+ заказы прямо в Telegram-магазине.
+ Если выключена — оформление заказов будет недоступно. Вместо кнопки «Добавить
+ в корзину» пользователи увидят кнопку «Перейти к товару», которая откроет страницу товара на
+ вашем сайте. В этом режиме Telecart работает как каталог.
+
+
+
+ Выберите, какие товары показывать на главной странице магазина в Telegram.
+ Это влияет на первую видимую секцию каталога для пользователя.
+
+
+
+ На главной странице будут отображаться избранные товары, если вы выберете этот вариант в
+ настройке “Товары на главной”. Если товары не выбраны, то будут показаны популярные товары.
+
+
+
+ Выберите, какие товары показывать на главной странице магазина в Telegram.
+ Это влияет на первую видимую секцию каталога для пользователя.
+
+
+
+ На главной странице будут отображаться эти категории,
+ если вы выберете этот вариант в настройке “Категории на главной”.
+
+
+
+
+ Позволяет использовать стандартные
+ купоны OpenCart
+ для предоставления скидок при оформлении заказа.