feat: use floating customer service widget in pro
Deploy olons-server production / deploy (push) Successful in 3m1s
Deploy olons-server production / deploy (push) Successful in 3m1s
This commit is contained in:
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
|
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
|
||||||
docker build -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
|
docker build --build-arg VITE_CUSTOMER_SERVICE_PROVIDER=third_party -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
|
||||||
docker push "$IMAGE:pro"
|
docker push "$IMAGE:pro"
|
||||||
docker push "$IMAGE:$SHORT_SHA"
|
docker push "$IMAGE:$SHORT_SHA"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ RUN corepack enable
|
|||||||
COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml ./
|
COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
COPY web/ ./
|
COPY web/ ./
|
||||||
|
ARG VITE_CUSTOMER_SERVICE_PROVIDER=
|
||||||
ENV VITE_PUBLIC_PATH=/
|
ENV VITE_PUBLIC_PATH=/
|
||||||
|
ENV VITE_CUSTOMER_SERVICE_PROVIDER=${VITE_CUSTOMER_SERVICE_PROVIDER}
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# ---- Stage 2: build the Go backend ----
|
# ---- Stage 2: build the Go backend ----
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ class DeployWorkflowContractTest(unittest.TestCase):
|
|||||||
self.assertIn("git.olns.shop/pro/olons_server", self.workflow)
|
self.assertIn("git.olns.shop/pro/olons_server", self.workflow)
|
||||||
self.assertIn("docker push", self.workflow)
|
self.assertIn("docker push", self.workflow)
|
||||||
self.assertIn(":pro", self.workflow)
|
self.assertIn(":pro", self.workflow)
|
||||||
|
self.assertIn("--build-arg VITE_CUSTOMER_SERVICE_PROVIDER=third_party", self.workflow)
|
||||||
|
|
||||||
|
def test_production_image_enables_customer_service_widget(self):
|
||||||
|
dockerfile = (WORKFLOW.parents[2] / "Dockerfile").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("ARG VITE_CUSTOMER_SERVICE_PROVIDER=", dockerfile)
|
||||||
|
self.assertIn("ENV VITE_CUSTOMER_SERVICE_PROVIDER=${VITE_CUSTOMER_SERVICE_PROVIDER}", dockerfile)
|
||||||
|
|
||||||
def test_deploys_compose_service_with_pinned_host_key(self):
|
def test_deploys_compose_service_with_pinned_host_key(self):
|
||||||
self.assertIn("DEPLOY_SSH_KEY", self.workflow)
|
self.assertIn("DEPLOY_SSH_KEY", self.workflow)
|
||||||
@@ -53,4 +59,3 @@ class DeployWorkflowContractTest(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
v-for="item in services"
|
v-for="item in services"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="m-service-card"
|
class="m-service-card"
|
||||||
@click="$router.push(`/app/support/${item.id}`)"
|
@click="openService(item)"
|
||||||
>
|
>
|
||||||
<div class="m-service-avatar"
|
<div class="m-service-avatar"
|
||||||
><img
|
><img
|
||||||
@@ -35,6 +35,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="useThirdPartyChat" class="m-third-party-launch">
|
||||||
|
<PageHeader title="在线客服" />
|
||||||
|
<div class="m-third-party-launch__content">
|
||||||
|
<h2>在线客服</h2>
|
||||||
|
<p>聊天窗口将在当前页面内打开。</p>
|
||||||
|
<button type="button" class="m-primary-btn" @click="openThirdPartyChat">打开在线客服</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section v-else class="m-chat">
|
<section v-else class="m-chat">
|
||||||
<PageHeader :title="current?.name || '在线客服'" />
|
<PageHeader :title="current?.name || '在线客服'" />
|
||||||
<div class="m-chat-time">会话内容已安全加密,请勿发送密码或验证码</div>
|
<div class="m-chat-time">会话内容已安全加密,请勿发送密码或验证码</div>
|
||||||
@@ -144,7 +153,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
HappyOutline,
|
HappyOutline,
|
||||||
HeadsetOutline,
|
HeadsetOutline,
|
||||||
@@ -161,8 +170,77 @@
|
|||||||
type CustomerServiceConnectionState,
|
type CustomerServiceConnectionState,
|
||||||
type CustomerServiceSession,
|
type CustomerServiceSession,
|
||||||
} from '../im';
|
} from '../im';
|
||||||
|
type ThirdPartyChatWidget = {
|
||||||
|
initialized: boolean;
|
||||||
|
configReady: boolean;
|
||||||
|
init: (config: { channelId: string; useCustomButton: boolean }) => void;
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ChatWidget?: ThirdPartyChatWidget;
|
||||||
|
__olonsChatWidgetScriptPromise?: Promise<void>;
|
||||||
|
__olonsChatWidgetConfigPromise?: Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const isChat = computed(() => Boolean(route.params.id));
|
const isChat = computed(() => Boolean(route.params.id));
|
||||||
|
const useThirdPartyChat = import.meta.env.VITE_CUSTOMER_SERVICE_PROVIDER === 'third_party';
|
||||||
|
const thirdPartyChatChannelId = 'ebdf4b92054645d992ec9ede4ac19161';
|
||||||
|
const thirdPartyChatScriptUrl = 'https://47.115.208.214:1582/chat/widget.js';
|
||||||
|
function openService(item: any) {
|
||||||
|
if (useThirdPartyChat) {
|
||||||
|
void openThirdPartyChat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void router.push(`/app/support/${item.id}`);
|
||||||
|
}
|
||||||
|
function loadThirdPartyChatWidget() {
|
||||||
|
if (window.ChatWidget?.initialized) return Promise.resolve();
|
||||||
|
if (!window.__olonsChatWidgetScriptPromise) {
|
||||||
|
window.__olonsChatWidgetScriptPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = thirdPartyChatScriptUrl;
|
||||||
|
script.async = true;
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = () => reject(new Error('客服插件加载失败,请稍后重试'));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return window.__olonsChatWidgetScriptPromise;
|
||||||
|
}
|
||||||
|
async function initializeThirdPartyChatWidget() {
|
||||||
|
if (!useThirdPartyChat) return;
|
||||||
|
try {
|
||||||
|
await loadThirdPartyChatWidget();
|
||||||
|
const widget = window.ChatWidget;
|
||||||
|
if (!widget) throw new Error('客服插件初始化失败,请刷新页面重试');
|
||||||
|
if (!widget.initialized) {
|
||||||
|
window.__olonsChatWidgetConfigPromise = new Promise<void>((resolve) => {
|
||||||
|
window.addEventListener('chatWidgetReady', () => resolve(), { once: true });
|
||||||
|
});
|
||||||
|
widget.init({ channelId: thirdPartyChatChannelId, useCustomButton: true });
|
||||||
|
}
|
||||||
|
if (widget.configReady) return;
|
||||||
|
await Promise.race([
|
||||||
|
window.__olonsChatWidgetConfigPromise,
|
||||||
|
new Promise<void>((_, reject) => window.setTimeout(() => reject(new Error('客服配置加载超时,请稍后重试')), 10000)),
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
window.$message?.error(errorText(err));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function openThirdPartyChat() {
|
||||||
|
try {
|
||||||
|
await initializeThirdPartyChatWidget();
|
||||||
|
window.ChatWidget?.open();
|
||||||
|
} catch {
|
||||||
|
// initializeThirdPartyChatWidget already displays a user-facing error.
|
||||||
|
}
|
||||||
|
}
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const services = ref<any[]>([]);
|
const services = ref<any[]>([]);
|
||||||
@@ -418,9 +496,62 @@
|
|||||||
}).format(new Date(value));
|
}).format(new Date(value));
|
||||||
}
|
}
|
||||||
watch(() => route.params.id, (serviceID) => {
|
watch(() => route.params.id, (serviceID) => {
|
||||||
|
if (useThirdPartyChat) {
|
||||||
|
if (!serviceID) void load();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (serviceID) void connectChat();
|
if (serviceID) void connectChat();
|
||||||
else disconnectChat();
|
else disconnectChat();
|
||||||
}, { immediate: true, flush: 'sync' });
|
}, { immediate: true, flush: 'sync' });
|
||||||
onMounted(load);
|
onMounted(() => {
|
||||||
onBeforeUnmount(disconnectChat);
|
if (!isChat.value) void load();
|
||||||
|
if (useThirdPartyChat) void initializeThirdPartyChatWidget().catch(() => undefined);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
disconnectChat();
|
||||||
|
if (useThirdPartyChat && window.ChatWidget?.initialized) window.ChatWidget.close();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.m-third-party-launch {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100dvh;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-third-party-launch__content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-third-party-launch__content h2,
|
||||||
|
.m-third-party-launch__content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.chat-widget-iframe-container.mobile) {
|
||||||
|
width: min(92vw, 380px) !important;
|
||||||
|
height: min(72dvh, 560px) !important;
|
||||||
|
max-height: calc(100dvh - 150px) !important;
|
||||||
|
top: auto !important;
|
||||||
|
right: 12px !important;
|
||||||
|
bottom: calc(84px + env(safe-area-inset-bottom)) !important;
|
||||||
|
left: auto !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
box-shadow: 0 8px 28px rgb(0 0 0 / 20%) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.chat-widget-iframe-container.mobile .chat-widget-iframe) {
|
||||||
|
border-radius: 14px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user