fix: add play mode toggle, wire play history API, add back navigation buttons

- Add play mode cycle button (seq/loop/random/rloop) to Player bar
- Wire GET /api/player/:botId/history to query database instead of returning empty array
- Pass database instance from server.ts to createPlayerRouter
- Add back navigation button to Playlist, Lyrics, History, Search, and Settings views
- SongCard already had dblclick-to-play (no change needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 02:16:18 +08:00
1 parent 9dce883384
commit ee5a75ab5e
8 files changed
+140 -3

No files matched your search

+19 -2
View File
@@ -1,11 +1,13 @@
import { Router } from "express"; import { Router } from "express";
import type { BotManager } from "../../bot/manager.js"; import type { BotManager } from "../../bot/manager.js";
import type { BotDatabase } from "../../data/database.js";
import type { Logger } from "../../logger.js"; import type { Logger } from "../../logger.js";
import { parseCommand } from "../../bot/commands.js"; import { parseCommand } from "../../bot/commands.js";
export function createPlayerRouter( export function createPlayerRouter(
botManager: BotManager, botManager: BotManager,
logger: Logger logger: Logger,
database?: BotDatabase
): Router { ): Router {
const router = Router(); const router = Router();
@@ -132,8 +134,23 @@ export function createPlayerRouter(
}); });
router.get("/:botId/history", (req, res) => { router.get("/:botId/history", (req, res) => {
// Will be wired when database access is available through bot if (!database) {
res.json({ history: [] }); res.json({ history: [] });
return;
}
const limit = parseInt(req.query.limit as string) || 50;
const records = database.getPlayHistory(req.params.botId, limit);
const history = records.map((r) => ({
id: r.songId,
name: r.songName,
artist: r.artist,
album: r.album,
duration: 0,
coverUrl: r.coverUrl,
platform: r.platform,
playedAt: r.playedAt,
}));
res.json({ history });
}); });
return router; return router;
+1 -1
View File
@@ -46,7 +46,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
"/api/music", "/api/music",
createMusicRouter(options.neteaseProvider, options.qqProvider, logger) createMusicRouter(options.neteaseProvider, options.qqProvider, logger)
); );
app.use("/api/player", createPlayerRouter(options.botManager, logger)); app.use("/api/player", createPlayerRouter(options.botManager, logger, options.database));
app.use( app.use(
"/api/auth", "/api/auth",
createAuthRouter(options.neteaseProvider, options.qqProvider, logger, options.cookieStore) createAuthRouter(options.neteaseProvider, options.qqProvider, logger, options.cookieStore)
+40
View File
@@ -18,6 +18,10 @@
<button class="control-btn" @click="store.next()"> <button class="control-btn" @click="store.next()">
<Icon icon="mdi:skip-next" /> <Icon icon="mdi:skip-next" />
</button> </button>
<button class="control-btn mode-btn" @click="cycleMode" :title="modeLabel">
<Icon :icon="modeIcon" />
<span class="mode-label">{{ modeLabel }}</span>
</button>
</div> </div>
<div class="player-right"> <div class="player-right">
@@ -66,6 +70,30 @@ function onVolumeChange(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
store.setVolume(parseInt(target.value)); store.setVolume(parseInt(target.value));
} }
const modeOrder = ['seq', 'loop', 'random', 'rloop'] as const;
const modeIcons: Record<string, string> = {
seq: 'mdi:arrow-right',
loop: 'mdi:repeat',
random: 'mdi:shuffle',
rloop: 'mdi:shuffle-variant',
};
const modeLabels: Record<string, string> = {
seq: '顺序',
loop: '循环',
random: '随机',
rloop: '随机循环',
};
const currentMode = computed(() => activeBot.value?.playMode ?? 'seq');
const modeIcon = computed(() => modeIcons[currentMode.value] ?? modeIcons.seq);
const modeLabel = computed(() => modeLabels[currentMode.value] ?? modeLabels.seq);
function cycleMode() {
const idx = modeOrder.indexOf(currentMode.value as typeof modeOrder[number]);
const next = modeOrder[(idx + 1) % modeOrder.length];
store.setMode(next);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -121,6 +149,18 @@ function onVolumeChange(e: Event) {
&:hover { opacity: 1; } &:hover { opacity: 1; }
} }
.mode-btn {
display: flex;
align-items: center;
gap: 4px;
font-size: 18px;
}
.mode-label {
font-size: 11px;
font-weight: 500;
}
.play-btn { .play-btn {
width: 32px; width: 32px;
height: 32px; height: 32px;
+16
View File
@@ -1,5 +1,9 @@
<template> <template>
<div class="history-page"> <div class="history-page">
<button class="back-btn" @click="$router.back()">
<Icon icon="mdi:arrow-left" />
返回
</button>
<h1 class="page-title">播放历史</h1> <h1 class="page-title">播放历史</h1>
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">加载中...</div>
@@ -24,6 +28,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue';
import axios from 'axios'; import axios from 'axios';
import { usePlayerStore } from '../stores/player.js'; import { usePlayerStore } from '../stores/player.js';
import SongCard from '../components/SongCard.vue'; import SongCard from '../components/SongCard.vue';
@@ -52,6 +57,17 @@ onMounted(async () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.back-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
opacity: 0.7;
margin-bottom: 16px;
transition: opacity var(--transition-fast);
&:hover { opacity: 1; }
}
.page-title { .page-title {
font-size: 28px; font-size: 28px;
font-weight: 800; font-weight: 800;
+19
View File
@@ -1,6 +1,10 @@
<template> <template>
<div class="lyrics-page" :style="bgStyle"> <div class="lyrics-page" :style="bgStyle">
<div class="lyrics-overlay" /> <div class="lyrics-overlay" />
<button class="back-btn" @click="$router.back()">
<Icon icon="mdi:arrow-left" />
返回
</button>
<div v-if="currentSong" class="lyrics-content"> <div v-if="currentSong" class="lyrics-content">
<div class="lyrics-left"> <div class="lyrics-left">
@@ -40,6 +44,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue';
import axios from 'axios'; import axios from 'axios';
import { usePlayerStore } from '../stores/player.js'; import { usePlayerStore } from '../stores/player.js';
import CoverArt from '../components/CoverArt.vue'; import CoverArt from '../components/CoverArt.vue';
@@ -175,6 +180,20 @@ onUnmounted(() => {
-webkit-backdrop-filter: blur(60px); -webkit-backdrop-filter: blur(60px);
} }
.back-btn {
position: absolute;
top: 24px;
left: 24px;
z-index: 2;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
transition: color var(--transition-fast);
&:hover { color: white; }
}
.lyrics-content { .lyrics-content {
position: relative; position: relative;
z-index: 1; z-index: 1;
+15
View File
@@ -1,5 +1,9 @@
<template> <template>
<div class="playlist-page"> <div class="playlist-page">
<button class="back-btn" @click="$router.back()">
<Icon icon="mdi:arrow-left" />
返回
</button>
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">加载中...</div>
<template v-else-if="playlist"> <template v-else-if="playlist">
@@ -101,6 +105,17 @@ onMounted(async () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.back-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
opacity: 0.7;
margin-bottom: 16px;
transition: opacity var(--transition-fast);
&:hover { opacity: 1; }
}
.playlist-hero { .playlist-hero {
display: flex; display: flex;
gap: 32px; gap: 32px;
+15
View File
@@ -1,5 +1,9 @@
<template> <template>
<div class="search-page"> <div class="search-page">
<button class="back-btn" @click="$router.back()">
<Icon icon="mdi:arrow-left" />
返回
</button>
<div class="search-header"> <div class="search-header">
<div class="search-input-wrap"> <div class="search-input-wrap">
<Icon icon="mdi:magnify" class="search-icon" /> <Icon icon="mdi:magnify" class="search-icon" />
@@ -89,6 +93,17 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.back-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
opacity: 0.7;
margin-bottom: 16px;
transition: opacity var(--transition-fast);
&:hover { opacity: 1; }
}
.search-header { .search-header {
margin-bottom: 24px; margin-bottom: 24px;
} }
+15
View File
@@ -1,5 +1,9 @@
<template> <template>
<div class="settings-page"> <div class="settings-page">
<button class="back-btn" @click="$router.back()">
<Icon icon="mdi:arrow-left" />
返回
</button>
<h1 class="page-title">设置</h1> <h1 class="page-title">设置</h1>
<!-- Theme Toggle --> <!-- Theme Toggle -->
@@ -385,6 +389,17 @@ onUnmounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.back-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
opacity: 0.7;
margin-bottom: 16px;
transition: opacity var(--transition-fast);
&:hover { opacity: 1; }
}
.page-title { .page-title {
font-size: 28px; font-size: 28px;
font-weight: 800; font-weight: 800;