mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
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:
1 parent
9dce883384
commit
ee5a75ab5e
8 files changed
+140
-3
No files matched your search
+19
-2
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user