diff --git a/CodeGenerator/Lib/CloudBuilder.Core.dll b/CodeGenerator/Lib/CloudBuilder.Core.dll
index b976e5d..6212e7a 100644
Binary files a/CodeGenerator/Lib/CloudBuilder.Core.dll and b/CodeGenerator/Lib/CloudBuilder.Core.dll differ
diff --git a/WebVue/src/components/basic/VideoPlayer.vue b/WebVue/src/components/basic/VideoPlayer.vue
index e43493f..a70cbb3 100644
--- a/WebVue/src/components/basic/VideoPlayer.vue
+++ b/WebVue/src/components/basic/VideoPlayer.vue
@@ -8,12 +8,18 @@
+
+
@@ -139,6 +147,7 @@
@@ -594,6 +746,23 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%) scale(0.94);
}
+/* 缓冲提示:让「正在加载」和「卡死」区分开。
+ 不加 pointer-events: none 会挡住画面区的点击(播放/暂停) */
+.vp-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.9);
+ pointer-events: none;
+}
+
/* ---------- 画面区 ---------- */
.vp-stage {
position: relative;
@@ -792,6 +961,16 @@ onBeforeUnmount(() => {
background: rgba(255, 255, 255, 0.32);
}
+/* 已缓冲区间:比已播进度浅,能看出加载走到哪了 */
+.vp-bar-buffer {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ border-radius: 2px;
+ background: rgba(255, 255, 255, 0.5);
+}
+
.vp-bar-fill {
position: absolute;
left: 0;
diff --git a/WebVue/src/components/basic/VinylAudioPlayer.vue b/WebVue/src/components/basic/VinylAudioPlayer.vue
index d6aacf7..c53223b 100644
--- a/WebVue/src/components/basic/VinylAudioPlayer.vue
+++ b/WebVue/src/components/basic/VinylAudioPlayer.vue
@@ -386,10 +386,25 @@ const seekTo = (t: number) => {
/** 两个 audio 都会触发事件,只有当前在播的那个才需要响应 */
const isActive = (el: EventTarget | null) => !!el && el === activeAudio.value;
+/**
+ * 直接从当前在播的 audio 读时长。
+ *
+ * 不能只依赖 loadedmetadata 事件:切歌若是换到了「后台已经预载好的那一个」,
+ * 它的 loadedmetadata 早在预载阶段就触发过了(那时它还不是主角,被 isActive 挡掉),
+ * 成为主角后不会再触发第二次,duration 就永远停在 0——
+ * 总时长显示 00:00、进度条不动、也没法拖动。
+ * 这条路径只在「会交换角色的情况」下出现,比如手机连 WiFi 时预载开启。
+ */
+const syncDuration = () => {
+ const el = activeAudio.value;
+ if (el && isFinite(el.duration) && el.duration > 0) duration.value = el.duration;
+};
+
const onTimeUpdate = (e: Event) => {
if (!isActive(e.target) || dragRatio.value !== null) return;
const a = e.target as HTMLAudioElement;
currentTime.value = a.currentTime;
+ syncDuration();
syncLyric();
};
const onLoadedMetadata = (e: Event) => {
@@ -636,6 +651,9 @@ watch(current, async (t) => {
lyricIndex.value = -1;
if (!t) return;
await nextTick();
+ // 换到预载好的那一个时不会再有 loadedmetadata,这里主动读一次,
+ // 否则总时长停在 00:00、进度条也不走
+ syncDuration();
loadLrc(t.lrc);
if (resumeAfterSwitch) {
resumeAfterSwitch = false;
@@ -664,6 +682,8 @@ watch([volume, muted, rate], () => applyAudioSettings());
watch(activeSlot, async () => {
await nextTick();
applyAudioSettings();
+ // 交换角色后补读时长:这个元素的 loadedmetadata 可能已经在预载时被丢掉了
+ syncDuration();
});
let timerId = 0;
@@ -920,7 +940,7 @@ onBeforeUnmount(() => {
比自然位置高一点,又不至于像之前 -160px 那样贴着唱片下沿。
用 relative + top 只挪视觉位置,不影响下面标题、按钮的排布 */
position: relative;
- top: -60px;
+ top: -20px;
height: 102px;
margin-top: 30px;
overflow: hidden;