733 lines
32 KiB
Vue
733 lines
32 KiB
Vue
<!-- =============================================================================
|
||
企微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>
|