feat(support): виджет онлайн-чата JivoSite (спит без ключа)

- JivoWidget.vue: подгружает скрипт Jivo при заданном VITE_JIVO_WIDGET_ID
  (по образцу SmartCaptchaWidget); ключ пуст → ничего не грузит, виджет спит.
- Смонтирован в AppLayout (клиентский портал). +VITE_JIVO_WIDGET_ID в vite-env.d.ts.
- TDD: JivoWidget.spec (грузит при ключе / спит без) GREEN; AppLayout.spec 16/16; eslint 0.
- Активируется вставкой VITE_JIVO_WIDGET_ID (твой ID канала Jivo). На прод не выкачено.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-22 19:19:58 +03:00
parent 15b55d5cb5
commit 6d943db936
4 changed files with 60 additions and 0 deletions
@@ -0,0 +1,30 @@
<script setup lang="ts">
/**
* Виджет онлайн-чата JivoSite (G7-A). Когда задан VITE_JIVO_WIDGET_ID —
* на странице подгружается скрипт Jivo и появляется плавающая кнопка чата
* (Jivo рисует её сам). Ключ пуст (dev/CI/нет аккаунта) → ничего не грузим,
* виджет спит. По образцу SmartCaptchaWidget (условная подгрузка скрипта).
*/
import { onMounted } from 'vue';
const widgetId = import.meta.env.VITE_JIVO_WIDGET_ID ?? '';
const SCRIPT_ID = 'jivosite-widget-js';
function injectJivo(): void {
if (widgetId === '' || document.getElementById(SCRIPT_ID) !== null) {
return;
}
const script = document.createElement('script');
script.id = SCRIPT_ID;
script.src = `//code.jivosite.com/widget/${widgetId}`;
script.async = true;
document.head.appendChild(script);
}
onMounted(injectJivo);
</script>
<template>
<!-- Jivo рисует плавающую кнопку чата сам (в body); здесь скрытый якорь. -->
<span class="jivo-widget-anchor" aria-hidden="true" style="display: none" />
</template>
+2
View File
@@ -20,6 +20,7 @@ import AppSidebar from '../components/layout/AppSidebar.vue';
import AppTopbar from '../components/layout/AppTopbar.vue';
import DevIndexBadge from '../components/DevIndexBadge.vue';
import CommandPalette from '../components/layout/CommandPalette.vue';
import JivoWidget from '../components/support/JivoWidget.vue';
import BalanceFrozenBanner from '../components/billing/BalanceFrozenBanner.vue';
import ImpersonationSessionBanner from '../components/admin/ImpersonationSessionBanner.vue';
@@ -88,6 +89,7 @@ usePolling(loadBalanceStatus, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabl
</v-main>
<DevIndexBadge :index="route.meta.devIndex" :label="route.meta.devLabel" />
<CommandPalette />
<JivoWidget />
</v-app>
</template>
+1
View File
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_YANDEX_SMARTCAPTCHA_SITEKEY?: string;
readonly VITE_JIVO_WIDGET_ID?: string;
}
interface ImportMeta {
+27
View File
@@ -0,0 +1,27 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mount } from '@vue/test-utils';
import JivoWidget from '../../resources/js/components/support/JivoWidget.vue';
beforeEach(() => {
document.querySelectorAll('#jivosite-widget-js').forEach((n) => n.remove());
});
afterEach(() => {
vi.unstubAllEnvs();
});
describe('JivoWidget', () => {
it('подгружает скрипт Jivo, когда задан VITE_JIVO_WIDGET_ID', () => {
vi.stubEnv('VITE_JIVO_WIDGET_ID', 'abc123');
mount(JivoWidget);
const script = document.getElementById('jivosite-widget-js') as HTMLScriptElement | null;
expect(script).not.toBeNull();
expect(script?.src).toContain('code.jivosite.com/widget/abc123');
});
it('ничего не грузит, когда ключ пуст (виджет спит)', () => {
vi.stubEnv('VITE_JIVO_WIDGET_ID', '');
mount(JivoWidget);
expect(document.getElementById('jivosite-widget-js')).toBeNull();
});
});