更新
This commit is contained in:
2026-02-23 18:52:32 +08:00
parent d429560511
commit 123fa6a7aa
81 changed files with 5952 additions and 407 deletions
@@ -0,0 +1,124 @@
<template>
<div class="video-msg-container" @click="handlePlay">
<img :src="thumbnailUrl" class="video-poster" :style="containerStyle" />
<div class="play-icon-overlay">
<div class="play-button"></div>
</div>
<span v-if="duration" class="duration-label">
{{ formatDuration(duration) }}
</span>
<div v-if="uploading" class="upload-mask">
<div class="progress">{{ progress }}%</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
// 缩略图地址(可以是 URL 或 Blob 生成的 ObjectURL
thumbnailUrl: String,
// 视频时长(秒)
duration: Number,
// 视频原始宽高(用于计算比例)
w: Number,
h: Number,
uploading: Boolean,
progress: Number
});
const emit = defineEmits(['play']);
// 格式化时长:如 75 -> "01:15"
const formatDuration = (seconds) => {
if (!seconds) return '00:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
};
// 计算容器尺寸,防止长视频或宽视频撑破布局
const containerStyle = computed(() => {
const maxSide = 200; // 最大显示尺寸
if (props.w > props.h) {
return { width: maxSide + 'px', height: 'auto', aspectRatio: `${props.w}/${props.h}` };
} else {
return { height: maxSide + 'px', width: 'auto', aspectRatio: `${props.w}/${props.h}` };
}
});
const handlePlay = () => {
emit('play');
};
</script>
<style scoped>
.video-msg-container {
position: relative;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.video-poster {
display: block;
object-fit: cover;
background: #2a2a2a;
}
.play-icon-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
}
.play-button {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
position: relative;
}
.play-button::after {
content: '';
position: absolute;
left: 16px;
top: 10px;
border-style: solid;
border-width: 10px 0 10px 15px;
border-color: transparent transparent transparent #333;
}
.duration-label {
position: absolute;
bottom: 4px;
right: 6px;
color: #fff;
font-size: 12px;
background: rgba(0, 0, 0, 0.5);
padding: 2px 6px;
border-radius: 4px;
}
.upload-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
</style>
@@ -0,0 +1,30 @@
<template>
<div id="Video"></div>
</template>
<script setup>
import Player from 'xgplayer/dist/simple_player';
import volume from 'xgplayer/dist/controls/volume';
import playbackRate from 'xgplayer/dist/controls/playbackRate';
import { defineProps } from 'vue';
const props = defineProps({
m: {
type: Object,
required:true
}
});
let player = new Player({
id: 'Video',
url: props.m.content.url,
controlPlugins: [
volume,
playbackRate
],
playbackRate: [0.5, 0.75, 1, 1.5, 2] //传入倍速可选数组
});
</script>
@@ -0,0 +1,158 @@
<template>
<div
class="voice-card"
:class="{ 'playing': isPlaying }"
:style="{ width: bubbleWidth + 'px' }"
@click="togglePlay"
>
<div class="play-icon-wrap">
<svg v-if="!isPlaying" viewBox="0 0 24 24" class="svg-obj">
<path fill="currentColor" d="M8 5v14l11-7z"/>
</svg>
<svg v-else viewBox="0 0 24 24" class="svg-obj">
<path fill="currentColor" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</div>
<div class="wave-track">
<div v-for="n in 40" :key="n" class="wave-item"></div>
</div>
<span class="time-label">{{ Math.floor(duration) }}''</span>
<div v-if="!isRead" class="unread-dot"></div>
<audio ref="audioRef" :src="url" @ended="stopPlay" @error="stopPlay"></audio>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
url: { type: String, required: true },
duration: { type: Number, default: 0 },
isRead: { type: Boolean, default: true }
});
const emit = defineEmits(['played']);
const audioRef = ref(null);
const isPlaying = ref(false);
const bubbleWidth = computed(() => Math.min(110 + props.duration * 4, 260));
const togglePlay = (e) => {
e.stopPropagation();
if (isPlaying.value) {
audioRef.value.pause();
stopPlay();
} else {
audioRef.value.play();
isPlaying.value = true;
if (!props.isRead) emit('played');
}
};
const stopPlay = () => {
isPlaying.value = false;
if (audioRef.value) audioRef.value.currentTime = 0;
};
</script>
<style scoped>
/* 核心改动:纯白背景 + 物理阴影 + 明确边框 */
.voice-card {
display: flex;
align-items: center;
height: 40px;
padding: 0 14px;
background: #ffffff; /* 强迫其在灰白背景上显现 */
border: 1.5px solid #e8eaed; /* 更有质感的深色边框 */
border-radius: 20px; /* 全圆角更现代 */
cursor: pointer;
position: relative;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
color: #202124;
/* 增加微妙的投影,产生高度感 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.voice-card:hover {
border-color: #007aff;
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.12);
}
.voice-card:active {
transform: scale(0.96);
}
/* 播放图标:使用品牌蓝 */
.play-icon-wrap {
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
color: #007aff;
}
.svg-obj { width: 100%; height: 100%; }
/* 波形区 */
.wave-track {
flex: 1;
display: flex;
align-items: center;
gap: 3px;
margin: 0 12px;
height: 24px;
overflow: hidden;
}
.wave-item {
width: 2px;
height: 4px;
background-color: #bdc1c6; /* 加深线条颜色,防止看不清 */
border-radius: 1px;
flex-shrink: 0;
transition: all 0.2s ease;
}
/* 播放态动画 */
.playing .wave-item {
background-color: #007aff;
animation: bar-throb 0.7s infinite alternate;
}
/* 使用 nth-child 赋予不同的波浪起伏感 */
.wave-item:nth-child(4n+1) { height: 6px; animation-delay: 0.1s; }
.wave-item:nth-child(4n+2) { height: 14px; animation-delay: 0.3s; }
.wave-item:nth-child(4n+3) { height: 10px; animation-delay: 0.2s; }
.wave-item:nth-child(4n+4) { height: 6px; animation-delay: 0.4s; }
@keyframes bar-throb {
from { transform: scaleY(1); opacity: 0.7; }
to { transform: scaleY(1.8); opacity: 1; }
}
.time-label {
font-size: 13px;
font-weight: 800; /* 加粗时间,更清晰 */
color: #5f6368;
flex-shrink: 0;
font-family: 'Helvetica Neue', sans-serif;
}
/* 未读红点:增加发光效果 */
.unread-dot {
position: absolute;
top: -2px;
right: -2px;
width: 9px;
height: 9px;
background: #f44336;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(244, 67, 54, 0.4);
}
</style>