add:
更新
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user