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:
saopig1andClaude Sonnet 4.6 committed 2026-05-07 20:14:34 +08:00
1 parent 914180792d
commit 0f9c7b3c4c
2 files changed
+81 -1

No files matched your search

+53
View File
@@ -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>
+28 -1
View File
@@ -116,6 +116,10 @@
<label>服务器密码(可选)</label>
<input v-model="newBotServerPassword" class="input" type="password" placeholder="服务器有密码时填写" />
</div>
<div class="form-group">
<label>自定义头像(可选)</label>
<AvatarUpload v-model="newBotAvatar" />
</div>
<button class="btn-primary" @click="createBot">创建</button>
</div>
</section>
@@ -439,6 +443,13 @@
@change="updateProfile(bot.id, t.key, ($event.target as HTMLInputElement).checked)"
/>
</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>
@@ -450,6 +461,8 @@
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue';
import axios from 'axios';
import AvatarUpload from '../components/AvatarUpload.vue';
import CustomAvatarRow from '../components/CustomAvatarRow.vue';
import QRCode from 'qrcode';
import { usePlayerStore } from '../stores/player.js';
@@ -475,6 +488,7 @@ const newBotPort = ref(9987);
const newBotNickname = ref('MusicBot');
const newBotChannel = ref('');
const newBotServerPassword = ref('');
const newBotAvatar = ref<string | null>(null);
// Edit bot
const editingBot = ref<string | null>(null);
@@ -635,7 +649,7 @@ async function pollQrStatus(platform: string) {
async function createBot() {
if (!newBotName.value || !newBotServer.value) return;
try {
await axios.post('/api/bot', {
const res = await axios.post('/api/bot', {
name: newBotName.value,
serverAddress: newBotServer.value,
serverPort: newBotPort.value || 9987,
@@ -644,12 +658,20 @@ async function createBot() {
serverPassword: newBotServerPassword.value || undefined,
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 = '';
newBotServer.value = '';
newBotPort.value = 9987;
newBotNickname.value = 'MusicBot';
newBotChannel.value = '';
newBotServerPassword.value = '';
newBotAvatar.value = null;
await store.fetchBots();
} catch {
// Ignore
@@ -1408,6 +1430,11 @@ onUnmounted(() => {
}
}
.profile-toggle-static {
cursor: default;
align-items: flex-start;
}
@media (max-width: 768px) {
.profile-bot-header {
padding: 14px 12px;