.
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
### Инструкция (Промпт) для AI Агента
|
||||||
|
|
||||||
|
Ты — Senior Vue.js разработчик. Мне нужно исправить рендеринг Markdown в компоненте.
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Компонент отображает сырой Markdown (символы #, \*, таблицы в виде текста), вместо HTML разметки. Стили CSS есть, но они не применяются, так как HTML не генерируется.
|
||||||
|
|
||||||
|
**Анализ кода:**
|
||||||
|
|
||||||
|
1. В секции imports из 'vue' забыли добавить `computed`.
|
||||||
|
2. Функция `renderMarkdown` скорее всего падает в `catch` и возвращает исходную строку.
|
||||||
|
3. Библиотека `marked` в последних версиях изменила API (удален `setOptions`, используется `marked.parse`).
|
||||||
|
|
||||||
|
**Задача:**
|
||||||
|
Перепиши секцию `<script setup>` и исправь логику рендеринга.
|
||||||
|
|
||||||
|
**Требования к исправлению:**
|
||||||
|
|
||||||
|
1. Добавь `computed` в импорты из 'vue'.
|
||||||
|
2. Обнови использование библиотеки `marked`. Убедись, что используется `marked.parse(text)`. Вместо устаревшего `setOptions` используй современный подход (или `marked.use()`, или просто дефолтный парсинг, так как `gfm` и `breaks` обычно включены по умолчанию).
|
||||||
|
3. Убедись, что переменная `renderedSummary` корректно обрабатывает `report.summary` и возвращает безопасный HTML (если summary нет, возвращать пустую строку).
|
||||||
|
4. (Опционально) Добавь импорт `DomPurify` (если он есть в проекте) для очистки HTML перед выводом в `v-html`, но если нет — оставь просто парсинг.
|
||||||
|
|
||||||
|
**Ожидаемый результат:**
|
||||||
|
Код компонента, где `renderedSummary` возвращает корректный HTML, и заголовки/таблицы отображаются красиво согласно CSS стилям `.markdown-content`.
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
### Что именно должен изменить агент (для вашего понимания):
|
||||||
|
|
||||||
|
Агент должен изменить блок скрипта примерно так:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Было
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
// Станет (добавится computed)
|
||||||
|
import { onBeforeUnmount, onMounted, ref, computed } from 'vue';
|
||||||
|
|
||||||
|
// Было (конфигурация marked)
|
||||||
|
if (marked.setOptions) { ... }
|
||||||
|
// Станет (упрощенно и надежно)
|
||||||
|
const renderMarkdown = (markdown) => {
|
||||||
|
if (!markdown) return '';
|
||||||
|
try {
|
||||||
|
// Просто парсим, так как в новых версиях gfm включен по умолчанию
|
||||||
|
return marked.parse(markdown, { breaks: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Markdown parsing error:', e);
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
````
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
# Social Media Credentials API - Frontend Guide
|
||||||
|
|
||||||
|
## Обзор изменений
|
||||||
|
|
||||||
|
API для управления credentials социальных сетей был обновлен для поддержки разных форматов данных в зависимости от платформы. Поле `credentials` теперь может принимать как простую строку (токен), так и JSON объект для платформ, требующих несколько параметров.
|
||||||
|
|
||||||
|
## Базовый URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://your-server:port/api/social-media/credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
Все endpoints требуют JWT токен в заголовке `Authorization`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your-jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Изменения в API
|
||||||
|
|
||||||
|
### Поле `credentials` теперь поддерживает разные типы
|
||||||
|
|
||||||
|
**До изменений:**
|
||||||
|
|
||||||
|
- `credentials` был только `String` (токен)
|
||||||
|
|
||||||
|
**После изменений:**
|
||||||
|
|
||||||
|
- `credentials` может быть `String` (для простых токенов) или `Object` (для сложных структур)
|
||||||
|
|
||||||
|
### Поддерживаемые форматы
|
||||||
|
|
||||||
|
#### 1. Простой токен (String) - для Facebook, LinkedIn
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"credentials": "EAABwzLix..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. JSON объект (Object) - для Telegram и других платформ с несколькими параметрами
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "telegram",
|
||||||
|
"credentials": {
|
||||||
|
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
|
||||||
|
"chatId": "-1001234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Сохранение/обновление credentials
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/social-media/credentials`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body для простого токена (Facebook/LinkedIn):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"credentials": "EAABwzLix..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body для JSON объекта (Telegram):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "telegram",
|
||||||
|
"credentials": {
|
||||||
|
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
|
||||||
|
"chatId": "-1001234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Credentials успешно сохранены для платформы telegram",
|
||||||
|
"data": {
|
||||||
|
"platform": "telegram",
|
||||||
|
"hasCredentials": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00",
|
||||||
|
"updatedAt": "2024-01-15T10:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примеры использования (JavaScript):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Сохранение простого токена (Facebook/LinkedIn)
|
||||||
|
const saveFacebookCredentials = async (accessToken) => {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'facebook',
|
||||||
|
credentials: accessToken, // Простая строка
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сохранение JSON объекта (Telegram)
|
||||||
|
const saveTelegramCredentials = async (botToken, chatId) => {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'telegram',
|
||||||
|
credentials: {
|
||||||
|
// JSON объект
|
||||||
|
botToken: botToken,
|
||||||
|
chatId: chatId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
await saveFacebookCredentials('EAABwzLix...');
|
||||||
|
await saveTelegramCredentials('123456789:ABC...', '-1001234567890');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Получение информации о credentials
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/social-media/credentials/{platform}`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
|
||||||
|
- `platform` (string) - Название платформы
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": {
|
||||||
|
"platform": "telegram",
|
||||||
|
"hasCredentials": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Получение списка всех credentials
|
||||||
|
|
||||||
|
**Endpoint:** `GET /api/social-media/credentials`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Операция выполнена успешно",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"hasCredentials": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00",
|
||||||
|
"updatedAt": "2024-01-15T10:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "telegram",
|
||||||
|
"hasCredentials": true,
|
||||||
|
"createdAt": "2024-01-15T11:00:00",
|
||||||
|
"updatedAt": "2024-01-15T11:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Удаление credentials
|
||||||
|
|
||||||
|
**Endpoint:** `DELETE /api/social-media/credentials/{platform}`
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
|
||||||
|
- `platform` (string) - Название платформы
|
||||||
|
|
||||||
|
**Response 200 OK:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Credentials для платформы telegram успешно удалены",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Форматы credentials по платформам
|
||||||
|
|
||||||
|
### Facebook
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "facebook",
|
||||||
|
"credentials": "EAABwzLix..." // Access Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LinkedIn
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "linkedin",
|
||||||
|
"credentials": "AQV..." // Access Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "telegram",
|
||||||
|
"credentials": {
|
||||||
|
"botToken": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
|
||||||
|
"chatId": "-1001234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примечание для Telegram:**
|
||||||
|
|
||||||
|
- `botToken` - токен бота, полученный от @BotFather
|
||||||
|
- `chatId` - ID чата/канала (может быть отрицательным для групп и каналов)
|
||||||
|
- Формат: `-1001234567890` для супергрупп и каналов
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
### 400 Bad Request - Ошибка валидации
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Ошибка валидации",
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Platform is required",
|
||||||
|
"details": {
|
||||||
|
"platform": "Platform is required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 401 Unauthorized
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Не авторизован",
|
||||||
|
"error": {
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Внутренняя ошибка сервера",
|
||||||
|
"error": {
|
||||||
|
"code": "INTERNAL_SERVER_ERROR",
|
||||||
|
"message": "Произошла ошибка при сохранении credentials"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Миграция с старого API
|
||||||
|
|
||||||
|
Если вы использовали старый API, где `credentials` был только строкой, изменения минимальны:
|
||||||
|
|
||||||
|
**Старый код:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'facebook',
|
||||||
|
credentials: 'EAABwzLix...',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Новый код (без изменений для простых токенов):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Работает как раньше
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'facebook',
|
||||||
|
credentials: 'EAABwzLix...', // String по-прежнему поддерживается
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Для Telegram (новый формат):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'telegram',
|
||||||
|
credentials: {
|
||||||
|
// Теперь можно передавать объект
|
||||||
|
botToken: '...',
|
||||||
|
chatId: '...',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
1. **Обратная совместимость**: API полностью обратно совместим. Старый формат (String) продолжает работать.
|
||||||
|
|
||||||
|
2. **Автоматическая конвертация**: Если вы передаете объект, он автоматически сериализуется в JSON строку перед сохранением.
|
||||||
|
|
||||||
|
3. **Безопасность**: Все credentials автоматически шифруются перед сохранением в базе данных.
|
||||||
|
|
||||||
|
4. **Валидация**: Поле `credentials` обязательно (`@NotNull`), но может быть как строкой, так и объектом.
|
||||||
|
|
||||||
|
## Примеры для TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SimpleCredentials {
|
||||||
|
platform: string;
|
||||||
|
credentials: string; // Для Facebook, LinkedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramCredentials {
|
||||||
|
platform: 'telegram';
|
||||||
|
credentials: {
|
||||||
|
botToken: string;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialsRequest = SimpleCredentials | TelegramCredentials;
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
const saveCredentials = async (request: CredentialsRequest) => {
|
||||||
|
const response = await fetch('/api/social-media/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwtToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -387,8 +387,8 @@ class MarketingService {
|
|||||||
/**
|
/**
|
||||||
* Сохранение/обновление credentials для социальной сети
|
* Сохранение/обновление credentials для социальной сети
|
||||||
* POST /api/social-media/credentials
|
* POST /api/social-media/credentials
|
||||||
* @param {string} platform - Название платформы (facebook, instagram, etc.)
|
* @param {string} platform - Название платформы (facebook, instagram, telegram, etc.)
|
||||||
* @param {string} credentials - Access token или другие credentials
|
* @param {string|Object} credentials - Access token (строка) для простых платформ или объект для сложных (например, {botToken, chatId} для Telegram)
|
||||||
* @returns {Promise<Object>} Объект с информацией о сохраненных credentials
|
* @returns {Promise<Object>} Объект с информацией о сохраненных credentials
|
||||||
*/
|
*/
|
||||||
async saveCredentials(platform, credentials) {
|
async saveCredentials(platform, credentials) {
|
||||||
@@ -398,7 +398,7 @@ class MarketingService {
|
|||||||
...DEFAULT_REQUEST_CONFIG,
|
...DEFAULT_REQUEST_CONFIG,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
platform: platform,
|
platform: platform,
|
||||||
credentials: credentials
|
credentials: credentials // Может быть строкой или объектом
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,8 @@
|
|||||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Краткое резюме</h3>
|
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Краткое резюме</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="text-surface-700 dark:text-surface-300 whitespace-pre-line">{{ report.summary }}</p>
|
<div v-if="report.summary" class="markdown-content" v-html="renderedSummary"></div>
|
||||||
|
<div v-else class="text-surface-500 dark:text-surface-400">Резюме недоступно</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -283,7 +284,7 @@ import Tag from 'primevue/tag';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -662,23 +663,27 @@ const resetAnalysis = () => {
|
|||||||
errors.value = {};
|
errors.value = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configure marked options
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render markdown to HTML
|
// Render markdown to HTML
|
||||||
const renderMarkdown = (markdown) => {
|
const renderMarkdown = (markdown) => {
|
||||||
if (!markdown) return '';
|
if (!markdown || typeof markdown !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return marked.parse(markdown);
|
// В marked v17 используется marked.parse() с опциями вторым параметром
|
||||||
|
return marked.parse(markdown, { breaks: true, gfm: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering markdown:', error);
|
console.error('Error rendering markdown:', error);
|
||||||
|
// В случае ошибки возвращаем исходный текст
|
||||||
return markdown;
|
return markdown;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Computed свойство для рендеринга summary
|
||||||
|
const renderedSummary = computed(() => {
|
||||||
|
if (!report.value || !report.value.summary) return '';
|
||||||
|
return renderMarkdown(report.value.summary);
|
||||||
|
});
|
||||||
|
|
||||||
// Get analysis type label
|
// Get analysis type label
|
||||||
const getAnalysisTypeLabel = (type) => {
|
const getAnalysisTypeLabel = (type) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
@@ -912,4 +917,40 @@ onBeforeUnmount(() => {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(thead) {
|
||||||
|
background-color: var(--surface-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(th) {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-bottom: 2px solid var(--surface-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(td) {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(tr:last-child td) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(tr:hover) {
|
||||||
|
background-color: var(--surface-50);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,49 +12,47 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Платформа <span class="text-red-500">*</span> </label>
|
||||||
Платформа <span class="text-red-500">*</span>
|
<Dropdown id="platform" v-model="formData.platform" :options="platformOptions" placeholder="Выберите платформу" class="w-full" :class="{ 'p-invalid': errors.platform }" />
|
||||||
</label>
|
|
||||||
<Dropdown
|
|
||||||
id="platform"
|
|
||||||
v-model="formData.platform"
|
|
||||||
:options="platformOptions"
|
|
||||||
placeholder="Выберите платформу"
|
|
||||||
class="w-full"
|
|
||||||
:class="{ 'p-invalid': errors.platform }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
|
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6">
|
<!-- Поля для простых платформ (Facebook, LinkedIn, etc.) -->
|
||||||
<div class="field">
|
<template v-if="formData.platform && formData.platform !== 'telegram'">
|
||||||
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
<div class="col-12 md:col-6">
|
||||||
Access Token <span class="text-red-500">*</span>
|
<div class="field">
|
||||||
</label>
|
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Access Token <span class="text-red-500">*</span> </label>
|
||||||
<InputText
|
<InputText id="credentials" v-model="formData.credentials" type="password" placeholder="Введите access token" class="w-full" :class="{ 'p-invalid': errors.credentials }" />
|
||||||
id="credentials"
|
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
|
||||||
v-model="formData.credentials"
|
<small class="text-surface-500 dark:text-surface-400 mt-1 block"> Токен будет зашифрован перед сохранением </small>
|
||||||
type="password"
|
</div>
|
||||||
placeholder="Введите access token"
|
|
||||||
class="w-full"
|
|
||||||
:class="{ 'p-invalid': errors.credentials }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
|
|
||||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">
|
|
||||||
Токен будет зашифрован перед сохранением
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<!-- Поля для Telegram -->
|
||||||
|
<template v-if="formData.platform === 'telegram'">
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<div class="field">
|
||||||
|
<label for="botToken" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Bot Token <span class="text-red-500">*</span> </label>
|
||||||
|
<InputText id="botToken" v-model="formData.botToken" type="password" placeholder="Введите bot token от @BotFather" class="w-full" :class="{ 'p-invalid': errors.botToken }" />
|
||||||
|
<small v-if="errors.botToken" class="p-error">{{ errors.botToken }}</small>
|
||||||
|
<small class="text-surface-500 dark:text-surface-400 mt-1 block"> Токен бота, полученный от @BotFather </small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 md:col-3">
|
||||||
|
<div class="field">
|
||||||
|
<label for="chatId" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Chat ID <span class="text-red-500">*</span> </label>
|
||||||
|
<InputText id="chatId" v-model="formData.chatId" type="text" placeholder="Введите chat ID (например: -1001234567890)" class="w-full" :class="{ 'p-invalid': errors.chatId }" />
|
||||||
|
<small v-if="errors.chatId" class="p-error">{{ errors.chatId }}</small>
|
||||||
|
<small class="text-surface-500 dark:text-surface-400 mt-1 block"> ID чата/канала (может быть отрицательным для групп и каналов) </small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="col-12 md:col-2">
|
<div class="col-12 md:col-2">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> </label>
|
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> </label>
|
||||||
<Button
|
<Button type="submit" label="Сохранить" icon="pi pi-save" class="w-full" :loading="saving" />
|
||||||
type="submit"
|
|
||||||
label="Сохранить"
|
|
||||||
icon="pi pi-save"
|
|
||||||
class="w-full"
|
|
||||||
:loading="saving"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,15 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<DataTable
|
<DataTable :value="credentials" :loading="loading" paginator :rows="10" :rowsPerPageOptions="[5, 10, 20]" dataKey="platform" :emptyMessage="'Нет настроенных платформ'">
|
||||||
:value="credentials"
|
|
||||||
:loading="loading"
|
|
||||||
paginator
|
|
||||||
:rows="10"
|
|
||||||
:rowsPerPageOptions="[5, 10, 20]"
|
|
||||||
dataKey="platform"
|
|
||||||
:emptyMessage="'Нет настроенных платформ'"
|
|
||||||
>
|
|
||||||
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
|
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<Tag :value="slotProps.data.platform" severity="info" />
|
<Tag :value="slotProps.data.platform" severity="info" />
|
||||||
@@ -87,10 +77,7 @@
|
|||||||
|
|
||||||
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
|
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<Tag
|
<Tag :value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'" :severity="slotProps.data.hasCredentials ? 'success' : 'warning'" />
|
||||||
:value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'"
|
|
||||||
:severity="slotProps.data.hasCredentials ? 'success' : 'warning'"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
@@ -110,14 +97,7 @@
|
|||||||
|
|
||||||
<Column header="Действия" style="min-width: 150px">
|
<Column header="Действия" style="min-width: 150px">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<Button
|
<Button icon="pi pi-trash" severity="danger" size="small" v-tooltip.top="'Удалить credentials'" @click="confirmDelete(slotProps.data.platform)" :disabled="!slotProps.data.hasCredentials" />
|
||||||
icon="pi pi-trash"
|
|
||||||
severity="danger"
|
|
||||||
size="small"
|
|
||||||
v-tooltip.top="'Удалить credentials'"
|
|
||||||
@click="confirmDelete(slotProps.data.platform)"
|
|
||||||
:disabled="!slotProps.data.hasCredentials"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
@@ -126,15 +106,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Диалог подтверждения удаления -->
|
<!-- Диалог подтверждения удаления -->
|
||||||
<Dialog
|
<Dialog v-model:visible="deleteDialogVisible" :header="'Подтверждение удаления'" :style="{ width: '450px' }" :modal="true">
|
||||||
v-model:visible="deleteDialogVisible"
|
|
||||||
:header="'Подтверждение удаления'"
|
|
||||||
:style="{ width: '450px' }"
|
|
||||||
:modal="true"
|
|
||||||
>
|
|
||||||
<div class="confirmation-content">
|
<div class="confirmation-content">
|
||||||
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
||||||
<span>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b>?</span>
|
<span
|
||||||
|
>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b
|
||||||
|
>?</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
|
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
|
||||||
@@ -155,14 +133,16 @@ import InputText from 'primevue/inputtext';
|
|||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
platform: null,
|
platform: null,
|
||||||
credentials: ''
|
credentials: '', // Для простых платформ (facebook, linkedin, etc.)
|
||||||
|
botToken: '', // Для Telegram
|
||||||
|
chatId: '' // Для Telegram
|
||||||
});
|
});
|
||||||
|
|
||||||
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
|
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
|
||||||
@@ -185,10 +165,24 @@ const validateForm = () => {
|
|||||||
errors.value.platform = 'Пожалуйста, выберите платформу';
|
errors.value.platform = 'Пожалуйста, выберите платформу';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
|
// Валидация для Telegram
|
||||||
errors.value.credentials = 'Access token обязателен';
|
if (formData.value.platform === 'telegram') {
|
||||||
} else if (formData.value.credentials.trim().length < 10) {
|
if (!formData.value.botToken || formData.value.botToken.trim().length === 0) {
|
||||||
errors.value.credentials = 'Access token слишком короткий';
|
errors.value.botToken = 'Bot token обязателен';
|
||||||
|
} else if (formData.value.botToken.trim().length < 10) {
|
||||||
|
errors.value.botToken = 'Bot token слишком короткий';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.value.chatId || formData.value.chatId.trim().length === 0) {
|
||||||
|
errors.value.chatId = 'Chat ID обязателен';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Валидация для остальных платформ
|
||||||
|
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
|
||||||
|
errors.value.credentials = 'Access token обязателен';
|
||||||
|
} else if (formData.value.credentials.trim().length < 10) {
|
||||||
|
errors.value.credentials = 'Access token слишком короткий';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(errors.value).length === 0;
|
return Object.keys(errors.value).length === 0;
|
||||||
@@ -228,7 +222,20 @@ const handleSaveCredentials = async () => {
|
|||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MarketingService.saveCredentials(formData.value.platform, formData.value.credentials.trim());
|
// Подготовка credentials в зависимости от платформы
|
||||||
|
let credentialsToSave;
|
||||||
|
if (formData.value.platform === 'telegram') {
|
||||||
|
// Для Telegram отправляем объект
|
||||||
|
credentialsToSave = {
|
||||||
|
botToken: formData.value.botToken.trim(),
|
||||||
|
chatId: formData.value.chatId.trim()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Для остальных платформ отправляем строку
|
||||||
|
credentialsToSave = formData.value.credentials.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
await MarketingService.saveCredentials(formData.value.platform, credentialsToSave);
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Успешно',
|
summary: 'Успешно',
|
||||||
@@ -239,7 +246,9 @@ const handleSaveCredentials = async () => {
|
|||||||
// Reset form
|
// Reset form
|
||||||
formData.value = {
|
formData.value = {
|
||||||
platform: null,
|
platform: null,
|
||||||
credentials: ''
|
credentials: '',
|
||||||
|
botToken: '',
|
||||||
|
chatId: ''
|
||||||
};
|
};
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
|
|
||||||
@@ -247,7 +256,7 @@ const handleSaveCredentials = async () => {
|
|||||||
await loadCredentials();
|
await loadCredentials();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при сохранении credentials:', error);
|
console.error('Ошибка при сохранении credentials:', error);
|
||||||
|
|
||||||
// Handle specific error codes
|
// Handle specific error codes
|
||||||
if (error.code === 'VALIDATION_ERROR') {
|
if (error.code === 'VALIDATION_ERROR') {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -304,7 +313,7 @@ const handleDeleteCredentials = async () => {
|
|||||||
await loadCredentials();
|
await loadCredentials();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при удалении credentials:', error);
|
console.error('Ошибка при удалении credentials:', error);
|
||||||
|
|
||||||
if (error.code === 'NOT_FOUND') {
|
if (error.code === 'NOT_FOUND') {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
@@ -345,6 +354,18 @@ const formatDateTime = (dateString) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch platform changes to reset form fields
|
||||||
|
watch(
|
||||||
|
() => formData.value.platform,
|
||||||
|
(newPlatform) => {
|
||||||
|
// Сброс полей при смене платформы
|
||||||
|
formData.value.credentials = '';
|
||||||
|
formData.value.botToken = '';
|
||||||
|
formData.value.chatId = '';
|
||||||
|
errors.value = {};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Load on mount
|
// Load on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCredentials();
|
loadCredentials();
|
||||||
@@ -381,4 +402,3 @@ onMounted(() => {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user