101 lines
2.4 KiB
Vue
101 lines
2.4 KiB
Vue
|
|
<template>
|
||
|
|
<Teleport to="body">
|
||
|
|
<Transition name="tts-menu-fade">
|
||
|
|
<div
|
||
|
|
v-if="visible"
|
||
|
|
class="tts-menu"
|
||
|
|
:style="menuStyle"
|
||
|
|
@mousedown.stop
|
||
|
|
>
|
||
|
|
<button class="tts-menu__btn" @click="$emit('speak')" :disabled="loading">
|
||
|
|
<svg v-if="!loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M11 5L6 9H2v6h4l5 4V5z"/>
|
||
|
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
||
|
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
|
||
|
|
</svg>
|
||
|
|
<svg v-else class="tts-menu__spinner" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||
|
|
</svg>
|
||
|
|
{{ loading ? '生成中...' : '朗读' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
</Teleport>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed } from 'vue'
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
visible: { type: Boolean, default: false },
|
||
|
|
x: { type: Number, default: 0 },
|
||
|
|
y: { type: Number, default: 0 },
|
||
|
|
loading: { type: Boolean, default: false },
|
||
|
|
})
|
||
|
|
|
||
|
|
defineEmits(['speak'])
|
||
|
|
|
||
|
|
const menuStyle = computed(() => ({
|
||
|
|
left: `${props.x}px`,
|
||
|
|
top: `${props.y}px`,
|
||
|
|
transform: 'translate(-50%, -100%)',
|
||
|
|
}))
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.tts-menu {
|
||
|
|
position: fixed;
|
||
|
|
z-index: 100000;
|
||
|
|
pointer-events: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu__btn {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
padding: 6px 14px;
|
||
|
|
border: 1px solid var(--panel-border);
|
||
|
|
border-radius: 20px;
|
||
|
|
background: var(--panel-bg);
|
||
|
|
backdrop-filter: blur(12px);
|
||
|
|
-webkit-backdrop-filter: blur(12px);
|
||
|
|
color: var(--app-text);
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
box-shadow: var(--panel-shadow);
|
||
|
|
white-space: nowrap;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu__btn:hover:not(:disabled) {
|
||
|
|
background: var(--btn-hover-bg);
|
||
|
|
color: var(--btn-hover-fg);
|
||
|
|
border-color: var(--btn-hover-bg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu__btn:disabled {
|
||
|
|
opacity: 0.6;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu__spinner {
|
||
|
|
animation: tts-spin 1s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes tts-spin {
|
||
|
|
to { transform: rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu-fade-enter-active,
|
||
|
|
.tts-menu-fade-leave-active {
|
||
|
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tts-menu-fade-enter-from,
|
||
|
|
.tts-menu-fade-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
transform: translate(-50%, calc(-100% + 4px));
|
||
|
|
}
|
||
|
|
</style>
|