chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
@@ -0,0 +1,732 @@
<!-- =============================================================================
企微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>