Files
wecom_it_smart_desk/frontend-h5/src/components/chat/CallAgentModal.vue
T

733 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- =============================================================================
企微IT智能服务台 H5用户端呼叫坐席动画弹窗
=============================================================================
流程简化版
1. 按钮出现后点击 弹出此弹窗
2. 立即随机播放趣味动画 + 话术同时发 shake 请求
3. 发送成功 显示成功提示3秒后自动关闭
注意问题描述已在聊天中完成AI 实质性回复 >= 3 弹窗不需要再录入
七种动画场景随机选一种
1. 🙋 招手 "看这里!看我这里...我有个问题!"
2. 🪑 拍桌子 "快快快!我等不及了!"
3. 💀 劈稻草人 "这个问题不解决我就要原地爆炸了💥"
4. 🍉 砍西瓜 "IT!救我!这个问题卡住了🍉"
5. 🔔 摇铃铛 "叮叮叮!有人吗!IT 在线吗!"
6. 💣 大炮发射 "开炮!这个问题必须解决了!"
7. 🚀 导弹发射 "发射!紧急呼叫 IT 特种部队!"
============================================================================= -->
<template>
<Teleport to="body">
<Transition name="modal-fade">
<div v-if="visible" class="call-modal__overlay" @click.self="handleClose">
<Transition name="modal-zoom" appear>
<div class="call-modal call-modal--compact" v-if="visible">
<!-- ========== 动画场景 + 话术 ========== -->
<div class="call-modal__step">
<div class="call-modal__header">
<span class="call-modal__icon">🔔</span>
<h3>呼叫人工坐席帮助...</h3>
</div>
<div class="call-modal__body call-modal__body--center">
<!-- 场景SVG动画区域根据 selectedScene 渲染对应场景 -->
<!-- 场景1招手 🙋 -->
<div v-if="selectedScene === 1" class="scene scene--hand">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="140" width="200" height="40" fill="#E8EAF6"/>
<rect x="45" y="75" width="110" height="65" rx="10" fill="#ECEFF1"/>
<rect x="50" y="80" width="100" height="55" rx="8" fill="#CFD8DC"/>
<rect x="55" y="85" width="40" height="25" rx="3" fill="#90CAF9"/>
<rect x="100" y="85" width="40" height="25" rx="3" fill="#90CAF9"/>
<circle cx="135" cy="70" r="16" fill="#F5C6A0"/>
<circle cx="130" cy="67" r="2" fill="#333"/>
<circle cx="140" cy="67" r="2" fill="#333"/>
<path d="M130 75 Q135 79 140 75" stroke="#333" stroke-width="1.5" fill="none"/>
<rect x="12" y="72" width="30" height="50" rx="8" fill="#F5C6A0"/>
<g class="hand-arm">
<line x1="42" y1="80" x2="55" y2="55" stroke="#F5C6A0" stroke-width="8" stroke-linecap="round"/>
</g>
<g class="bubble-float">
<rect x="55" y="36" width="100" height="24" rx="12" fill="#FF9800"/>
<text x="105" y="52" text-anchor="middle" fill="white" font-size="11" font-weight="bold">看这里🙋</text>
</g>
</svg>
</div>
<!-- 场景2拍桌子 🪑 -->
<div v-else-if="selectedScene === 2" class="scene scene--pound">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="140" width="200" height="40" fill="#D7CCC8"/>
<rect x="30" y="105" width="140" height="35" rx="6" fill="#8D6E63"/>
<rect x="35" y="110" width="130" height="25" rx="4" fill="#A1887F"/>
<rect x="20" y="75" width="35" height="50" rx="8" fill="#795548"/>
<circle cx="37" cy="60" r="16" fill="#F5C6A0"/>
<g class="pound-fists">
<circle cx="75" cy="98" r="9" fill="#F5C6A0"/>
<circle cx="105" cy="98" r="9" fill="#F5C6A0"/>
</g>
<g class="splash-lines">
<line x1="68" y1="90" x2="62" y2="82" stroke="#FF5722" stroke-width="2"/>
<line x1="80" y1="88" x2="80" y2="78" stroke="#FF5722" stroke-width="2"/>
<line x1="90" y1="88" x2="95" y2="78" stroke="#FF5722" stroke-width="2"/>
<line x1="110" y1="90" x2="110" y2="80" stroke="#FF5722" stroke-width="2"/>
<line x1="120" y1="90" x2="126" y2="82" stroke="#FF5722" stroke-width="2"/>
</g>
<g class="bubble-float">
<rect x="90" y="36" width="100" height="24" rx="12" fill="#FF5722"/>
<text x="140" y="52" text-anchor="middle" fill="white" font-size="11" font-weight="bold">快快快🪑</text>
</g>
</svg>
</div>
<!-- 场景3-7同原版保留全部7个场景的SVG -->
<!-- 场景3劈稻草人 💀 -->
<div v-else-if="selectedScene === 3" class="scene scene--scarecrow">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="140" width="200" height="40" fill="#C8E6C9"/>
<rect x="145" y="100" width="6" height="40" fill="#795548"/>
<rect x="133" y="95" width="30" height="5" rx="2" fill="#795548"/>
<circle cx="148" cy="85" r="14" fill="#FFE082"/>
<line x1="148" y1="74" x2="148" y2="70" stroke="#333" stroke-width="1.5"/>
<rect x="25" y="72" width="30" height="50" rx="8" fill="#1565C0"/>
<circle cx="40" cy="57" r="16" fill="#F5C6A0"/>
<g class="slash-blade">
<rect x="55" y="40" width="6" height="50" rx="2" fill="#B0BEC5"/>
<rect x="52" y="38" width="12" height="8" rx="2" fill="#90A4AE"/>
</g>
<g class="explosion-effect">
<circle cx="148" cy="85" r="10" fill="#FF5722" opacity="0"/>
</g>
<g class="bubble-float">
<rect x="5" y="5" width="105" height="24" rx="12" fill="#D32F2F"/>
<text x="57" y="21" text-anchor="middle" fill="white" font-size="10" font-weight="bold">我要爆炸了💥</text>
</g>
</svg>
</div>
<!-- 场景4砍西瓜 🍉 -->
<div v-else-if="selectedScene === 4" class="scene scene--watermelon">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="140" width="200" height="40" fill="#C8E6C9"/>
<g class="melon">
<ellipse cx="140" cy="125" rx="25" ry="18" fill="#4CAF50"/>
<ellipse cx="140" cy="122" rx="18" ry="10" fill="#F44336"/>
</g>
<rect x="25" y="75" width="30" height="47" rx="8" fill="#1565C0"/>
<circle cx="40" cy="60" r="16" fill="#F5C6A0"/>
<g class="knife">
<rect x="55" y="42" width="6" height="45" rx="2" fill="#B0BEC5"/>
<rect x="52" y="40" width="12" height="8" rx="2" fill="#90A4AE"/>
</g>
<g class="juice-splash">
<circle cx="130" cy="110" r="2" fill="#F44336" opacity="0"/>
<circle cx="145" cy="105" r="2.5" fill="#F44336" opacity="0"/>
<circle cx="155" cy="112" r="1.5" fill="#F44336" opacity="0"/>
</g>
<g class="bubble-float">
<rect x="5" y="5" width="105" height="24" rx="12" fill="#43A047"/>
<text x="57" y="21" text-anchor="middle" fill="white" font-size="10" font-weight="bold">IT救我🍉</text>
</g>
</svg>
</div>
<!-- 场景5摇传菜铃 🔔 -->
<div v-else-if="selectedScene === 5" class="scene scene--bell">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="140" width="200" height="40" fill="#FFF3E0"/>
<rect x="25" y="70" width="30" height="52" rx="8" fill="#FF9800"/>
<circle cx="40" cy="55" r="16" fill="#F5C6A0"/>
<g class="bell-left">
<path d="M52 72 Q52 58 60 50 Q68 42 72 50 Q76 58 74 72" fill="#FFD54F" stroke="#FFA000" stroke-width="1.5"/>
<ellipse cx="63" cy="72" rx="9" ry="3" fill="#FFA000"/>
</g>
<g class="bell-right">
<path d="M92 72 Q92 58 100 50 Q108 42 112 50 Q116 58 114 72" fill="#FFD54F" stroke="#FFA000" stroke-width="1.5"/>
<ellipse cx="103" cy="72" rx="9" ry="3" fill="#FFA000"/>
</g>
<g class="sound-waves">
<circle cx="160" cy="55" r="10" fill="none" stroke="#FF9800" stroke-width="1.5" opacity="0"/>
<circle cx="160" cy="55" r="16" fill="none" stroke="#FF9800" stroke-width="1.5" opacity="0"/>
<circle cx="160" cy="55" r="22" fill="none" stroke="#FF9800" stroke-width="1" opacity="0"/>
</g>
<g class="bubble-float">
<rect x="5" y="5" width="105" height="24" rx="12" fill="#FF9800"/>
<text x="57" y="21" text-anchor="middle" fill="white" font-size="10" font-weight="bold">叮叮叮有人吗</text>
</g>
</svg>
</div>
<!-- 场景6大炮发射 💣 -->
<div v-else-if="selectedScene === 6" class="scene scene--cannon">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="145" width="200" height="35" fill="#D7CCC8"/>
<rect x="55" y="132" width="50" height="16" rx="3" fill="#5D4037"/>
<circle cx="80" cy="138" r="9" fill="#4E342E"/>
<g class="cannon-barrel">
<rect x="58" y="110" width="55" height="14" rx="4" fill="#616161"/>
<rect x="108" y="108" width="12" height="18" rx="3" fill="#757575"/>
</g>
<g class="fuse">
<path d="M58 117 Q52 115 48 118 Q44 121 40 117" stroke="#FF9800" stroke-width="2" fill="none"/>
<circle cx="38" cy="116" r="3" fill="#FF5722" class="fuse-spark"/>
</g>
<g class="cannonball">
<circle cx="120" cy="105" r="7" fill="#37474F"/>
</g>
<g class="explosion">
<circle cx="170" cy="60" r="18" fill="#FF5722" opacity="0" class="blast-main"/>
<circle cx="170" cy="60" r="12" fill="#FFC107" opacity="0" class="blast-inner"/>
<line x1="170" y1="40" x2="170" y2="30" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-1"/>
<line x1="185" y1="48" x2="192" y2="42" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-2"/>
<line x1="188" y1="60" x2="196" y2="60" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-3"/>
<line x1="185" y1="72" x2="192" y2="78" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-4"/>
<line x1="170" y1="80" x2="170" y2="90" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-5"/>
<line x1="155" y1="72" x2="148" y2="78" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-6"/>
<line x1="152" y1="60" x2="144" y2="60" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-7"/>
<line x1="155" y1="48" x2="148" y2="42" stroke="#FF5722" stroke-width="3" opacity="0" class="blast-ray ray-8"/>
</g>
<g class="target">
<circle cx="170" cy="55" r="14" fill="none" stroke="#F44336" stroke-width="2"/>
<circle cx="170" cy="55" r="8" fill="none" stroke="#F44336" stroke-width="1.5"/>
<circle cx="170" cy="55" r="2" fill="#F44336"/>
<text x="170" y="38" text-anchor="middle" fill="#F44336" font-size="9" font-weight="bold">BUG</text>
</g>
<rect x="5" y="75" width="30" height="57" rx="7" fill="#795548"/>
<circle cx="20" cy="60" r="16" fill="#F5C6A0"/>
<circle cx="15" cy="57" r="2" fill="#333"/>
<circle cx="25" cy="57" r="2" fill="#333"/>
<path d="M14 66 Q20 70 26 66" stroke="#333" stroke-width="1.5" fill="none"/>
<line x1="35" y1="85" x2="48" y2="100" stroke="#F5C6A0" stroke-width="7" stroke-linecap="round" class="gunner-arm"/>
<g class="bubble-float">
<rect x="5" y="5" width="95" height="24" rx="12" fill="#FF5722"/>
<text x="52" y="21" text-anchor="middle" fill="white" font-size="11" font-weight="bold">开炮💣</text>
</g>
</svg>
</div>
<!-- 场景7导弹发射 🚀 -->
<div v-else-if="selectedScene === 7" class="scene scene--missile">
<svg viewBox="0 0 200 180" xmlns="http://www.w3.org/2000/svg" class="scene__svg">
<rect x="0" y="0" width="200" height="140" fill="#E3F2FD"/>
<rect x="0" y="140" width="200" height="40" fill="#BDBDBD"/>
<rect x="85" y="130" width="30" height="12" rx="2" fill="#616161"/>
<rect x="88" y="142" width="8" height="10" fill="#757575"/>
<rect x="104" y="142" width="8" height="10" fill="#757575"/>
<g class="missile-body">
<rect x="94" y="50" width="12" height="65" rx="4" fill="#E0E0E0"/>
<path d="M94 50 L100 35 L106 50 Z" fill="#F44336"/>
<polygon points="94,108 86,115 94,112" fill="#F44336"/>
<polygon points="106,108 114,115 106,112" fill="#F44336"/>
</g>
<g class="rocket-flame">
<ellipse cx="100" cy="118" rx="4" ry="10" fill="#FF9800" class="flame-outer"/>
<ellipse cx="100" cy="116" rx="2" ry="6" fill="#FFEB3B" class="flame-inner"/>
</g>
<g class="smoke">
<circle cx="88" cy="145" r="8" fill="#CFD8DC" opacity="0" class="smoke-puff puff-1"/>
<circle cx="112" cy="145" r="8" fill="#CFD8DC" opacity="0" class="smoke-puff puff-2"/>
<circle cx="100" cy="155" r="10" fill="#CFD8DC" opacity="0" class="smoke-puff puff-3"/>
</g>
<g class="trail">
<path d="M100 130 L100 145" stroke="#FFC107" stroke-width="2" opacity="0" class="trail-line"/>
</g>
<rect x="15" y="80" width="28" height="52" rx="7" fill="#1565C0"/>
<circle cx="29" cy="65" r="16" fill="#F5C6A0"/>
<rect x="18" y="58" width="22" height="8" rx="3" fill="#333" opacity="0.7"/>
<circle cx="24" cy="62" r="1.5" fill="#FFF"/>
<circle cx="34" cy="62" r="1.5" fill="#FFF"/>
<ellipse cx="29" cy="72" rx="4" ry="3" fill="#333"/>
<line x1="43" y1="90" x2="60" y2="100" stroke="#F5C6A0" stroke-width="7" stroke-linecap="round"/>
<rect x="55" y="105" width="15" height="20" rx="3" fill="#424242"/>
<circle cx="62.5" cy="112" r="3" fill="#F44336" class="launch-button"/>
</svg>
</div>
<!-- 话术文字 -->
<div class="call-modal__speech-text">
<span class="call-modal__speech-emoji">{{ sceneEmoji }}</span>
<span class="call-modal__speech-content">{{ sceneText }}</span>
</div>
<!-- 发送状态 -->
<div v-if="sending" class="call-modal__sending">
<span class="call-modal__dot-flashing"></span>
<span>正在通知 IT 坐席...</span>
</div>
<div v-if="sendSuccess" class="call-modal__success">
呼叫成功坐席马上就来
</div>
</div>
<!-- 底部关闭按钮 -->
<div class="call-modal__footer call-modal__footer--center">
<button
v-if="sendSuccess"
class="call-modal__btn call-modal__btn--primary call-modal__btn--large"
@click="handleClose"
>好的返回聊天</button>
<button
v-else
class="call-modal__btn call-modal__btn--cancel"
@click="handleClose"
>取消</button>
</div>
</div>
</div>
</Transition>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
/**
* CallAgentModal — 「呼叫坐席」动画弹窗(简化版)
*
* 流程:打开 → 随机播放趣味动画 + 发 shake 请求 → 关闭
* 前提:用户已在聊天中描述问题,AI 已回复 >= 3 轮
*/
import { ref, computed, watch } from 'vue'
import { useConversationStore } from '@/stores/conversation'
const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'call-success'): void
}>()
const store = useConversationStore()
// ── 状态 ──
const selectedScene = ref<number>(1)
const sending = ref<boolean>(false)
const sendSuccess = ref<boolean>(false)
// ── 场景配置 ──
const scenes = [
{ id: 1, emoji: '🙋', text: '看这里!看我这里...我有个问题!', weight: 3 },
{ id: 2, emoji: '🪑', text: '快快快!我等不及了!', weight: 3 },
{ id: 3, emoji: '💀', text: '这个问题不解决我就要原地爆炸了💥', weight: 1.5 },
{ id: 4, emoji: '🍉', text: 'IT!救我!这个问题卡住我了🍉', weight: 1.5 },
{ id: 5, emoji: '🔔', text: '叮叮叮!有人吗!IT 在线吗!', weight: 1 },
{ id: 6, emoji: '💣', text: '开炮!💣 这个问题必须解决了!', weight: 1.5 },
{ id: 7, emoji: '🚀', text: '发射!🚀 紧急呼叫 IT 特种部队!', weight: 1.5 },
] as const
/**
* 固定使用场景5:摇铃铛 🔔
* 不再随机选择,回归统一的摇铃呼叫体验
*/
function pickScene(): number {
return 5
}
const sceneText = computed(() => scenes.find(s => s.id === selectedScene.value)?.text ?? '')
const sceneEmoji = computed(() => scenes.find(s => s.id === selectedScene.value)?.emoji ?? '🙋')
// ── 弹窗打开时自动发起呼叫 ──
watch(() => props.visible, (newVal) => {
if (newVal) {
startCall()
}
})
async function startCall(): Promise<void> {
selectedScene.value = pickScene()
sending.value = true
sendSuccess.value = false
try {
await store.shakeAgent()
sendSuccess.value = true
emit('call-success')
// 3秒后自动关闭
setTimeout(() => {
if (sendSuccess.value) handleClose()
}, 4000)
} catch (err) {
// 发送失败,关闭弹窗
handleClose()
} finally {
sending.value = false
}
}
function handleClose(): void {
emit('update:visible', false)
// 重置状态
setTimeout(() => {
sending.value = false
sendSuccess.value = false
}, 300)
}
</script>
<style scoped>
/* ==========================================================================
弹窗容器
========================================================================== */
.call-modal__overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.call-modal {
background: var(--bg-secondary);
border-radius: 16px;
width: 88vw;
max-width: 360px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.call-modal--compact {
max-width: 340px;
}
.call-modal__header {
text-align: center;
padding: 20px 16px 8px;
}
.call-modal__icon { font-size: 32px; }
.call-modal__header h3 {
margin: 8px 0 4px;
font-size: 17px;
color: var(--text-primary);
}
.call-modal__body { padding: 12px 16px; }
.call-modal__body--center {
display: flex;
flex-direction: column;
align-items: center;
}
.call-modal__footer {
padding: 12px 16px 20px;
display: flex;
justify-content: space-between;
gap: 10px;
}
.call-modal__footer--center { justify-content: center; }
.call-modal__btn {
border: none;
border-radius: 10px;
padding: 10px 24px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.call-modal__btn--cancel { background: var(--bg-tertiary); color: var(--text-secondary); }
.call-modal__btn--primary { background: var(--color-warning); color: var(--bg-secondary); }
.call-modal__btn--large { padding: 12px 48px; font-size: 15px; }
.call-modal__btn:active { transform: scale(0.96); }
/* ==========================================================================
话术文字
========================================================================== */
.call-modal__speech-text {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 15px;
color: var(--text-primary);
font-weight: 600;
text-align: center;
}
.call-modal__speech-emoji { font-size: 22px; }
/* ==========================================================================
发送状态
========================================================================== */
.call-modal__sending {
margin-top: 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-tertiary);
}
.call-modal__dot-flashing {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-warning);
animation: dot-flash 0.6s infinite alternate;
}
@keyframes dot-flash {
0% { opacity: 0.2; }
100% { opacity: 1; }
}
.call-modal__success {
margin-top: 12px;
font-size: 15px;
color: var(--color-success);
font-weight: 600;
}
/* ==========================================================================
SVG 场景容器
========================================================================== */
.scene { margin-bottom: 4px; }
.scene__svg { width: 180px; height: 150px; display: block; margin: 0 auto; }
/* ==========================================================================
场景1:招手动画
========================================================================== */
.hand-arm {
animation: hand-wave 0.6s ease-in-out infinite;
transform-origin: 42px 80px;
}
@keyframes hand-wave {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(-30deg); }
}
.bubble-float { animation: bubble-updown 2s ease-in-out infinite; }
@keyframes bubble-updown {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
/* ==========================================================================
场景2:拍桌子动画
========================================================================== */
.pound-fists {
animation: desk-pound 0.4s ease-in-out infinite alternate;
}
@keyframes desk-pound {
0% { transform: translateY(0); }
100% { transform: translateY(-8px); }
}
.scene--pound {
animation: desk-shake 0.4s ease-in-out infinite alternate;
transform-origin: center bottom;
}
@keyframes desk-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-3px); }
75% { transform: translateX(3px); }
}
.splash-lines line:nth-child(1) { animation: splash 0.6s ease-out infinite; }
.splash-lines line:nth-child(2) { animation: splash 0.6s ease-out 0.1s infinite; }
.splash-lines line:nth-child(3) { animation: splash 0.6s ease-out 0.2s infinite; }
.splash-lines line:nth-child(4) { animation: splash 0.6s ease-out 0.15s infinite; }
.splash-lines line:nth-child(5) { animation: splash 0.6s ease-out 0.25s infinite; }
@keyframes splash {
0% { opacity: 1; transform: translate(0,0); }
100% { opacity: 0; transform: translate(var(--sx, 0), -12px); }
}
/* ==========================================================================
场景3:劈稻草人
========================================================================== */
.slash-blade {
animation: blade-slash 1.0s ease-in-out infinite;
transform-origin: 58px 38px;
}
@keyframes blade-slash {
0%, 15% { transform: rotate(-60deg); }
30%, 55% { transform: rotate(20deg); }
70%, 100% { transform: rotate(-60deg); }
}
.explosion-effect circle {
animation: boom-flash 1.0s ease-in-out infinite;
}
@keyframes boom-flash {
0%, 50% { opacity: 0; transform: scale(0.5); }
55% { opacity: 0.9; transform: scale(2); }
65% { opacity: 0; transform: scale(3); }
100% { opacity: 0; }
}
/* ==========================================================================
场景4:砍西瓜
========================================================================== */
.knife {
animation: knife-chop 0.8s ease-in-out infinite;
transform-origin: 58px 40px;
}
@keyframes knife-chop {
0%, 20% { transform: rotate(-50deg); }
40%, 60% { transform: rotate(15deg); }
80%, 100% { transform: rotate(-50deg); }
}
.juice-splash circle:nth-child(1) { animation: juice-splash 0.8s ease-out 0.4s infinite; }
.juice-splash circle:nth-child(2) { animation: juice-splash 0.8s ease-out 0.45s infinite; }
.juice-splash circle:nth-child(3) { animation: juice-splash 0.8s ease-out 0.5s infinite; }
@keyframes juice-splash {
0% { opacity: 0; transform: translate(0,0) scale(0); }
50% { opacity: 1; transform: translate(-5px, -10px) scale(1); }
100% { opacity: 0; transform: translate(-10px, -20px) scale(0.5); }
}
.melon {
animation: melon-shake 0.8s ease-in-out infinite;
}
@keyframes melon-shake {
0%, 37% { transform: translate(0,0); }
42%, 58% { transform: translate(-3px, 2px); }
62% { transform: translate(0,0); }
}
/* ==========================================================================
场景5:摇传菜铃
========================================================================== */
.bell-left {
animation: bell-ring-left 0.5s ease-in-out infinite;
transform-origin: 63px 58px;
}
.bell-right {
animation: bell-ring-right 0.5s ease-in-out 0.25s infinite;
transform-origin: 103px 58px;
}
@keyframes bell-ring-left {
0%, 100% { transform: rotate(0); }
50% { transform: rotate(-12deg); }
}
@keyframes bell-ring-right {
0%, 100% { transform: rotate(0); }
50% { transform: rotate(12deg); }
}
.sound-waves circle:nth-child(1) { animation: wave-expand 1s ease-out infinite; }
.sound-waves circle:nth-child(2) { animation: wave-expand 1s ease-out 0.3s infinite; }
.sound-waves circle:nth-child(3) { animation: wave-expand 1s ease-out 0.6s infinite; }
@keyframes wave-expand {
0% { opacity: 1; stroke-width: 2; }
100% { opacity: 0; stroke-width: 0.5; }
}
/* ==========================================================================
场景6:大炮发射
========================================================================== */
.cannon-barrel {
animation: cannon-recoil 1.2s ease-in-out infinite;
transform-origin: 80px 117px;
}
@keyframes cannon-recoil {
0%, 15% { transform: translateX(0); }
20%, 25% { transform: translateX(-8px); }
30%, 100% { transform: translateX(0); }
}
.fuse-spark {
animation: spark-flicker 0.15s ease-in-out infinite alternate;
}
@keyframes spark-flicker {
0% { r: 2; fill: #FF5722; opacity: 0.6; }
100% { r: 3.5; fill: #FFEB3B; opacity: 1; }
}
.cannonball {
animation: cannonball-fly 1.2s ease-in-out infinite;
}
@keyframes cannonball-fly {
0%, 25% { transform: translate(0, 0); opacity: 0; }
28% { transform: translate(15px, -20px); opacity: 1; }
50% { transform: translate(50px, -48px); opacity: 1; }
100% { transform: translate(50px, -48px); opacity: 1; }
}
.blast-main { animation: blast-appear 1.2s ease-in-out infinite; }
@keyframes blast-appear {
0%, 55% { opacity: 0; transform: scale(0); }
58% { opacity: 1; transform: scale(1.5); }
65% { opacity: 0.9; transform: scale(1.2); }
75% { opacity: 0; transform: scale(1.8); }
100% { opacity: 0; }
}
.blast-inner { animation: blast-inner 1.2s ease-in-out infinite; }
@keyframes blast-inner {
0%, 58% { opacity: 0; transform: scale(0); }
62% { opacity: 1; transform: scale(1); }
68% { opacity: 0.7; transform: scale(1.3); }
75% { opacity: 0; }
100% { opacity: 0; }
}
.blast-ray { animation: blast-ray 1.2s ease-in-out infinite; }
.ray-1, .ray-5 { animation-delay: 0.55s; }
.ray-2, .ray-6 { animation-delay: 0.56s; }
.ray-3, .ray-7 { animation-delay: 0.57s; }
.ray-4, .ray-8 { animation-delay: 0.58s; }
@keyframes blast-ray {
0%, 60% { opacity: 0; }
62% { opacity: 1; }
70% { opacity: 0; }
100% { opacity: 0; }
}
.target { animation: target-shake 1.2s ease-in-out infinite; }
@keyframes target-shake {
0%, 56% { transform: translate(0,0) rotate(0); }
60% { transform: translate(3px,-3px) rotate(5deg); }
64% { transform: translate(-3px,2px) rotate(-5deg); }
68% { transform: translate(0,0) rotate(0); }
100% { transform: translate(0,0) rotate(0); }
}
.gunner-arm {
animation: gunner-arm 1.2s ease-in-out infinite;
transform-origin: 35px 85px;
}
@keyframes gunner-arm {
0%, 25% { transform: rotate(0deg); }
28% { transform: rotate(15deg); }
35% { transform: rotate(0deg); }
100% { transform: rotate(0deg); }
}
/* ==========================================================================
场景7:导弹发射
========================================================================== */
.missile-body { animation: missile-launch 1.5s ease-in-out infinite; }
@keyframes missile-launch {
0%, 10% { transform: translateY(0); }
15%, 70% { transform: translateY(-80px); }
80%, 100% { transform: translateY(-80px); }
}
.rocket-flame { animation: missile-launch 1.5s ease-in-out infinite; }
.flame-outer { animation: flame-pulse 0.2s ease-in-out infinite alternate; }
.flame-inner { animation: flame-pulse 0.15s ease-in-out infinite alternate; }
@keyframes flame-pulse {
0% { transform: scaleY(1); opacity: 0.8; }
100% { transform: scaleY(1.3); opacity: 1; }
}
.smoke-puff { animation: smoke-burst 1.5s ease-out infinite; }
.puff-1 { animation-delay: 0.2s; }
.puff-2 { animation-delay: 0.3s; }
.puff-3 { animation-delay: 0.4s; }
@keyframes smoke-burst {
0% { opacity: 0; transform: scale(0.5) translateY(0); }
15% { opacity: 0.7; transform: scale(1) translateY(0); }
100% { opacity: 0; transform: scale(2.5) translateY(-20px); }
}
.trail-line { animation: trail-glow 1.5s ease-out infinite; }
@keyframes trail-glow {
0%, 15% { opacity: 0; }
20% { opacity: 0.8; }
70% { opacity: 0.6; }
100% { opacity: 0; }
}
.launch-button { animation: button-blink 0.5s ease-in-out infinite alternate; }
@keyframes button-blink {
0% { fill: #F44336; }
100% { fill: #FFEB3B; }
}
/* ==========================================================================
过渡动画
========================================================================== */
.modal-fade-enter-active,
.modal-fade-leave-active { transition: opacity 0.25s ease; }
.modal-fade-enter-from,
.modal-fade-leave-to { opacity: 0; }
.modal-zoom-enter-active { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.modal-zoom-leave-active { transition: all 0.2s ease-in; }
.modal-zoom-enter-from { opacity: 0; transform: scale(0.8); }
.modal-zoom-leave-to { opacity: 0; transform: scale(0.9); }
</style>