2026-08-17 11:32:40 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view v-if="visible" class="qiaobao-assistant" :class="{ 'is-collapsed': collapsed }">
|
|
|
|
|
|
<view v-if="collapsed" class="qiaobao-edge-tab"
|
|
|
|
|
|
:class="{ 'is-left': collapsedEdge === 'left', 'is-dragging': dockDragging }" :style="dockStyle" role="button"
|
2026-08-26 17:00:58 +08:00
|
|
|
|
aria-label="桥宝已收起,点击展开" @click.stop="restoreFromDock" @tap.stop="restoreFromDock" @touchstart.stop="handleDockDragStart"
|
2026-08-17 11:32:40 +08:00
|
|
|
|
@touchmove.stop.prevent="handleDockDragMove" @touchend.stop="handleDockDragEnd"
|
|
|
|
|
|
@touchcancel.stop="handleDockDragEnd" @mousedown.stop.prevent="handleDockMouseDragStart">
|
|
|
|
|
|
<image class="qiaobao-edge-head" src="https://static.tbmall.xin/static/mine/qiaobao-edge-peek.png"
|
|
|
|
|
|
mode="aspectFit" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else class="qiaobao-float" :class="{ 'is-scratching': action === 'scratch' }" :style="floatStyle"
|
2026-08-26 17:00:58 +08:00
|
|
|
|
role="button" aria-label="桥宝,点击查看当前页面说明,长按可隐藏" @click.stop="openHelp" @tap.stop="openHelp" @longpress.stop="openHideMenu"
|
2026-08-17 11:32:40 +08:00
|
|
|
|
@contextmenu.stop.prevent="openHideMenu" @touchstart.stop="handleDragStart"
|
|
|
|
|
|
@touchmove.stop.prevent="handleDragMove" @touchend.stop="handleDragEnd" @touchcancel.stop="handleDragEnd"
|
|
|
|
|
|
@mousedown.stop.prevent="handleMouseDragStart">
|
|
|
|
|
|
<view v-if="showBubble" class="qiaobao-bubble">点我,了解这个页面</view>
|
|
|
|
|
|
<image class="qiaobao-image" :src="actionImage" mode="aspectFit" />
|
|
|
|
|
|
<view class="qiaobao-shadow" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-if="panelVisible" class="qiaobao-mask" @click="closeHelp" @touchmove.stop.prevent>
|
|
|
|
|
|
<view class="qiaobao-sheet" @click.stop>
|
|
|
|
|
|
<view class="qiaobao-handle" />
|
|
|
|
|
|
<view class="qiaobao-sheet-head">
|
|
|
|
|
|
<view class="qiaobao-avatar-wrap">
|
|
|
|
|
|
<image class="qiaobao-avatar" src="https://static.tbmall.xin/static/mine/qiaobao-idle.png"
|
|
|
|
|
|
mode="aspectFit" />
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="qiaobao-title-wrap">
|
2026-08-26 17:00:58 +08:00
|
|
|
|
<text class="qiaobao-kicker">桥宝 · AI 商城助手</text>
|
|
|
|
|
|
<view class="qiaobao-online-row">
|
|
|
|
|
|
<view class="qiaobao-online-dot" />
|
|
|
|
|
|
<text class="qiaobao-title">陪你逛信任桥TB商城</text>
|
|
|
|
|
|
</view>
|
2026-08-17 11:32:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="qiaobao-close" aria-label="关闭" @click="closeHelp">×</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
<view class="qiaobao-page-context">
|
|
|
|
|
|
<text class="qiaobao-context-label">当前页面</text>
|
|
|
|
|
|
<text class="qiaobao-context-title">{{ resolvedTitle }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<scroll-view class="qiaobao-chat" scroll-y :scroll-top="chatScrollTop" :scroll-with-animation="true">
|
|
|
|
|
|
<view v-for="(item, index) in chatMessages" :key="`${item.role}-${index}`" class="qiaobao-message"
|
|
|
|
|
|
:class="`is-${item.role}`">
|
|
|
|
|
|
<image v-if="item.role === 'assistant'" class="qiaobao-message-avatar"
|
|
|
|
|
|
src="https://static.tbmall.xin/static/mine/qiaobao-idle.png" mode="aspectFit" />
|
|
|
|
|
|
<view class="qiaobao-message-bubble">
|
|
|
|
|
|
<text>{{ item.content }}</text>
|
2026-08-17 11:32:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-08-26 17:00:58 +08:00
|
|
|
|
<view v-if="chatLoading" class="qiaobao-message is-assistant">
|
|
|
|
|
|
<image class="qiaobao-message-avatar" src="https://static.tbmall.xin/static/mine/qiaobao-blink.png"
|
|
|
|
|
|
mode="aspectFit" />
|
|
|
|
|
|
<view class="qiaobao-message-bubble is-typing"><text>桥宝正在想</text><text class="qiaobao-dots">•••</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="qiaobao-chat-spacer" />
|
2026-08-17 11:32:40 +08:00
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
<scroll-view v-if="suggestions.length" class="qiaobao-suggestions" scroll-x :show-scrollbar="false">
|
|
|
|
|
|
<view class="qiaobao-suggestion-row">
|
|
|
|
|
|
<button v-for="suggestion in suggestions" :key="suggestion" class="qiaobao-suggestion"
|
|
|
|
|
|
@click="sendSuggestion(suggestion)">{{ suggestion }}</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="qiaobao-compose">
|
|
|
|
|
|
<input v-model="chatInput" class="qiaobao-input" maxlength="500" confirm-type="send"
|
|
|
|
|
|
placeholder="问桥宝商城、商品或活动问题" :disabled="chatLoading" @confirm="sendMessage" />
|
|
|
|
|
|
<button class="qiaobao-send" :disabled="chatLoading || !chatInput" @click="sendMessage">发送</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="qiaobao-sheet-foot">
|
|
|
|
|
|
<text class="qiaobao-hide-hint">长按桥宝可收起到屏幕边缘</text>
|
|
|
|
|
|
<text class="qiaobao-human-link" @click="goToCustomerService">人工客服</text>
|
|
|
|
|
|
</view>
|
2026-08-17 11:32:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
consumeQiaobaoAutoIntro,
|
|
|
|
|
|
closeQiaobaoForSession,
|
|
|
|
|
|
getQiaobaoChatHistory,
|
|
|
|
|
|
getQiaobaoChatSessionId,
|
2026-08-17 11:32:40 +08:00
|
|
|
|
getCurrentPagePath,
|
|
|
|
|
|
getQiaobaoDockEdge,
|
|
|
|
|
|
getQiaobaoPosition,
|
|
|
|
|
|
hideQiaobaoForPage,
|
|
|
|
|
|
hideQiaobaoGlobally,
|
2026-08-26 17:00:58 +08:00
|
|
|
|
isQiaobaoClosedForSession,
|
2026-08-17 11:32:40 +08:00
|
|
|
|
isQiaobaoHidden,
|
|
|
|
|
|
saveQiaobaoPosition,
|
2026-08-26 17:00:58 +08:00
|
|
|
|
saveQiaobaoChatHistory,
|
2026-08-17 11:32:40 +08:00
|
|
|
|
setQiaobaoDockEdge,
|
|
|
|
|
|
showQiaobao,
|
|
|
|
|
|
} from "@/utils/qiaobao.js";
|
|
|
|
|
|
import { resolveQiaobaoPageHelp } from "@/utils/qiaobao-page-help.js";
|
2026-08-26 17:00:58 +08:00
|
|
|
|
import { getStorageFun, USER_DATA } from "@/utils/auth.js";
|
|
|
|
|
|
import { chatWithQiaobao } from "@/api/qiaobao.js";
|
|
|
|
|
|
|
|
|
|
|
|
const MALL_WELCOME = "你好,我是桥宝。信任桥TB商城为你提供食品饮料、家居日用、个护清洁等日常商品。商城重视商品信息、价格展示、规则透明和消费权益记录。你可以问我商城特色、商品活动、订单入口或商城规则。";
|
|
|
|
|
|
const LOGIN_WELCOME = "欢迎回来,我是桥宝。以后逛商城、看活动或不清楚规则时,点我就可以直接问。账户、订单、物流等实时信息请以对应页面为准,需要人工帮助我也会告诉你入口。";
|
|
|
|
|
|
const DEFAULT_SUGGESTIONS = ["商城有什么特点", "怎么买更划算", "贡献值是什么", "联系人工客服"];
|
2026-08-17 11:32:40 +08:00
|
|
|
|
|
|
|
|
|
|
const ACTION_IMAGES = {
|
|
|
|
|
|
idle: "https://static.tbmall.xin/static/mine/qiaobao-idle.png",
|
|
|
|
|
|
blink: "https://static.tbmall.xin/static/mine/qiaobao-blink.png",
|
|
|
|
|
|
scratch: "https://static.tbmall.xin/static/mine/qiaobao-scratch.png",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: "QiaobaoAssistant",
|
|
|
|
|
|
props: {
|
|
|
|
|
|
pageKey: { type: String, default: "" },
|
|
|
|
|
|
title: { type: String, default: "" },
|
|
|
|
|
|
summary: { type: String, default: "" },
|
|
|
|
|
|
tips: { type: Array, default: () => [] },
|
|
|
|
|
|
bubble: { type: Boolean, default: true },
|
|
|
|
|
|
},
|
|
|
|
|
|
emits: ["open", "close", "hide"],
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
visible: false,
|
|
|
|
|
|
collapsed: false,
|
|
|
|
|
|
collapsedEdge: "right",
|
|
|
|
|
|
panelVisible: false,
|
|
|
|
|
|
showBubble: false,
|
|
|
|
|
|
action: "idle",
|
|
|
|
|
|
actionTimer: null,
|
|
|
|
|
|
bubbleTimer: null,
|
|
|
|
|
|
longPressTriggered: false,
|
|
|
|
|
|
position: null,
|
|
|
|
|
|
viewportWidth: 0,
|
|
|
|
|
|
viewportHeight: 0,
|
|
|
|
|
|
safeAreaTop: 0,
|
|
|
|
|
|
safeAreaBottom: 0,
|
|
|
|
|
|
petWidth: 82,
|
|
|
|
|
|
petHeight: 88,
|
|
|
|
|
|
dockHeight: 48,
|
|
|
|
|
|
dockDragging: false,
|
|
|
|
|
|
dockDragMoved: false,
|
|
|
|
|
|
dockDragJustEnded: false,
|
|
|
|
|
|
dockDragStartPoint: { x: 0, y: 0 },
|
|
|
|
|
|
dockDragOriginY: 0,
|
|
|
|
|
|
dockPointerX: 0,
|
|
|
|
|
|
dragging: false,
|
|
|
|
|
|
dragMoved: false,
|
|
|
|
|
|
dragJustEnded: false,
|
|
|
|
|
|
dragStartPoint: { x: 0, y: 0 },
|
|
|
|
|
|
dragOrigin: { x: 0, y: 0 },
|
2026-08-26 17:00:58 +08:00
|
|
|
|
chatInput: "",
|
|
|
|
|
|
chatLoading: false,
|
|
|
|
|
|
chatMessages: [],
|
|
|
|
|
|
suggestions: [...DEFAULT_SUGGESTIONS],
|
|
|
|
|
|
chatScrollTop: 0,
|
|
|
|
|
|
chatSessionId: "",
|
|
|
|
|
|
chatScope: "guest",
|
|
|
|
|
|
autoIntroTimer: null,
|
2026-08-17 11:32:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
currentPageKey() {
|
|
|
|
|
|
return this.pageKey || getCurrentPagePath();
|
|
|
|
|
|
},
|
|
|
|
|
|
actionImage() {
|
|
|
|
|
|
return ACTION_IMAGES[this.action] || ACTION_IMAGES.idle;
|
|
|
|
|
|
},
|
|
|
|
|
|
floatStyle() {
|
|
|
|
|
|
if (!this.position) return null;
|
|
|
|
|
|
return {
|
|
|
|
|
|
left: `${this.position.x}px`,
|
|
|
|
|
|
top: `${this.position.y}px`,
|
|
|
|
|
|
right: "auto",
|
|
|
|
|
|
bottom: "auto",
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
dockStyle() {
|
|
|
|
|
|
if (!this.position) return null;
|
|
|
|
|
|
const maxTop = Math.max(this.safeAreaTop + 8, this.viewportHeight - this.safeAreaBottom - this.dockHeight - 8);
|
|
|
|
|
|
return {
|
|
|
|
|
|
top: `${Math.min(maxTop, Math.max(this.safeAreaTop + 8, this.position.y + (this.petHeight - this.dockHeight) / 2))}px`,
|
|
|
|
|
|
bottom: "auto",
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
automaticHelp() {
|
|
|
|
|
|
return resolveQiaobaoPageHelp(this.currentPageKey);
|
|
|
|
|
|
},
|
|
|
|
|
|
resolvedTitle() {
|
|
|
|
|
|
return this.title || this.automaticHelp.title;
|
|
|
|
|
|
},
|
|
|
|
|
|
resolvedSummary() {
|
|
|
|
|
|
return this.summary || this.automaticHelp.summary;
|
|
|
|
|
|
},
|
|
|
|
|
|
resolvedTips() {
|
|
|
|
|
|
const customTips = Array.isArray(this.tips) ? this.tips.filter(Boolean) : [];
|
|
|
|
|
|
return customTips.length ? customTips : this.automaticHelp.tips;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
uni.$on("qiaobao-visibility-change", this.handleVisibilityChange);
|
|
|
|
|
|
this.initializeDragPosition();
|
|
|
|
|
|
this.refreshVisibility();
|
2026-08-26 17:00:58 +08:00
|
|
|
|
this.initializeChat();
|
2026-08-17 11:32:40 +08:00
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
window.addEventListener("mousemove", this.handleMouseDragMove);
|
|
|
|
|
|
window.addEventListener("mouseup", this.handleMouseDragEnd);
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeUnmount() {
|
|
|
|
|
|
uni.$off("qiaobao-visibility-change", this.handleVisibilityChange);
|
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
window.removeEventListener("mousemove", this.handleMouseDragMove);
|
|
|
|
|
|
window.removeEventListener("mouseup", this.handleMouseDragEnd);
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
this.clearTimers();
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (this.autoIntroTimer) clearTimeout(this.autoIntroTimer);
|
2026-08-17 11:32:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
initializeChat() {
|
|
|
|
|
|
const userData = getStorageFun(USER_DATA) || {};
|
|
|
|
|
|
this.chatScope = userData.userId ? `user-${userData.userId}` : "guest";
|
|
|
|
|
|
this.chatSessionId = getQiaobaoChatSessionId();
|
|
|
|
|
|
this.chatMessages = getQiaobaoChatHistory(this.chatScope);
|
|
|
|
|
|
if (!this.chatMessages.length) {
|
|
|
|
|
|
this.chatMessages = [{ role: "assistant", content: MALL_WELCOME }];
|
|
|
|
|
|
saveQiaobaoChatHistory(this.chatMessages, this.chatScope);
|
|
|
|
|
|
}
|
|
|
|
|
|
const reason = consumeQiaobaoAutoIntro(userData.userId);
|
|
|
|
|
|
if (reason === "first-login") {
|
|
|
|
|
|
this.appendAssistantMessage(LOGIN_WELCOME, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
appendAssistantMessage(content, persist = true) {
|
|
|
|
|
|
if (!content) return;
|
|
|
|
|
|
const last = this.chatMessages[this.chatMessages.length - 1];
|
|
|
|
|
|
if (!last || last.role !== "assistant" || last.content !== content) {
|
|
|
|
|
|
this.chatMessages.push({ role: "assistant", content });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (persist) saveQiaobaoChatHistory(this.chatMessages, this.chatScope);
|
|
|
|
|
|
this.scrollChatToBottom();
|
|
|
|
|
|
},
|
|
|
|
|
|
scrollChatToBottom() {
|
|
|
|
|
|
this.$nextTick(() => { this.chatScrollTop += 10000; });
|
|
|
|
|
|
},
|
|
|
|
|
|
sendSuggestion(value) {
|
|
|
|
|
|
if (this.chatLoading) return;
|
|
|
|
|
|
if (value === "联系人工客服") {
|
|
|
|
|
|
this.goToCustomerService();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.chatInput = value;
|
|
|
|
|
|
this.sendMessage();
|
|
|
|
|
|
},
|
|
|
|
|
|
async sendMessage() {
|
|
|
|
|
|
const content = String(this.chatInput || "").trim();
|
|
|
|
|
|
if (!content || this.chatLoading) return;
|
|
|
|
|
|
this.chatInput = "";
|
|
|
|
|
|
this.chatMessages.push({ role: "user", content });
|
|
|
|
|
|
saveQiaobaoChatHistory(this.chatMessages, this.chatScope);
|
|
|
|
|
|
this.chatLoading = true;
|
|
|
|
|
|
this.scrollChatToBottom();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const history = this.chatMessages.slice(-9, -1).map(({ role, content: value }) => ({ role, content: value }));
|
|
|
|
|
|
const data = await chatWithQiaobao({
|
|
|
|
|
|
message: content,
|
|
|
|
|
|
pagePath: this.currentPageKey,
|
|
|
|
|
|
pageTitle: this.resolvedTitle,
|
|
|
|
|
|
sessionId: this.chatSessionId,
|
|
|
|
|
|
history,
|
|
|
|
|
|
});
|
|
|
|
|
|
this.suggestions = Array.isArray(data.suggestions) && data.suggestions.length ? data.suggestions : [...DEFAULT_SUGGESTIONS];
|
|
|
|
|
|
this.appendAssistantMessage(data.message || "桥宝暂时没理解,可以换一种方式问我。", true);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.appendAssistantMessage("网络有点忙。你可以稍后再问,或进入人工客服咨询商品、订单、物流和售后问题。", true);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.chatLoading = false;
|
|
|
|
|
|
this.scrollChatToBottom();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
goToCustomerService() {
|
|
|
|
|
|
this.closeHelp();
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: "/pages/rwa_package/setting/kefu",
|
|
|
|
|
|
fail: () => uni.navigateTo({ url: "/pages/rwa_package/kefu/kefu" }),
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-08-17 11:32:40 +08:00
|
|
|
|
initializeDragPosition() {
|
|
|
|
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
|
this.viewportWidth = Number(systemInfo.windowWidth) || 375;
|
|
|
|
|
|
this.viewportHeight = Number(systemInfo.windowHeight) || 667;
|
|
|
|
|
|
this.safeAreaTop = Number(systemInfo.safeAreaInsets?.top) || 0;
|
|
|
|
|
|
this.safeAreaBottom = Number(systemInfo.safeAreaInsets?.bottom) || 0;
|
|
|
|
|
|
const scale = this.viewportWidth / 750;
|
|
|
|
|
|
this.petWidth = 164 * scale;
|
|
|
|
|
|
this.petHeight = 176 * scale;
|
|
|
|
|
|
this.dockHeight = 126 * scale;
|
|
|
|
|
|
const storedPosition = getQiaobaoPosition();
|
|
|
|
|
|
const defaultPosition = {
|
|
|
|
|
|
x: this.viewportWidth - this.petWidth - 10,
|
|
|
|
|
|
y: this.viewportHeight - this.safeAreaBottom - this.petHeight - 66,
|
|
|
|
|
|
};
|
|
|
|
|
|
this.position = this.clampPosition(storedPosition || defaultPosition);
|
|
|
|
|
|
},
|
|
|
|
|
|
clampPosition(position) {
|
|
|
|
|
|
const minX = 0;
|
|
|
|
|
|
const maxX = Math.max(0, this.viewportWidth - this.petWidth);
|
|
|
|
|
|
const minY = this.safeAreaTop + 4;
|
|
|
|
|
|
const maxY = Math.max(minY, this.viewportHeight - this.safeAreaBottom - this.petHeight - 4);
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: Math.min(maxX, Math.max(minX, Number(position?.x) || 0)),
|
|
|
|
|
|
y: Math.min(maxY, Math.max(minY, Number(position?.y) || minY)),
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
getTouchPoint(event) {
|
|
|
|
|
|
const touch = event?.touches?.[0] || event?.changedTouches?.[0];
|
|
|
|
|
|
if (!touch) return null;
|
|
|
|
|
|
return { x: Number(touch.clientX), y: Number(touch.clientY) };
|
|
|
|
|
|
},
|
|
|
|
|
|
getMousePoint(event) {
|
|
|
|
|
|
if (!event || !Number.isFinite(Number(event.clientX))) return null;
|
|
|
|
|
|
return { x: Number(event.clientX), y: Number(event.clientY) };
|
|
|
|
|
|
},
|
|
|
|
|
|
startDragging(point) {
|
|
|
|
|
|
if (!point || this.panelVisible) return;
|
|
|
|
|
|
this.dragging = true;
|
|
|
|
|
|
this.dragMoved = false;
|
|
|
|
|
|
this.dragStartPoint = point;
|
|
|
|
|
|
this.dragOrigin = { ...(this.position || { x: 0, y: 0 }) };
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDragStart(event) {
|
|
|
|
|
|
this.startDragging(this.getTouchPoint(event));
|
|
|
|
|
|
},
|
|
|
|
|
|
handleMouseDragStart(event) {
|
|
|
|
|
|
this.startDragging(this.getMousePoint(event));
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDragMove(event) {
|
|
|
|
|
|
if (!this.dragging) return;
|
|
|
|
|
|
const point = this.getTouchPoint(event);
|
|
|
|
|
|
if (!point) return;
|
|
|
|
|
|
const deltaX = point.x - this.dragStartPoint.x;
|
|
|
|
|
|
const deltaY = point.y - this.dragStartPoint.y;
|
|
|
|
|
|
if (!this.dragMoved && Math.hypot(deltaX, deltaY) > 6) {
|
|
|
|
|
|
this.dragMoved = true;
|
|
|
|
|
|
this.longPressTriggered = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.position = this.clampPosition({
|
|
|
|
|
|
x: this.dragOrigin.x + deltaX,
|
|
|
|
|
|
y: this.dragOrigin.y + deltaY,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
handleMouseDragMove(event) {
|
|
|
|
|
|
if (this.dockDragging) {
|
|
|
|
|
|
this.moveDockDragging(this.getMousePoint(event));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.dragging) return;
|
|
|
|
|
|
const point = this.getMousePoint(event);
|
|
|
|
|
|
if (!point) return;
|
|
|
|
|
|
const deltaX = point.x - this.dragStartPoint.x;
|
|
|
|
|
|
const deltaY = point.y - this.dragStartPoint.y;
|
|
|
|
|
|
if (!this.dragMoved && Math.hypot(deltaX, deltaY) > 6) this.dragMoved = true;
|
|
|
|
|
|
this.position = this.clampPosition({
|
|
|
|
|
|
x: this.dragOrigin.x + deltaX,
|
|
|
|
|
|
y: this.dragOrigin.y + deltaY,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
handleMouseDragEnd() {
|
|
|
|
|
|
if (this.dockDragging) {
|
|
|
|
|
|
this.handleDockDragEnd();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.handleDragEnd();
|
|
|
|
|
|
},
|
|
|
|
|
|
startDockDragging(point) {
|
|
|
|
|
|
if (!point) return;
|
|
|
|
|
|
this.dockDragging = true;
|
|
|
|
|
|
this.dockDragMoved = false;
|
|
|
|
|
|
this.dockDragStartPoint = point;
|
|
|
|
|
|
this.dockDragOriginY = this.position ? this.position.y : this.safeAreaTop + 120;
|
|
|
|
|
|
this.dockPointerX = point.x;
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDockDragStart(event) {
|
|
|
|
|
|
this.startDockDragging(this.getTouchPoint(event));
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDockMouseDragStart(event) {
|
|
|
|
|
|
this.startDockDragging(this.getMousePoint(event));
|
|
|
|
|
|
},
|
|
|
|
|
|
moveDockDragging(point) {
|
|
|
|
|
|
if (!this.dockDragging || !point) return;
|
|
|
|
|
|
const deltaX = point.x - this.dockDragStartPoint.x;
|
|
|
|
|
|
const deltaY = point.y - this.dockDragStartPoint.y;
|
|
|
|
|
|
if (!this.dockDragMoved && Math.hypot(deltaX, deltaY) > 5) {
|
|
|
|
|
|
this.dockDragMoved = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.dockPointerX = point.x;
|
|
|
|
|
|
this.collapsedEdge = point.x < this.viewportWidth / 2 ? "left" : "right";
|
|
|
|
|
|
this.position = this.clampPosition({
|
|
|
|
|
|
x: this.position ? this.position.x : 0,
|
|
|
|
|
|
y: this.dockDragOriginY + deltaY,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDockDragMove(event) {
|
|
|
|
|
|
this.moveDockDragging(this.getTouchPoint(event));
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDockDragEnd() {
|
|
|
|
|
|
if (!this.dockDragging) return;
|
|
|
|
|
|
this.dockDragging = false;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (!this.dockDragMoved) {
|
|
|
|
|
|
this.restoreFromDock();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-17 11:32:40 +08:00
|
|
|
|
setQiaobaoDockEdge(this.collapsedEdge);
|
|
|
|
|
|
saveQiaobaoPosition(this.position);
|
|
|
|
|
|
this.dockDragJustEnded = true;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.dockDragJustEnded = false;
|
|
|
|
|
|
this.dockDragMoved = false;
|
|
|
|
|
|
}, 280);
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDragEnd() {
|
|
|
|
|
|
if (!this.dragging) return;
|
|
|
|
|
|
this.dragging = false;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (!this.dragMoved) {
|
|
|
|
|
|
if (!this.longPressTriggered) {
|
|
|
|
|
|
this.openHelp();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.longPressTriggered = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-17 11:32:40 +08:00
|
|
|
|
this.position = this.clampPosition(this.position);
|
|
|
|
|
|
saveQiaobaoPosition(this.position);
|
|
|
|
|
|
const edgeThreshold = Math.max(22, this.viewportWidth * 0.06);
|
|
|
|
|
|
if (this.position.x <= edgeThreshold) {
|
|
|
|
|
|
hideQiaobaoGlobally("left");
|
|
|
|
|
|
} else if (this.position.x + this.petWidth >= this.viewportWidth - edgeThreshold) {
|
|
|
|
|
|
hideQiaobaoGlobally("right");
|
|
|
|
|
|
}
|
|
|
|
|
|
this.dragJustEnded = true;
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.dragJustEnded = false;
|
|
|
|
|
|
this.dragMoved = false;
|
|
|
|
|
|
}, 280);
|
|
|
|
|
|
},
|
|
|
|
|
|
getNearestEdge() {
|
|
|
|
|
|
if (!this.position) return "right";
|
|
|
|
|
|
return this.position.x + this.petWidth / 2 < this.viewportWidth / 2 ? "left" : "right";
|
|
|
|
|
|
},
|
|
|
|
|
|
refreshVisibility() {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (isQiaobaoClosedForSession()) {
|
|
|
|
|
|
this.visible = false;
|
|
|
|
|
|
this.panelVisible = false;
|
|
|
|
|
|
this.clearTimers();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-17 11:32:40 +08:00
|
|
|
|
this.visible = true;
|
|
|
|
|
|
this.collapsed = isQiaobaoHidden(this.currentPageKey);
|
|
|
|
|
|
this.collapsedEdge = getQiaobaoDockEdge();
|
|
|
|
|
|
if (this.collapsed) {
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
this.clearTimers();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.preloadImages();
|
|
|
|
|
|
this.startMotion();
|
|
|
|
|
|
if (this.bubble) {
|
|
|
|
|
|
this.showBubble = true;
|
|
|
|
|
|
this.bubbleTimer = setTimeout(() => {
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleVisibilityChange({ scope, pagePath, edge } = {}) {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (scope === "close") {
|
|
|
|
|
|
this.visible = false;
|
|
|
|
|
|
this.panelVisible = false;
|
|
|
|
|
|
this.clearTimers();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-17 11:32:40 +08:00
|
|
|
|
if (scope === "all") this.collapseToEdge(edge);
|
|
|
|
|
|
if (scope === "page" && pagePath === this.currentPageKey) this.collapseToEdge(edge);
|
|
|
|
|
|
if (scope === "show" && pagePath === this.currentPageKey) this.expandFromEdge();
|
|
|
|
|
|
if (scope === "reset") this.refreshVisibility();
|
|
|
|
|
|
},
|
|
|
|
|
|
preloadImages() {
|
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
Object.values(ACTION_IMAGES).forEach((src) => {
|
|
|
|
|
|
const image = new Image();
|
|
|
|
|
|
image.src = src;
|
|
|
|
|
|
});
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
startMotion() {
|
|
|
|
|
|
this.clearActionTimer();
|
|
|
|
|
|
const nextDelay = 3200 + Math.floor(Math.random() * 2600);
|
|
|
|
|
|
this.actionTimer = setTimeout(() => {
|
|
|
|
|
|
const shouldScratch = Math.random() > 0.72;
|
|
|
|
|
|
this.action = shouldScratch ? "scratch" : "blink";
|
|
|
|
|
|
const duration = shouldScratch ? 1450 : 180;
|
|
|
|
|
|
this.actionTimer = setTimeout(() => {
|
|
|
|
|
|
this.action = "idle";
|
|
|
|
|
|
this.startMotion();
|
|
|
|
|
|
}, duration);
|
|
|
|
|
|
}, nextDelay);
|
|
|
|
|
|
},
|
|
|
|
|
|
openHelp() {
|
|
|
|
|
|
if (this.dragJustEnded) return;
|
|
|
|
|
|
if (this.longPressTriggered) {
|
|
|
|
|
|
this.longPressTriggered = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
this.panelVisible = true;
|
|
|
|
|
|
this.action = "blink";
|
|
|
|
|
|
this.$emit("open", this.currentPageKey);
|
|
|
|
|
|
},
|
|
|
|
|
|
closeHelp() {
|
|
|
|
|
|
this.panelVisible = false;
|
|
|
|
|
|
this.action = "idle";
|
|
|
|
|
|
this.$emit("close", this.currentPageKey);
|
|
|
|
|
|
},
|
|
|
|
|
|
openHideMenu() {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
// longpress is emitted after touchstart, so `dragging` is normally true here.
|
|
|
|
|
|
// Only an actual move should cancel the hide action.
|
|
|
|
|
|
if (this.dragMoved || this.dockDragging) return;
|
|
|
|
|
|
this.dragging = false;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
this.longPressTriggered = true;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
this.showBubble = false;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
const edge = this.getNearestEdge();
|
2026-08-26 17:00:58 +08:00
|
|
|
|
uni.vibrateShort?.({ type: "light" });
|
2026-08-17 11:32:40 +08:00
|
|
|
|
uni.showActionSheet({
|
2026-08-26 17:00:58 +08:00
|
|
|
|
title: "桥宝操作",
|
|
|
|
|
|
itemList: ["仅在当前页面隐藏", "在所有页面隐藏", "关闭桥宝"],
|
2026-08-17 11:32:40 +08:00
|
|
|
|
success: ({ tapIndex }) => {
|
|
|
|
|
|
if (tapIndex === 0) hideQiaobaoForPage(this.currentPageKey, edge);
|
|
|
|
|
|
if (tapIndex === 1) hideQiaobaoGlobally(edge);
|
2026-08-26 17:00:58 +08:00
|
|
|
|
if (tapIndex === 2) this.closeQiaobao();
|
|
|
|
|
|
if (tapIndex < 2) this.$emit("hide", tapIndex === 1 ? "all" : "page");
|
2026-08-17 11:32:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
complete: () => {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.longPressTriggered = false;
|
|
|
|
|
|
}, 350);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-08-26 17:00:58 +08:00
|
|
|
|
closeQiaobao() {
|
|
|
|
|
|
this.panelVisible = false;
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
this.action = "idle";
|
|
|
|
|
|
this.clearTimers();
|
|
|
|
|
|
closeQiaobaoForSession();
|
|
|
|
|
|
this.$emit("hide", "close");
|
|
|
|
|
|
},
|
2026-08-17 11:32:40 +08:00
|
|
|
|
collapseToEdge(edge = getQiaobaoDockEdge()) {
|
|
|
|
|
|
this.collapsedEdge = edge === "left" ? "left" : "right";
|
|
|
|
|
|
this.collapsed = true;
|
|
|
|
|
|
this.panelVisible = false;
|
|
|
|
|
|
this.showBubble = false;
|
|
|
|
|
|
this.action = "idle";
|
|
|
|
|
|
this.clearTimers();
|
|
|
|
|
|
},
|
|
|
|
|
|
expandFromEdge() {
|
|
|
|
|
|
this.collapsed = false;
|
|
|
|
|
|
const edgeX = this.collapsedEdge === "left"
|
|
|
|
|
|
? 12
|
|
|
|
|
|
: Math.max(12, this.viewportWidth - this.petWidth - 12);
|
|
|
|
|
|
const currentY = this.position ? this.position.y : this.safeAreaTop + 120;
|
|
|
|
|
|
this.position = this.clampPosition({ x: edgeX, y: currentY });
|
|
|
|
|
|
saveQiaobaoPosition(this.position);
|
|
|
|
|
|
this.action = "blink";
|
|
|
|
|
|
this.preloadImages();
|
|
|
|
|
|
this.startMotion();
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!this.collapsed) this.action = "idle";
|
|
|
|
|
|
}, 180);
|
|
|
|
|
|
},
|
|
|
|
|
|
restoreFromDock() {
|
|
|
|
|
|
if (this.dockDragJustEnded) return;
|
|
|
|
|
|
showQiaobao(this.currentPageKey);
|
|
|
|
|
|
},
|
|
|
|
|
|
show() {
|
|
|
|
|
|
this.visible = true;
|
|
|
|
|
|
showQiaobao(this.currentPageKey);
|
|
|
|
|
|
},
|
|
|
|
|
|
clearActionTimer() {
|
|
|
|
|
|
if (this.actionTimer) clearTimeout(this.actionTimer);
|
|
|
|
|
|
this.actionTimer = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
clearTimers() {
|
|
|
|
|
|
this.clearActionTimer();
|
|
|
|
|
|
if (this.bubbleTimer) clearTimeout(this.bubbleTimer);
|
|
|
|
|
|
this.bubbleTimer = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.qiaobao-assistant {
|
|
|
|
|
|
position: relative;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
z-index: 980;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-edge-tab {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: -18rpx;
|
|
|
|
|
|
bottom: calc(150rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
width: 132rpx;
|
|
|
|
|
|
height: 126rpx;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
z-index: 981;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
overflow: visible;
|
|
|
|
|
|
animation: qiaobao-edge-in-right .22s cubic-bezier(.22, 1, .36, 1) both;
|
|
|
|
|
|
-webkit-tap-highlight-color: transparent;
|
|
|
|
|
|
touch-action: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-edge-tab.is-left {
|
|
|
|
|
|
left: -18rpx;
|
|
|
|
|
|
right: auto;
|
|
|
|
|
|
animation-name: qiaobao-edge-in-left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-edge-tab.is-dragging {
|
|
|
|
|
|
animation: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-edge-head {
|
|
|
|
|
|
width: 132rpx;
|
|
|
|
|
|
height: 132rpx;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transform-origin: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-edge-tab.is-left .qiaobao-edge-head {
|
|
|
|
|
|
transform: scaleX(-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-float {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 20rpx;
|
|
|
|
|
|
bottom: calc(132rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
width: 164rpx;
|
|
|
|
|
|
height: 176rpx;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
z-index: 981;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
transform-origin: 50% 90%;
|
|
|
|
|
|
-webkit-tap-highlight-color: transparent;
|
|
|
|
|
|
touch-action: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-image {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
width: 164rpx;
|
|
|
|
|
|
height: 164rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-shadow {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 28rpx;
|
|
|
|
|
|
right: 22rpx;
|
|
|
|
|
|
bottom: 3rpx;
|
|
|
|
|
|
height: 16rpx;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: rgba(41, 21, 77, .2);
|
|
|
|
|
|
filter: blur(6rpx);
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-bubble {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 132rpx;
|
|
|
|
|
|
top: 26rpx;
|
|
|
|
|
|
width: 250rpx;
|
|
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
|
|
border-radius: 24rpx 24rpx 6rpx 24rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #302645;
|
|
|
|
|
|
box-shadow: 0 12rpx 32rpx rgba(27, 17, 51, .18);
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
line-height: 1.45;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
animation: qiaobao-bubble-in .2s ease-out both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-scratching {
|
|
|
|
|
|
animation: qiaobao-curious 1.45s ease-in-out both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-mask {
|
|
|
|
|
|
position: fixed;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 10080;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
background: rgba(20, 16, 29, .52);
|
|
|
|
|
|
animation: qiaobao-mask-in .2s ease-out both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-sheet {
|
|
|
|
|
|
width: 100%;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
height: min(84vh, 1280rpx);
|
|
|
|
|
|
max-height: 84vh;
|
|
|
|
|
|
padding: 12rpx 28rpx calc(22rpx + env(safe-area-inset-bottom));
|
2026-08-17 11:32:40 +08:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
border-radius: 36rpx 36rpx 0 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-shadow: 0 -16rpx 48rpx rgba(29, 17, 55, .14);
|
|
|
|
|
|
animation: qiaobao-sheet-in .24s ease-out both;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-handle {
|
|
|
|
|
|
width: 72rpx;
|
|
|
|
|
|
height: 8rpx;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
margin: 4rpx auto 14rpx;
|
|
|
|
|
|
flex: 0 0 auto;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
background: #ddd8e8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-sheet-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
min-height: 92rpx;
|
|
|
|
|
|
flex: 0 0 auto;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-avatar-wrap {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
width: 88rpx;
|
|
|
|
|
|
height: 88rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #f1eaff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-avatar {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
width: 82rpx;
|
|
|
|
|
|
height: 82rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-title-wrap {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
margin-left: 18rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-kicker {
|
|
|
|
|
|
color: #7b2cff;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-online-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 5rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-online-dot {
|
|
|
|
|
|
width: 12rpx;
|
|
|
|
|
|
height: 12rpx;
|
|
|
|
|
|
margin-right: 10rpx;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #39ba76;
|
|
|
|
|
|
box-shadow: 0 0 0 6rpx rgba(57, 186, 118, .12);
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-title {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
color: #17131f;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
font-size: 30rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
line-height: 1.25;
|
|
|
|
|
|
font-weight: 700;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-close {
|
2026-08-26 17:00:58 +08:00
|
|
|
|
width: 58rpx;
|
|
|
|
|
|
height: 58rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
margin-left: 12rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
color: #6c6674;
|
|
|
|
|
|
background: #f4f2f7;
|
|
|
|
|
|
font-size: 46rpx;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
.qiaobao-page-context {
|
2026-08-17 11:32:40 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
margin: 12rpx 0 10rpx;
|
|
|
|
|
|
padding: 13rpx 18rpx;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
background: #f7f3ff;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
.qiaobao-context-label {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
color: #8e77b7;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-context-title {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
margin-left: 14rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
color: #392857;
|
|
|
|
|
|
font-size: 23rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-chat {
|
|
|
|
|
|
min-height: 0;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
flex: 1;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
padding: 10rpx 0;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
.qiaobao-chat-spacer {
|
|
|
|
|
|
height: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-message {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
margin: 12rpx 0;
|
|
|
|
|
|
padding-right: 54rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-message.is-user {
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: 0 0 0 54rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-message-avatar {
|
|
|
|
|
|
width: 54rpx;
|
|
|
|
|
|
height: 54rpx;
|
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
background: #f0e8ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-message-bubble {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
padding: 17rpx 20rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
border-radius: 10rpx 24rpx 24rpx 24rpx;
|
|
|
|
|
|
color: #292431;
|
|
|
|
|
|
background: #f4f1f7;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-message.is-user .qiaobao-message-bubble {
|
|
|
|
|
|
border-radius: 24rpx 10rpx 24rpx 24rpx;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #7433f1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
.qiaobao-message-bubble.is-typing {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
color: #756c80;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-dots {
|
|
|
|
|
|
margin-left: 8rpx;
|
|
|
|
|
|
color: #7b2cff;
|
|
|
|
|
|
letter-spacing: 3rpx;
|
|
|
|
|
|
animation: qiaobao-dots 1s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-suggestions {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
margin: 2rpx 0 12rpx;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-suggestion-row {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
padding-right: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-suggestion {
|
|
|
|
|
|
height: 56rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0 22rpx;
|
|
|
|
|
|
border: 1rpx solid #ddcff8;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
color: #6430bd;
|
|
|
|
|
|
background: #faf8ff;
|
|
|
|
|
|
font-size: 23rpx;
|
|
|
|
|
|
line-height: 54rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-suggestion::after {
|
2026-08-17 11:32:40 +08:00
|
|
|
|
border: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
.qiaobao-compose {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 10rpx;
|
|
|
|
|
|
border: 1rpx solid #e9e4ef;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
background: #f8f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-input {
|
|
|
|
|
|
height: 66rpx;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 0 14rpx;
|
|
|
|
|
|
color: #25202c;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-send {
|
|
|
|
|
|
width: 112rpx;
|
|
|
|
|
|
height: 62rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #7433f1;
|
|
|
|
|
|
font-size: 25rpx;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
line-height: 62rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-send[disabled] {
|
|
|
|
|
|
color: #a9a3af;
|
|
|
|
|
|
background: #e7e3eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-send::after {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-sheet-foot {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 14rpx 4rpx 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 11:32:40 +08:00
|
|
|
|
.qiaobao-hide-hint {
|
|
|
|
|
|
color: #9a94a3;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-human-link {
|
|
|
|
|
|
color: #7034dc;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
font-size: 22rpx;
|
2026-08-26 17:00:58 +08:00
|
|
|
|
font-weight: 600;
|
2026-08-17 11:32:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes qiaobao-bubble-in {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(8rpx) scale(.96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes qiaobao-mask-in {
|
|
|
|
|
|
from {
|
|
|
|
|
|
background: rgba(20, 16, 29, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
background: rgba(20, 16, 29, .52);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes qiaobao-sheet-in {
|
|
|
|
|
|
from {
|
|
|
|
|
|
transform: translateY(100%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes qiaobao-curious {
|
|
|
|
|
|
|
|
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
transform: rotate(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
32% {
|
|
|
|
|
|
transform: rotate(2deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
68% {
|
|
|
|
|
|
transform: rotate(-1deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:00:58 +08:00
|
|
|
|
@keyframes qiaobao-dots {
|
|
|
|
|
|
|
|
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
opacity: .35;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
50% {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 11:32:40 +08:00
|
|
|
|
@keyframes qiaobao-edge-in-right {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(54rpx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes qiaobao-edge-in-left {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-54rpx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
|
|
|
|
|
|
.qiaobao-bubble,
|
|
|
|
|
|
.qiaobao-mask,
|
|
|
|
|
|
.qiaobao-sheet,
|
|
|
|
|
|
.qiaobao-edge-tab,
|
|
|
|
|
|
.is-scratching {
|
|
|
|
|
|
animation: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|