mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-24 17:13:53 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ae739a4d | ||
|
|
d9cbcc2991 | ||
|
|
ad12701fe2 | ||
|
|
2b426a47c6 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.13.14",
|
||||
"version": "2.13.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onAgentAssistantNotificationBubble,
|
||||
onAgentAssistantBubble,
|
||||
setAgentAssistantBubbleEntryActive,
|
||||
type AgentAssistantBubbleKind,
|
||||
type AgentAssistantBubblePayload,
|
||||
type AgentAssistantBubbleVariant,
|
||||
type AgentAssistantNotificationBubblePayload,
|
||||
} from '@/utils/agentAssistantBubble'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
type AgentAssistantEntryBubbleKind = 'assistant' | 'custom' | 'notification'
|
||||
|
||||
interface AgentAssistantEntryBubble {
|
||||
id: string
|
||||
kind: AgentAssistantEntryBubbleKind
|
||||
kind: AgentAssistantBubbleKind
|
||||
variant: AgentAssistantBubbleVariant
|
||||
title?: string
|
||||
text: string
|
||||
keepOpen?: boolean
|
||||
@@ -17,7 +20,8 @@ interface AgentAssistantEntryBubble {
|
||||
|
||||
interface AgentAssistantEntryBubbleInput {
|
||||
id?: string
|
||||
kind?: AgentAssistantEntryBubbleKind
|
||||
kind?: AgentAssistantBubbleKind
|
||||
variant?: AgentAssistantBubbleVariant
|
||||
title?: string
|
||||
text: string
|
||||
autoClose?: boolean
|
||||
@@ -45,6 +49,7 @@ const { t } = useI18n()
|
||||
const FAB_IDLE_DOCK_DELAY = 4200
|
||||
const FAB_RIGHT_EDGE_DOCK_DISTANCE = 88
|
||||
const FAB_NOTIFICATION_BUBBLE_DURATION = 7000
|
||||
const FAB_TOAST_BUBBLE_DURATION = 4500
|
||||
const FAB_MAX_BUBBLES = 4
|
||||
const FAB_DEFAULT_RIGHT_OFFSET = 18
|
||||
const FAB_DEFAULT_VERTICAL_RATIO = 2 / 3
|
||||
@@ -128,7 +133,7 @@ let fabPendingPointerPoint: FabPointerPoint | null = null
|
||||
let fabLastRandomAction: FabRandomAction | null = null
|
||||
let fabRandomActionTimer: number | null = null
|
||||
let fabRandomActionEndTimer: number | null = null
|
||||
let stopNotificationBubbleListener: (() => void) | null = null
|
||||
let stopBubbleListener: (() => void) | null = null
|
||||
|
||||
const fabBubbleTimers = new Map<string, number>()
|
||||
|
||||
@@ -377,7 +382,10 @@ function pauseFabAutoDock() {
|
||||
|
||||
// 返回下一次趣味动作的随机等待时间,让动作出现节奏更自然。
|
||||
function getFabRandomActionDelay() {
|
||||
return FAB_RANDOM_ACTION_MIN_DELAY + Math.round(Math.random() * (FAB_RANDOM_ACTION_MAX_DELAY - FAB_RANDOM_ACTION_MIN_DELAY))
|
||||
return (
|
||||
FAB_RANDOM_ACTION_MIN_DELAY +
|
||||
Math.round(Math.random() * (FAB_RANDOM_ACTION_MAX_DELAY - FAB_RANDOM_ACTION_MIN_DELAY))
|
||||
)
|
||||
}
|
||||
|
||||
// 判断当前交互状态是否适合播放随机动作,避免干扰半隐藏、拖拽和思考态。
|
||||
@@ -456,7 +464,8 @@ function runFabRandomAction() {
|
||||
// 根据当前显示和交互状态同步随机动作队列。
|
||||
function syncFabRandomActionSchedule() {
|
||||
if (canRunFabRandomAction()) {
|
||||
if (!fabRandomAction.value && fabRandomActionTimer === null && fabRandomActionEndTimer === null) scheduleFabRandomAction()
|
||||
if (!fabRandomAction.value && fabRandomActionTimer === null && fabRandomActionEndTimer === null)
|
||||
scheduleFabRandomAction()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -483,6 +492,36 @@ function buildNotificationBubbleText(payload: AgentAssistantNotificationBubblePa
|
||||
return stripMarkdownPreview(payload.text || payload.title || payload.source || payload.mtype || '')
|
||||
}
|
||||
|
||||
function getBubbleVariant(payload: AgentAssistantBubblePayload): AgentAssistantBubbleVariant {
|
||||
return payload.variant || 'default'
|
||||
}
|
||||
|
||||
function getBubbleIcon(variant: AgentAssistantBubbleVariant) {
|
||||
const icons: Record<AgentAssistantBubbleVariant, string> = {
|
||||
default: 'mdi-bell-outline',
|
||||
error: 'mdi-alert-circle-outline',
|
||||
info: 'mdi-information-outline',
|
||||
success: 'mdi-check-circle-outline',
|
||||
warning: 'mdi-alert-outline',
|
||||
}
|
||||
|
||||
return icons[variant]
|
||||
}
|
||||
|
||||
function getToastBubbleTitle(payload: AgentAssistantBubblePayload) {
|
||||
if (payload.title) return payload.title
|
||||
|
||||
const titles: Record<AgentAssistantBubbleVariant, string> = {
|
||||
default: t('common.notice'),
|
||||
error: t('common.error'),
|
||||
info: t('common.notice'),
|
||||
success: t('common.success'),
|
||||
warning: t('common.notice'),
|
||||
}
|
||||
|
||||
return titles[getBubbleVariant(payload)]
|
||||
}
|
||||
|
||||
function clearFabBubbleTimer(id: string) {
|
||||
const timer = fabBubbleTimers.get(id)
|
||||
if (!timer) return
|
||||
@@ -525,6 +564,7 @@ function showBubble(input: AgentAssistantEntryBubbleInput) {
|
||||
{
|
||||
id: input.id || createBubbleId(input.kind || 'custom'),
|
||||
kind: input.kind || 'custom',
|
||||
variant: input.variant || 'default',
|
||||
title: input.title,
|
||||
text,
|
||||
keepOpen: input.keepOpen,
|
||||
@@ -551,6 +591,7 @@ function showNotificationBubble(payload: AgentAssistantNotificationBubblePayload
|
||||
showBubble({
|
||||
id: payload.id,
|
||||
kind: 'notification',
|
||||
variant: getBubbleVariant(payload),
|
||||
title: buildNotificationBubbleTitle(payload),
|
||||
text,
|
||||
autoClose: true,
|
||||
@@ -558,6 +599,31 @@ function showNotificationBubble(payload: AgentAssistantNotificationBubblePayload
|
||||
})
|
||||
}
|
||||
|
||||
function showToastBubble(payload: AgentAssistantBubblePayload) {
|
||||
const text = stripMarkdownPreview(payload.text || payload.title || '')
|
||||
if (!text) return
|
||||
|
||||
showBubble({
|
||||
id: payload.id,
|
||||
kind: 'toast',
|
||||
variant: getBubbleVariant(payload),
|
||||
title: getToastBubbleTitle(payload),
|
||||
text,
|
||||
autoClose: true,
|
||||
duration: payload.duration || FAB_TOAST_BUBBLE_DURATION,
|
||||
keepOpen: payload.keepOpen,
|
||||
})
|
||||
}
|
||||
|
||||
function showAgentAssistantBubble(payload: AgentAssistantBubblePayload) {
|
||||
if ((payload.kind || 'notification') === 'toast') {
|
||||
showToastBubble(payload)
|
||||
return
|
||||
}
|
||||
|
||||
showNotificationBubble(payload as AgentAssistantNotificationBubblePayload)
|
||||
}
|
||||
|
||||
function closeBubble(id?: string) {
|
||||
if (id) {
|
||||
clearFabBubbleTimer(id)
|
||||
@@ -596,7 +662,10 @@ function setFabDocked(docked: boolean) {
|
||||
|
||||
updateFabPosition({
|
||||
...currentPosition,
|
||||
x: Math.min(currentPosition.x, Math.max(0, getViewportSize().width - getOpenFabSize().width - FAB_DEFAULT_RIGHT_OFFSET)),
|
||||
x: Math.min(
|
||||
currentPosition.x,
|
||||
Math.max(0, getViewportSize().width - getOpenFabSize().width - FAB_DEFAULT_RIGHT_OFFSET),
|
||||
),
|
||||
})
|
||||
scheduleFabAutoDock()
|
||||
}
|
||||
@@ -614,7 +683,6 @@ function handleFabTriggerPointerDown(event: PointerEvent) {
|
||||
startY: currentPosition.y,
|
||||
moved: false,
|
||||
}
|
||||
|
||||
;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
|
||||
}
|
||||
|
||||
@@ -684,16 +752,19 @@ function handleFabPointerEnter() {
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(resetFabPosition)
|
||||
setAgentAssistantBubbleEntryActive(props.active)
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
window.addEventListener('pointermove', handleGlobalFabPointer, { passive: true })
|
||||
window.addEventListener('pointerdown', handleGlobalFabPointer, { passive: true })
|
||||
stopNotificationBubbleListener = onAgentAssistantNotificationBubble(showNotificationBubble)
|
||||
stopBubbleListener = onAgentAssistantBubble(showAgentAssistantBubble)
|
||||
scheduleFabRandomAction()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
active => {
|
||||
setAgentAssistantBubbleEntryActive(active)
|
||||
|
||||
if (active) {
|
||||
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
||||
return
|
||||
@@ -712,8 +783,9 @@ onScopeDispose(clearFabIdleTimer)
|
||||
onScopeDispose(clearFabRandomAction)
|
||||
onScopeDispose(resetFabBubbles)
|
||||
onScopeDispose(() => {
|
||||
stopNotificationBubbleListener?.()
|
||||
stopNotificationBubbleListener = null
|
||||
setAgentAssistantBubbleEntryActive(false)
|
||||
stopBubbleListener?.()
|
||||
stopBubbleListener = null
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
teardownFabPointerTracking()
|
||||
})
|
||||
@@ -725,6 +797,7 @@ defineExpose({
|
||||
showAssistantReplyPreview,
|
||||
showBubble,
|
||||
showNotificationBubble,
|
||||
showToastBubble,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -750,10 +823,13 @@ defineExpose({
|
||||
v-for="bubble in fabBubbles"
|
||||
:key="bubble.id"
|
||||
class="agent-assistant-fab__bubble"
|
||||
:class="`agent-assistant-fab__bubble--${bubble.kind}`"
|
||||
:class="[`agent-assistant-fab__bubble--${bubble.kind}`, `agent-assistant-fab__bubble--${bubble.variant}`]"
|
||||
role="status"
|
||||
>
|
||||
<strong v-if="bubble.title">{{ bubble.title }}</strong>
|
||||
<strong v-if="bubble.title" class="agent-assistant-fab__bubble-title">
|
||||
<VIcon class="agent-assistant-fab__bubble-icon" :icon="getBubbleIcon(bubble.variant)" size="20" />
|
||||
<span>{{ bubble.title }}</span>
|
||||
</strong>
|
||||
<span>{{ bubble.text }}</span>
|
||||
<button
|
||||
class="agent-assistant-fab__bubble-close"
|
||||
@@ -801,6 +877,7 @@ defineExpose({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
|
||||
.agent-assistant-fab {
|
||||
position: fixed;
|
||||
@@ -886,14 +963,14 @@ defineExpose({
|
||||
.agent-assistant-fab__bubbles {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
overflow: visible;
|
||||
gap: 0.45rem;
|
||||
inline-size: 13.2rem;
|
||||
inline-size: clamp(15.5rem, 22vw, 19rem);
|
||||
inset-block-end: 4.45rem;
|
||||
inset-inline-end: 2.75rem;
|
||||
max-block-size: min(22rem, calc(100vh - 8rem));
|
||||
max-block-size: min(34rem, calc(100vh - 8rem));
|
||||
max-inline-size: calc(100vw - 6.4rem);
|
||||
opacity: 0;
|
||||
overflow-y: auto;
|
||||
pointer-events: none;
|
||||
transform: translateY(0.22rem) scale(0.96);
|
||||
transform-origin: 100% 100%;
|
||||
@@ -905,6 +982,10 @@ defineExpose({
|
||||
.agent-assistant-fab__bubble {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
||||
--agent-assistant-bubble-accent: var(--v-theme-primary);
|
||||
--agent-assistant-bubble-accent-rgb: var(--v-theme-primary);
|
||||
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-radius: 18px;
|
||||
backdrop-filter: blur(12px);
|
||||
@@ -921,26 +1002,60 @@ defineExpose({
|
||||
linear-gradient(135deg, rgba(var(--v-theme-primary), 0.1), transparent 48%), rgba(var(--v-theme-surface), 0.94);
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble strong {
|
||||
.agent-assistant-fab__bubble--success {
|
||||
--agent-assistant-bubble-accent-rgb: var(--v-theme-success);
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble--error {
|
||||
--agent-assistant-bubble-accent-rgb: var(--v-theme-error);
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble--warning {
|
||||
--agent-assistant-bubble-accent-rgb: 245, 158, 11;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble--info {
|
||||
--agent-assistant-bubble-accent-rgb: 14, 165, 233;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble--toast {
|
||||
border-color: rgba(var(--agent-assistant-bubble-accent-rgb), 0.3);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(var(--agent-assistant-bubble-accent-rgb), 0.12), transparent 54%),
|
||||
rgba(var(--v-theme-surface), 0.95);
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble-title {
|
||||
display: inline-grid;
|
||||
overflow: hidden;
|
||||
color: rgba(var(--v-theme-primary), 0.92);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
align-items: center;
|
||||
color: rgba(var(--agent-assistant-bubble-accent-rgb), 0.92);
|
||||
column-gap: 0.32rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
line-height: 1.25;
|
||||
margin-block-end: 0.22rem;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble-title span {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble span {
|
||||
.agent-assistant-fab__bubble-icon {
|
||||
color: rgba(var(--agent-assistant-bubble-accent-rgb), 0.92) !important;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble > span {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||
font-size: 0.78rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
-webkit-line-clamp: 4;
|
||||
line-height: 1.42;
|
||||
-webkit-line-clamp: 8;
|
||||
line-height: 1.46;
|
||||
text-align: start;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -1034,12 +1149,12 @@ defineExpose({
|
||||
z-index: 3;
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
animation: agent-fab-antenna-idle 3.9s ease-in-out infinite;
|
||||
background: var(--agent-assistant-robot-outline);
|
||||
block-size: 0.66rem;
|
||||
inline-size: 0.18rem;
|
||||
inset-block-start: 0.72rem;
|
||||
inset-inline-start: 2.62rem;
|
||||
animation: agent-fab-antenna-idle 3.9s ease-in-out infinite;
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y)) rotate(22deg);
|
||||
transform-origin: bottom center;
|
||||
transition:
|
||||
@@ -1065,6 +1180,7 @@ defineExpose({
|
||||
display: block;
|
||||
border: 2px solid var(--agent-assistant-robot-outline);
|
||||
border-radius: 11px;
|
||||
animation: agent-fab-head-idle 4.6s ease-in-out infinite;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--agent-assistant-robot-shell-start) 0%,
|
||||
@@ -1077,7 +1193,6 @@ defineExpose({
|
||||
inline-size: 2.82rem;
|
||||
inset-block-start: 1.42rem;
|
||||
inset-inline-start: 0.88rem;
|
||||
animation: agent-fab-head-idle 4.6s ease-in-out infinite;
|
||||
transform: translate(var(--agent-assistant-head-x), var(--agent-assistant-head-y));
|
||||
transform-origin: 50% 85%;
|
||||
}
|
||||
@@ -1085,6 +1200,7 @@ defineExpose({
|
||||
.agent-assistant-fab__face {
|
||||
position: absolute;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--agent-assistant-robot-outline-soft);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(
|
||||
@@ -1097,7 +1213,6 @@ defineExpose({
|
||||
inline-size: 2.1rem;
|
||||
inset-block-start: 0.33rem;
|
||||
inset-inline-start: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__eye {
|
||||
@@ -1110,6 +1225,7 @@ defineExpose({
|
||||
inline-size: 0.42rem;
|
||||
inset-block-start: 0.36rem;
|
||||
transform: translate(var(--agent-assistant-eye-x), var(--agent-assistant-eye-y));
|
||||
|
||||
/* 触屏设备没有连续 hover 轨迹,给眼神位移补过渡避免点按时瞬移。 */
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
@@ -1125,9 +1241,9 @@ defineExpose({
|
||||
.agent-assistant-fab__smile {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-block-end: 0.13rem solid var(--agent-assistant-robot-eye);
|
||||
border-radius: 0 0 999px 999px;
|
||||
block-size: 0.32rem;
|
||||
border-block-end: 0.13rem solid var(--agent-assistant-robot-eye);
|
||||
inline-size: 0.7rem;
|
||||
inset-block-start: 0.75rem;
|
||||
inset-inline-start: 50%;
|
||||
@@ -1142,6 +1258,7 @@ defineExpose({
|
||||
display: block;
|
||||
border: 2px solid var(--agent-assistant-robot-outline);
|
||||
border-radius: 0.65rem 0.65rem 0.55rem 0.55rem;
|
||||
animation: agent-fab-body-idle 4.2s ease-in-out infinite;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--agent-assistant-robot-shell-mid) 0%,
|
||||
@@ -1154,7 +1271,6 @@ defineExpose({
|
||||
inline-size: 1.88rem;
|
||||
inset-block-start: 3.24rem;
|
||||
inset-inline-start: 1.32rem;
|
||||
animation: agent-fab-body-idle 4.2s ease-in-out infinite;
|
||||
transform: translate(var(--agent-assistant-body-x), var(--agent-assistant-body-y));
|
||||
transform-origin: 50% 18%;
|
||||
transition:
|
||||
@@ -1271,10 +1387,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-docked .agent-assistant-fab__eye {
|
||||
transform: translate(
|
||||
calc(var(--agent-assistant-eye-x) * 0.24 - 0.22rem),
|
||||
calc(var(--agent-assistant-eye-y) * 0.24)
|
||||
);
|
||||
transform: translate(calc(var(--agent-assistant-eye-x) * 0.24 - 0.22rem), calc(var(--agent-assistant-eye-y) * 0.24));
|
||||
}
|
||||
|
||||
.agent-assistant-fab.is-docked .agent-assistant-fab__body,
|
||||
@@ -2046,23 +2159,27 @@ defineExpose({
|
||||
9%,
|
||||
37%,
|
||||
65% {
|
||||
transform: translateY(0.22rem) scale(var(--agent-assistant-bot-scale)) rotate(calc(var(--agent-assistant-robot-tilt) - 3deg));
|
||||
transform: translateY(0.22rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) - 3deg));
|
||||
}
|
||||
|
||||
20%,
|
||||
48%,
|
||||
76% {
|
||||
transform: translateY(-0.76rem) scale(var(--agent-assistant-bot-scale)) rotate(calc(var(--agent-assistant-robot-tilt) + 4deg));
|
||||
transform: translateY(-0.76rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) + 4deg));
|
||||
}
|
||||
|
||||
29%,
|
||||
57%,
|
||||
85% {
|
||||
transform: translateY(0.1rem) scale(var(--agent-assistant-bot-scale)) rotate(calc(var(--agent-assistant-robot-tilt) - 2deg));
|
||||
transform: translateY(0.1rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) - 2deg));
|
||||
}
|
||||
|
||||
92% {
|
||||
transform: translateY(-0.1rem) scale(var(--agent-assistant-bot-scale)) rotate(calc(var(--agent-assistant-robot-tilt) + 1deg));
|
||||
transform: translateY(-0.1rem) scale(var(--agent-assistant-bot-scale))
|
||||
rotate(calc(var(--agent-assistant-robot-tilt) + 1deg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2178,19 +2295,22 @@ defineExpose({
|
||||
9%,
|
||||
37%,
|
||||
65% {
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) + 0.18rem)) scaleY(0.84) rotate(-2deg);
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) + 0.18rem)) scaleY(0.84)
|
||||
rotate(-2deg);
|
||||
}
|
||||
|
||||
20%,
|
||||
48%,
|
||||
76% {
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) - 0.1rem)) scaleY(1.08) rotate(4deg);
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) - 0.1rem)) scaleY(1.08)
|
||||
rotate(4deg);
|
||||
}
|
||||
|
||||
29%,
|
||||
57%,
|
||||
85% {
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) + 0.08rem)) scaleY(0.94) rotate(-3deg);
|
||||
transform: translate(var(--agent-assistant-body-x), calc(var(--agent-assistant-body-y) + 0.08rem)) scaleY(0.94)
|
||||
rotate(-3deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2334,9 +2454,9 @@ defineExpose({
|
||||
|
||||
.agent-assistant-fab__bubbles {
|
||||
gap: 0.38rem;
|
||||
inline-size: min(9.6rem, calc(100vw - 5.6rem));
|
||||
inline-size: min(16.5rem, calc(100vw - 5.6rem));
|
||||
inset-inline-end: 2.35rem;
|
||||
max-block-size: min(18rem, calc(100vh - 9.2rem));
|
||||
max-block-size: min(30rem, calc(100vh - 9.2rem));
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble {
|
||||
@@ -2344,12 +2464,12 @@ defineExpose({
|
||||
padding-inline: 0.72rem 1.62rem;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble strong {
|
||||
font-size: 0.8rem;
|
||||
.agent-assistant-fab__bubble-title {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__bubble span {
|
||||
font-size: 0.68rem;
|
||||
.agent-assistant-fab__bubble > span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.agent-assistant-fab__trigger {
|
||||
|
||||
65
src/main.ts
65
src/main.ts
@@ -14,9 +14,14 @@ import App from '@/App.vue'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
|
||||
// 4. 其他插件和功能模块
|
||||
import Toast from 'vue-toastification'
|
||||
import Toast, { TYPE, type PluginOptions } from 'vue-toastification'
|
||||
import ConfirmDialog from '@/composables/useConfirm'
|
||||
import { configureApexChartsTheme } from '@/utils/apexCharts'
|
||||
import {
|
||||
canUseAgentAssistantBubble,
|
||||
emitAgentAssistantToastBubble,
|
||||
type AgentAssistantBubbleVariant,
|
||||
} from '@/utils/agentAssistantBubble'
|
||||
|
||||
// 5. 注册自定义组件
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
@@ -29,6 +34,8 @@ import '@/styles/main.scss'
|
||||
// 7. 状态恢复插件
|
||||
import stateRestorePlugin from '@/plugins/stateRestore'
|
||||
|
||||
type ToastFilterPayload = Parameters<NonNullable<PluginOptions['filterBeforeCreate']>>[0]
|
||||
|
||||
function runWhenBrowserIdle(callback: () => void, timeout = 1500) {
|
||||
const requestIdle = globalThis.requestIdleCallback
|
||||
if (requestIdle) {
|
||||
@@ -53,6 +60,61 @@ function loadRemoteComponentsAfterLogin() {
|
||||
})
|
||||
}
|
||||
|
||||
function shouldUseAgentAssistantToastBubble() {
|
||||
const settings = pinia.state.value.globalSettings
|
||||
if (!settings?.initialized) return false
|
||||
|
||||
return (
|
||||
settings.data?.AI_AGENT_ENABLE === true &&
|
||||
settings.data?.AI_AGENT_HIDE_ENTRY !== true &&
|
||||
canUseAgentAssistantBubble()
|
||||
)
|
||||
}
|
||||
|
||||
function getAgentAssistantToastVariant(type?: ToastFilterPayload['type']): AgentAssistantBubbleVariant {
|
||||
const variants: Record<string, AgentAssistantBubbleVariant> = {
|
||||
[TYPE.DEFAULT]: 'default',
|
||||
[TYPE.ERROR]: 'error',
|
||||
[TYPE.INFO]: 'info',
|
||||
[TYPE.SUCCESS]: 'success',
|
||||
[TYPE.WARNING]: 'warning',
|
||||
}
|
||||
|
||||
return variants[type || TYPE.DEFAULT] || 'default'
|
||||
}
|
||||
|
||||
function getToastBubbleDuration(type?: ToastFilterPayload['type'], timeout?: ToastFilterPayload['timeout']) {
|
||||
if (typeof timeout === 'number') return timeout
|
||||
if (timeout === false) return undefined
|
||||
|
||||
return type === TYPE.ERROR || type === TYPE.WARNING ? 7000 : 4500
|
||||
}
|
||||
|
||||
function getToastTextContent(content: ToastFilterPayload['content']) {
|
||||
if (typeof content === 'string') return content
|
||||
|
||||
// 组件型 toast 可能包含操作按钮或复杂布局,无法可靠转成气泡文本时继续使用原生 toast。
|
||||
return ''
|
||||
}
|
||||
|
||||
function routeToastToAgentAssistantBubble(toast: ToastFilterPayload) {
|
||||
const text = getToastTextContent(toast.content)
|
||||
if (!text || !shouldUseAgentAssistantToastBubble()) return toast
|
||||
|
||||
const variant = getAgentAssistantToastVariant(toast.type)
|
||||
|
||||
emitAgentAssistantToastBubble({
|
||||
id: `toast-${String(toast.id)}`,
|
||||
kind: 'toast',
|
||||
variant,
|
||||
text,
|
||||
duration: getToastBubbleDuration(toast.type, toast.timeout),
|
||||
keepOpen: toast.timeout === false,
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let remoteComponentsInitialized = false
|
||||
|
||||
const AsyncAceEditor = defineAsyncComponent(async () => {
|
||||
@@ -111,6 +173,7 @@ app
|
||||
.use(Toast, {
|
||||
position: 'bottom-right',
|
||||
hideProgressBar: true,
|
||||
filterBeforeCreate: routeToastToAgentAssistantBubble,
|
||||
})
|
||||
.use(ConfirmDialog)
|
||||
.use(i18n)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import type { SystemNotification } from '@/api/types'
|
||||
|
||||
const AGENT_ASSISTANT_BUBBLE_EVENT = 'agentAssistantBubble'
|
||||
let agentAssistantBubbleListenerCount = 0
|
||||
let agentAssistantBubbleEntryActive = false
|
||||
|
||||
export interface AgentAssistantNotificationBubblePayload {
|
||||
export type AgentAssistantBubbleKind = 'assistant' | 'custom' | 'notification' | 'toast'
|
||||
export type AgentAssistantBubbleVariant = 'default' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
export interface AgentAssistantBubblePayload {
|
||||
id: string
|
||||
kind?: AgentAssistantBubbleKind
|
||||
variant?: AgentAssistantBubbleVariant
|
||||
title?: string
|
||||
text?: string
|
||||
duration?: number
|
||||
keepOpen?: boolean
|
||||
type?: string
|
||||
mtype?: string
|
||||
source?: string
|
||||
@@ -13,7 +22,16 @@ export interface AgentAssistantNotificationBubblePayload {
|
||||
reg_time?: string
|
||||
}
|
||||
|
||||
interface AgentAssistantBubbleEvent extends CustomEvent<AgentAssistantNotificationBubblePayload> {}
|
||||
export interface AgentAssistantNotificationBubblePayload extends AgentAssistantBubblePayload {
|
||||
kind?: 'notification'
|
||||
}
|
||||
|
||||
export interface AgentAssistantToastBubblePayload extends AgentAssistantBubblePayload {
|
||||
kind: 'toast'
|
||||
variant: AgentAssistantBubbleVariant
|
||||
}
|
||||
|
||||
interface AgentAssistantBubbleEvent extends CustomEvent<AgentAssistantBubblePayload> {}
|
||||
|
||||
function createNotificationBubbleId(notification: SystemNotification) {
|
||||
if (notification.id) return `notification-${notification.id}`
|
||||
@@ -21,29 +39,44 @@ function createNotificationBubbleId(notification: SystemNotification) {
|
||||
return `notification-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
// 通知中心和智能助手入口没有父子关系,通过全局事件传递实时通知气泡数据。
|
||||
export function emitAgentAssistantNotificationBubble(notification: SystemNotification) {
|
||||
function emitAgentAssistantBubble(payload: AgentAssistantBubblePayload) {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<AgentAssistantNotificationBubblePayload>(AGENT_ASSISTANT_BUBBLE_EVENT, {
|
||||
detail: {
|
||||
id: createNotificationBubbleId(notification),
|
||||
title: notification.title,
|
||||
text: notification.text,
|
||||
type: notification.type,
|
||||
mtype: notification.mtype,
|
||||
source: notification.source,
|
||||
date: notification.date,
|
||||
reg_time: notification.reg_time,
|
||||
},
|
||||
new CustomEvent<AgentAssistantBubblePayload>(AGENT_ASSISTANT_BUBBLE_EVENT, {
|
||||
detail: payload,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function onAgentAssistantNotificationBubble(
|
||||
callback: (payload: AgentAssistantNotificationBubblePayload) => void,
|
||||
) {
|
||||
// 通知中心、toast 和智能助手入口没有父子关系,通过全局事件传递实时气泡数据。
|
||||
export function emitAgentAssistantNotificationBubble(notification: SystemNotification) {
|
||||
emitAgentAssistantBubble({
|
||||
id: createNotificationBubbleId(notification),
|
||||
kind: 'notification',
|
||||
title: notification.title,
|
||||
text: notification.text,
|
||||
type: notification.type,
|
||||
mtype: notification.mtype,
|
||||
source: notification.source,
|
||||
date: notification.date,
|
||||
reg_time: notification.reg_time,
|
||||
})
|
||||
}
|
||||
|
||||
export function emitAgentAssistantToastBubble(payload: AgentAssistantToastBubblePayload) {
|
||||
emitAgentAssistantBubble(payload)
|
||||
}
|
||||
|
||||
export function setAgentAssistantBubbleEntryActive(active: boolean) {
|
||||
agentAssistantBubbleEntryActive = active
|
||||
}
|
||||
|
||||
export function canUseAgentAssistantBubble() {
|
||||
return agentAssistantBubbleEntryActive && agentAssistantBubbleListenerCount > 0
|
||||
}
|
||||
|
||||
export function onAgentAssistantBubble(callback: (payload: AgentAssistantBubblePayload) => void) {
|
||||
if (typeof window === 'undefined') return () => {}
|
||||
|
||||
const handler = (event: Event) => {
|
||||
@@ -51,6 +84,18 @@ export function onAgentAssistantNotificationBubble(
|
||||
}
|
||||
|
||||
window.addEventListener(AGENT_ASSISTANT_BUBBLE_EVENT, handler)
|
||||
agentAssistantBubbleListenerCount += 1
|
||||
|
||||
return () => window.removeEventListener(AGENT_ASSISTANT_BUBBLE_EVENT, handler)
|
||||
return () => {
|
||||
window.removeEventListener(AGENT_ASSISTANT_BUBBLE_EVENT, handler)
|
||||
agentAssistantBubbleListenerCount = Math.max(0, agentAssistantBubbleListenerCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function onAgentAssistantNotificationBubble(
|
||||
callback: (payload: AgentAssistantNotificationBubblePayload) => void,
|
||||
) {
|
||||
return onAgentAssistantBubble(payload => {
|
||||
if ((payload.kind || 'notification') === 'notification') callback(payload as AgentAssistantNotificationBubblePayload)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user