ваше сообщение коммита
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -213,3 +213,9 @@ sync-to-vds.sh
|
|||||||
|
|
||||||
# Database initialization helper script
|
# Database initialization helper script
|
||||||
scripts/internal/db/db_init_helper.sh
|
scripts/internal/db/db_init_helper.sh
|
||||||
|
|
||||||
|
# Blog content (будет перенесен в блог приложения)
|
||||||
|
blog-content/
|
||||||
|
|
||||||
|
# Public docs (загружены в БД)
|
||||||
|
public-docs/
|
||||||
|
|||||||
@@ -1590,6 +1590,107 @@ router.get('/blog/:slug', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Получить публичную страницу по slug (для /content/published)
|
||||||
|
router.get('/published/:slug', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tableName = `admin_pages_simple`;
|
||||||
|
let slug = req.params.slug;
|
||||||
|
|
||||||
|
// Декодируем slug (на случай если он был закодирован)
|
||||||
|
try {
|
||||||
|
slug = decodeURIComponent(slug);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[pages] Ошибка декодирования slug:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация slug
|
||||||
|
if (!slug || typeof slug !== 'string' || slug.trim() === '') {
|
||||||
|
return res.status(400).json({ error: 'Невалидный slug' });
|
||||||
|
}
|
||||||
|
|
||||||
|
slug = slug.trim();
|
||||||
|
|
||||||
|
// Проверяем, есть ли таблица
|
||||||
|
const existsRes = await db.getQuery()(
|
||||||
|
`SELECT to_regclass($1) as exists`, [tableName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existsRes.rows[0].exists) {
|
||||||
|
return res.status(404).json({ error: 'Страница не найдена' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем страницу по slug (без условия show_in_blog)
|
||||||
|
const { rows } = await db.getQuery()(
|
||||||
|
`SELECT * FROM ${tableName}
|
||||||
|
WHERE slug = $1
|
||||||
|
AND visibility = 'public'
|
||||||
|
AND status = 'published'
|
||||||
|
LIMIT 1`,
|
||||||
|
[slug]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[pages] GET /published/:slug: поиск по slug "${slug}", найдено строк: ${rows.length}`);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
// Пробуем найти страницу без учета регистра
|
||||||
|
const { rows: rowsCaseInsensitive } = await db.getQuery()(
|
||||||
|
`SELECT * FROM ${tableName}
|
||||||
|
WHERE LOWER(TRIM(slug)) = LOWER(TRIM($1))
|
||||||
|
AND visibility = 'public'
|
||||||
|
AND status = 'published'
|
||||||
|
LIMIT 1`,
|
||||||
|
[slug]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rowsCaseInsensitive.length > 0) {
|
||||||
|
console.log(`[pages] GET /published/:slug: найдено с учетом регистра, slug в БД: "${rowsCaseInsensitive[0].slug}"`);
|
||||||
|
return res.json(rowsCaseInsensitive[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).json({ error: 'Страница не найдена' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[pages] GET /published/:slug: страница найдена, id: ${rows[0].id}, slug: ${rows[0].slug}`);
|
||||||
|
|
||||||
|
// Расшифровываем зашифрованные поля
|
||||||
|
const page = rows[0];
|
||||||
|
const encryptionUtils = require('../utils/encryptionUtils');
|
||||||
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
|
// Создаем объект с расшифрованными данными
|
||||||
|
const decryptedPage = { ...page };
|
||||||
|
|
||||||
|
// Расшифровываем поля, если они зашифрованы
|
||||||
|
const fieldsToDecrypt = ['title', 'summary', 'content', 'seo', 'settings'];
|
||||||
|
for (const field of fieldsToDecrypt) {
|
||||||
|
const encryptedField = `${field}_encrypted`;
|
||||||
|
if (page[encryptedField]) {
|
||||||
|
try {
|
||||||
|
const decryptResult = await db.getQuery()(
|
||||||
|
`SELECT decrypt_text($1, $2) as ${field}`,
|
||||||
|
[page[encryptedField], encryptionKey]
|
||||||
|
);
|
||||||
|
if (decryptResult.rows[0] && decryptResult.rows[0][field] !== null) {
|
||||||
|
decryptedPage[field] = decryptResult.rows[0][field];
|
||||||
|
}
|
||||||
|
} catch (decryptError) {
|
||||||
|
console.warn(`[pages] GET /published/:slug: ошибка расшифровки поля ${field}:`, decryptError.message);
|
||||||
|
if (page[field]) {
|
||||||
|
decryptedPage[field] = page[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (page[field]) {
|
||||||
|
decryptedPage[field] = page[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(decryptedPage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения публичной страницы по slug:', error);
|
||||||
|
res.status(500).json({ error: 'Ошибка получения страницы' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Получить иерархическую структуру всех публичных страниц
|
// Получить иерархическую структуру всех публичных страниц
|
||||||
router.get('/public/structure', async (req, res) => {
|
router.get('/public/structure', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1903,7 +2004,12 @@ router.get('/internal/all', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Получить одну опубликованную страницу по id
|
// Получить одну опубликованную страницу по id
|
||||||
router.get('/public/:id', async (req, res) => {
|
router.get('/public/:id', async (req, res, next) => {
|
||||||
|
// Пропускаем специальные endpoints
|
||||||
|
if (req.params.id === 'robots.txt' || req.params.id === 'sitemap.xml') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tableName = `admin_pages_simple`;
|
const tableName = `admin_pages_simple`;
|
||||||
|
|
||||||
@@ -1950,7 +2056,7 @@ Disallow: /admin/
|
|||||||
Disallow: /content/create
|
Disallow: /content/create
|
||||||
Disallow: /content/edit
|
Disallow: /content/edit
|
||||||
|
|
||||||
Sitemap: ${baseUrl}/pages/public/sitemap.xml
|
Sitemap: ${baseUrl}/api/pages/public/sitemap.xml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
@@ -2005,7 +2111,8 @@ router.get('/public/sitemap.xml', async (req, res) => {
|
|||||||
|
|
||||||
// Добавляем страницы блога с использованием slug
|
// Добавляем страницы блога с использованием slug
|
||||||
for (const page of blogPages) {
|
for (const page of blogPages) {
|
||||||
const lastmod = page.updated_at || page.created_at || new Date().toISOString();
|
const dateObj = page.updated_at || page.created_at || new Date();
|
||||||
|
const lastmod = dateObj instanceof Date ? dateObj.toISOString() : String(dateObj);
|
||||||
const pageUrl = page.slug
|
const pageUrl = page.slug
|
||||||
? `${baseUrl}/blog/${page.slug}`
|
? `${baseUrl}/blog/${page.slug}`
|
||||||
: `${baseUrl}/blog?page=${page.id}`;
|
: `${baseUrl}/blog?page=${page.id}`;
|
||||||
@@ -2021,22 +2128,25 @@ router.get('/public/sitemap.xml', async (req, res) => {
|
|||||||
|
|
||||||
// Получаем остальные публичные страницы (без show_in_blog)
|
// Получаем остальные публичные страницы (без show_in_blog)
|
||||||
const { rows: otherPages } = await db.getQuery()(`
|
const { rows: otherPages } = await db.getQuery()(`
|
||||||
SELECT id, updated_at, created_at
|
SELECT id, slug, updated_at, created_at
|
||||||
FROM ${tableName}
|
FROM ${tableName}
|
||||||
WHERE status = 'published' AND visibility = 'public' AND (show_in_blog IS NULL OR show_in_blog = FALSE)
|
WHERE status = 'published' AND visibility = 'public' AND (show_in_blog IS NULL OR show_in_blog = FALSE)
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Добавляем остальные публичные страницы
|
// Добавляем остальные публичные страницы с использованием slug
|
||||||
for (const page of otherPages) {
|
for (const page of otherPages) {
|
||||||
const lastmod = page.updated_at || page.created_at || new Date().toISOString();
|
const dateObj = page.updated_at || page.created_at || new Date();
|
||||||
const pageUrl = `${baseUrl}/content/published?page=${page.id}`;
|
const lastmod = dateObj instanceof Date ? dateObj.toISOString() : String(dateObj);
|
||||||
|
const pageUrl = page.slug
|
||||||
sitemap += ` <url>
|
? `${baseUrl}/content/published/${page.slug}`
|
||||||
|
: `${baseUrl}/content/published?page=${page.id}`;
|
||||||
|
|
||||||
|
sitemap += ` <url>
|
||||||
<loc>${escapeXml(pageUrl)}</loc>
|
<loc>${escapeXml(pageUrl)}</loc>
|
||||||
<lastmod>${lastmod.split('T')[0]}</lastmod>
|
<lastmod>${lastmod.split('T')[0]}</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Похожие статьи (только для блога) -->
|
||||||
|
<section v-if="page && isBlogPage && relatedArticles.length > 0" class="related-articles">
|
||||||
|
<h3 class="related-title">
|
||||||
|
<i class="fas fa-newspaper"></i>
|
||||||
|
Читайте также
|
||||||
|
</h3>
|
||||||
|
<div class="related-grid">
|
||||||
|
<article
|
||||||
|
v-for="article in relatedArticles"
|
||||||
|
:key="article.id"
|
||||||
|
class="related-card"
|
||||||
|
@click="openRelatedArticle(article)"
|
||||||
|
>
|
||||||
|
<h4 class="related-card-title">{{ article.title }}</h4>
|
||||||
|
<p v-if="article.summary" class="related-card-summary">{{ truncateSummary(article.summary) }}</p>
|
||||||
|
<span class="related-card-link">Читать →</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Загрузка -->
|
<!-- Загрузка -->
|
||||||
<div v-if="!page && isLoading" class="loading-state">
|
<div v-if="!page && isLoading" class="loading-state">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
@@ -177,6 +197,10 @@ const props = defineProps({
|
|||||||
hideBackButton: {
|
hideBackButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isPublishedRoute: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +216,10 @@ const page = ref(null);
|
|||||||
const navigation = ref(null);
|
const navigation = ref(null);
|
||||||
const breadcrumbs = ref([]);
|
const breadcrumbs = ref([]);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
const relatedArticles = ref([]);
|
||||||
|
|
||||||
|
// Определяем, это страница блога
|
||||||
|
const isBlogPage = computed(() => route.path.startsWith('/blog'));
|
||||||
|
|
||||||
// Установка мета-тегов для SEO
|
// Установка мета-тегов для SEO
|
||||||
function updateMetaTags(pageData) {
|
function updateMetaTags(pageData) {
|
||||||
@@ -330,27 +358,31 @@ async function loadPage() {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Определяем, это slug или id
|
// Определяем, это slug или id
|
||||||
// Проверяем, находимся ли мы на странице блога
|
// Проверяем, находимся ли мы на странице блога или published
|
||||||
const isBlogRoute = route.path.startsWith('/blog');
|
const isBlogRoute = route.path.startsWith('/blog');
|
||||||
|
const isPublishedSlugRoute = props.isPublishedRoute || route.path.startsWith('/content/published/');
|
||||||
|
|
||||||
// Если это строка и не чисто число, или мы на странице блога - считаем это slug
|
// Если это строка и не чисто число, или мы на странице блога/published - считаем это slug
|
||||||
const isSlug = isBlogRoute || (typeof props.pageId === 'string' && !/^\d+$/.test(props.pageId));
|
const isSlug = isBlogRoute || isPublishedSlugRoute || (typeof props.pageId === 'string' && !/^\d+$/.test(props.pageId));
|
||||||
|
|
||||||
console.log('[DocsContent] loadPage:', {
|
console.log('[DocsContent] loadPage:', {
|
||||||
pageId: props.pageId,
|
pageId: props.pageId,
|
||||||
isBlogRoute,
|
isBlogRoute,
|
||||||
|
isPublishedSlugRoute,
|
||||||
isSlug,
|
isSlug,
|
||||||
routePath: route.path
|
routePath: route.path
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSlug) {
|
if (isSlug) {
|
||||||
// Загружаем по slug через новый endpoint блога
|
// Загружаем по slug
|
||||||
// Если pageId это число, но мы на странице блога, конвертируем в строку
|
|
||||||
const slug = typeof props.pageId === 'string' ? props.pageId : String(props.pageId);
|
const slug = typeof props.pageId === 'string' ? props.pageId : String(props.pageId);
|
||||||
console.log('[DocsContent] Загрузка по slug:', slug);
|
console.log('[DocsContent] Загрузка по slug:', slug, 'isPublishedSlugRoute:', isPublishedSlugRoute);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await pagesService.getBlogPageBySlug(slug);
|
// Используем разные endpoints для blog и published
|
||||||
|
const response = isPublishedSlugRoute
|
||||||
|
? await pagesService.getPublishedPageBySlug(slug)
|
||||||
|
: await pagesService.getBlogPageBySlug(slug);
|
||||||
console.log('[DocsContent] Ответ от API:', {
|
console.log('[DocsContent] Ответ от API:', {
|
||||||
hasData: !!response,
|
hasData: !!response,
|
||||||
type: typeof response,
|
type: typeof response,
|
||||||
@@ -411,6 +443,11 @@ async function loadPage() {
|
|||||||
console.error('[DocsContent] Ошибка установки мета-тегов (не критично):', metaError);
|
console.error('[DocsContent] Ошибка установки мета-тегов (не критично):', metaError);
|
||||||
// Продолжаем работу, даже если мета-теги не установились
|
// Продолжаем работу, даже если мета-теги не установились
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем похожие статьи для блога
|
||||||
|
if (isBlogPage.value) {
|
||||||
|
loadRelatedArticles();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('[DocsContent] page.value пусто после загрузки!', {
|
console.error('[DocsContent] page.value пусто после загрузки!', {
|
||||||
response: 'данные не были установлены',
|
response: 'данные не были установлены',
|
||||||
@@ -629,6 +666,38 @@ function formatDate(date) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загрузка похожих статей для блога
|
||||||
|
async function loadRelatedArticles() {
|
||||||
|
if (!isBlogPage.value || !page.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allArticles = await pagesService.getBlogPages();
|
||||||
|
// Исключаем текущую статью и берём до 3 других
|
||||||
|
relatedArticles.value = allArticles
|
||||||
|
.filter(article => article.id !== page.value.id)
|
||||||
|
.slice(0, 3);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[DocsContent] Ошибка загрузки похожих статей:', e);
|
||||||
|
relatedArticles.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрезка summary для карточек
|
||||||
|
function truncateSummary(text, maxLength = 100) {
|
||||||
|
if (!text) return '';
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength).trim() + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открытие похожей статьи
|
||||||
|
function openRelatedArticle(article) {
|
||||||
|
if (article.slug) {
|
||||||
|
router.push({ name: 'blog-article', params: { slug: article.slug } });
|
||||||
|
} else if (article.id) {
|
||||||
|
router.push({ name: 'blog', query: { page: article.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(path) {
|
function navigateTo(path) {
|
||||||
// Поддержка разных форматов путей
|
// Поддержка разных форматов путей
|
||||||
const match1 = path.match(/\/content\/published\/(\d+)/);
|
const match1 = path.match(/\/content\/published\/(\d+)/);
|
||||||
@@ -1280,5 +1349,79 @@ onMounted(() => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Похожие статьи */
|
||||||
|
.related-articles {
|
||||||
|
margin-top: 48px;
|
||||||
|
padding-top: 32px;
|
||||||
|
border-top: 1px solid var(--border-color, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #111827);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title i {
|
||||||
|
color: var(--primary-color, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
background: var(--bg-secondary, #f9fafb);
|
||||||
|
border: 1px solid var(--border-color, #e5e7eb);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card:hover {
|
||||||
|
border-color: var(--primary-color, #3b82f6);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #111827);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-summary {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-link {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--primary-color, #3b82f6);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.related-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-articles {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,11 @@ const routes = [
|
|||||||
name: 'content-published',
|
name: 'content-published',
|
||||||
component: () => import('../views/content/PublishedListView.vue'),
|
component: () => import('../views/content/PublishedListView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/content/published/:slug',
|
||||||
|
name: 'content-published-slug',
|
||||||
|
component: () => import('../views/content/PublishedPageView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/content/internal',
|
path: '/content/internal',
|
||||||
name: 'content-internal',
|
name: 'content-internal',
|
||||||
|
|||||||
@@ -83,6 +83,17 @@ export default {
|
|||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
async getPublishedPageBySlug(slug) {
|
||||||
|
console.log('[pagesService] getPublishedPageBySlug:', slug);
|
||||||
|
const res = await api.get(`/pages/published/${encodeURIComponent(slug)}`);
|
||||||
|
console.log('[pagesService] getPublishedPageBySlug response:', {
|
||||||
|
status: res.status,
|
||||||
|
hasData: !!res.data,
|
||||||
|
id: res.data?.id,
|
||||||
|
title: res.data?.title
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
async getPublicPagesStructure() {
|
async getPublicPagesStructure() {
|
||||||
const res = await api.get('/pages/public/structure');
|
const res = await api.get('/pages/public/structure');
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
192
frontend/src/views/content/PublishedPageView.vue
Normal file
192
frontend/src/views/content/PublishedPageView.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/VC-HB3-Accelerator
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout :is-authenticated="isAuthenticated" :identities="identities" :token-balances="tokenBalances" :is-loading-tokens="isLoadingTokens" @auth-action-completed="$emit('auth-action-completed')">
|
||||||
|
<div class="docs-page">
|
||||||
|
<div class="docs-header">
|
||||||
|
<button class="close-btn" @click="goBack" title="Закрыть">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основной контент: сайдбар + контент -->
|
||||||
|
<div class="docs-layout has-content">
|
||||||
|
<!-- Сайдбар навигации -->
|
||||||
|
<DocsSidebar :current-page-id="currentPageId" />
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<div class="docs-main">
|
||||||
|
<DocsContent v-if="pageSlug" :page-id="pageSlug" :is-published-route="true" @back="goToIndex" />
|
||||||
|
|
||||||
|
<div v-else class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>Загрузка документа...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import DocsSidebar from '../../components/docs/DocsSidebar.vue';
|
||||||
|
import DocsContent from '../../components/docs/DocsContent.vue';
|
||||||
|
import pagesService from '../../services/pagesService';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isAuthenticated: { type: Boolean, default: false },
|
||||||
|
identities: { type: Array, default: () => [] },
|
||||||
|
tokenBalances: { type: Object, default: () => ({}) },
|
||||||
|
isLoadingTokens: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const currentPageId = ref(null);
|
||||||
|
const pageSlug = computed(() => route.params.slug);
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'content-published' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToIndex() {
|
||||||
|
router.push({ name: 'content-published' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем ID страницы по slug для сайдбара
|
||||||
|
onMounted(async () => {
|
||||||
|
if (pageSlug.value) {
|
||||||
|
try {
|
||||||
|
const page = await pagesService.getPublishedPageBySlug(pageSlug.value);
|
||||||
|
if (page && page.id) {
|
||||||
|
currentPageId.value = page.id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PublishedPageView] Ошибка загрузки страницы:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.docs-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
background: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #888;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.docs-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-main {
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.docs-page {
|
||||||
|
height: auto;
|
||||||
|
min-height: calc(100vh - 40px);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-layout {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-layout.has-content .docs-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-main {
|
||||||
|
overflow-y: visible;
|
||||||
|
min-height: auto;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user