mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat(web): custom avatar in create-bot + Settings
Adds AvatarUpload to the create-bot form (PUT on new bot id after POST) and a CustomAvatarRow per-bot in the profile-toggles section (GET on mount, PUT/DELETE on user action with initializing guard). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
914180792d
commit
0f9c7b3c4c
2 files changed
+81
-1
No files matched your search
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<AvatarUpload v-model="avatarDataUrl" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import AvatarUpload from './AvatarUpload.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ botId: string }>();
|
||||||
|
const avatarDataUrl = ref<string | null>(null);
|
||||||
|
let initializing = true;
|
||||||
|
|
||||||
|
async function loadCurrent() {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`/api/bot/${props.botId}/avatar`, { responseType: 'blob' });
|
||||||
|
const blob = res.data as Blob;
|
||||||
|
avatarDataUrl.value = await blobToDataUrl(blob);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.response?.status !== 404) {
|
||||||
|
console.warn('failed to load avatar', err);
|
||||||
|
}
|
||||||
|
avatarDataUrl.value = null;
|
||||||
|
} finally {
|
||||||
|
initializing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function blobToDataUrl(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(avatarDataUrl, async (newVal, oldVal) => {
|
||||||
|
if (initializing) return;
|
||||||
|
if (newVal === oldVal) return;
|
||||||
|
try {
|
||||||
|
if (newVal && newVal.startsWith('data:')) {
|
||||||
|
await axios.put(`/api/bot/${props.botId}/avatar`, { dataUrl: newVal });
|
||||||
|
} else if (newVal === null) {
|
||||||
|
await axios.delete(`/api/bot/${props.botId}/avatar`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('avatar update failed', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(loadCurrent);
|
||||||
|
</script>
|
||||||
@@ -116,6 +116,10 @@
|
|||||||
<label>服务器密码(可选)</label>
|
<label>服务器密码(可选)</label>
|
||||||
<input v-model="newBotServerPassword" class="input" type="password" placeholder="服务器有密码时填写" />
|
<input v-model="newBotServerPassword" class="input" type="password" placeholder="服务器有密码时填写" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>自定义头像(可选)</label>
|
||||||
|
<AvatarUpload v-model="newBotAvatar" />
|
||||||
|
</div>
|
||||||
<button class="btn-primary" @click="createBot">创建</button>
|
<button class="btn-primary" @click="createBot">创建</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -439,6 +443,13 @@
|
|||||||
@change="updateProfile(bot.id, t.key, ($event.target as HTMLInputElement).checked)"
|
@change="updateProfile(bot.id, t.key, ($event.target as HTMLInputElement).checked)"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="profileConfigs[bot.id]" class="profile-toggle profile-toggle-static">
|
||||||
|
<div class="profile-toggle-text">
|
||||||
|
<div class="profile-toggle-label">自定义头像</div>
|
||||||
|
<div class="profile-toggle-hint">无论封面同步是否开启,停播时都会回到这张图</div>
|
||||||
|
</div>
|
||||||
|
<CustomAvatarRow :bot-id="bot.id" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -450,6 +461,8 @@
|
|||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import AvatarUpload from '../components/AvatarUpload.vue';
|
||||||
|
import CustomAvatarRow from '../components/CustomAvatarRow.vue';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { usePlayerStore } from '../stores/player.js';
|
import { usePlayerStore } from '../stores/player.js';
|
||||||
|
|
||||||
@@ -475,6 +488,7 @@ const newBotPort = ref(9987);
|
|||||||
const newBotNickname = ref('MusicBot');
|
const newBotNickname = ref('MusicBot');
|
||||||
const newBotChannel = ref('');
|
const newBotChannel = ref('');
|
||||||
const newBotServerPassword = ref('');
|
const newBotServerPassword = ref('');
|
||||||
|
const newBotAvatar = ref<string | null>(null);
|
||||||
|
|
||||||
// Edit bot
|
// Edit bot
|
||||||
const editingBot = ref<string | null>(null);
|
const editingBot = ref<string | null>(null);
|
||||||
@@ -635,7 +649,7 @@ async function pollQrStatus(platform: string) {
|
|||||||
async function createBot() {
|
async function createBot() {
|
||||||
if (!newBotName.value || !newBotServer.value) return;
|
if (!newBotName.value || !newBotServer.value) return;
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/bot', {
|
const res = await axios.post('/api/bot', {
|
||||||
name: newBotName.value,
|
name: newBotName.value,
|
||||||
serverAddress: newBotServer.value,
|
serverAddress: newBotServer.value,
|
||||||
serverPort: newBotPort.value || 9987,
|
serverPort: newBotPort.value || 9987,
|
||||||
@@ -644,12 +658,20 @@ async function createBot() {
|
|||||||
serverPassword: newBotServerPassword.value || undefined,
|
serverPassword: newBotServerPassword.value || undefined,
|
||||||
autoStart: false,
|
autoStart: false,
|
||||||
});
|
});
|
||||||
|
if (newBotAvatar.value && res.data?.id) {
|
||||||
|
try {
|
||||||
|
await axios.put(`/api/bot/${res.data.id}/avatar`, { dataUrl: newBotAvatar.value });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('failed to set avatar on new bot', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
newBotName.value = '';
|
newBotName.value = '';
|
||||||
newBotServer.value = '';
|
newBotServer.value = '';
|
||||||
newBotPort.value = 9987;
|
newBotPort.value = 9987;
|
||||||
newBotNickname.value = 'MusicBot';
|
newBotNickname.value = 'MusicBot';
|
||||||
newBotChannel.value = '';
|
newBotChannel.value = '';
|
||||||
newBotServerPassword.value = '';
|
newBotServerPassword.value = '';
|
||||||
|
newBotAvatar.value = null;
|
||||||
await store.fetchBots();
|
await store.fetchBots();
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
@@ -1408,6 +1430,11 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-toggle-static {
|
||||||
|
cursor: default;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.profile-bot-header {
|
.profile-bot-header {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
|
|||||||
Reference in new issue
Block a user