Files
maimaiDX-API-Web-Server/frontend/src/views/ProfileView.vue

397 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import logger from '@/utils/logger'
// --- API Configuration ---
const apiBaseUrl = computed(() => {
return `http://${window.location.hostname}:8000/api`
})
// --- State Management ---
const inputUserId = ref('')
const isLoading = ref(false)
const userData = ref<any>(null)
const previewData = ref<any>(null)
const musicResults = ref<any[] | null>(null)
const anyData = ref<any>(null)
// AimeDB Scan State
const qrContent = ref('')
const isScanning = ref(false)
// Action-specific state
const fishImportToken = ref('')
const itemKind = ref('')
const itemIds = ref('')
const ticketType = ref('')
const apiName = ref('')
const scoreForm = reactive({
musicId: '',
levelId: '',
achievement: '',
dxScore: ''
})
// --- Generic Action Wrapper ---
async function performAction(action: () => Promise<void>, confirmOptions?: { title: string; message: string }) {
if (!inputUserId.value) {
ElMessage.error('请先输入用户ID');
logger.logWarn('Perform action failed: User ID is empty');
return;
}
if (confirmOptions) {
try {
await ElMessageBox.confirm(confirmOptions.message, confirmOptions.title, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
} catch {
ElMessage.info('操作已取消');
logger.logInfo('Action cancelled by user');
return;
}
}
isLoading.value = true;
try {
await action();
} catch (e: any) {
const message = e.response?.data?.detail || e.message;
logger.logError('Action failed', { error: message, stack: e.stack });
ElMessage.error(message);
console.error(e);
} finally {
isLoading.value = false;
}
}
// --- AimeDB Scan Function ---
async function scanAimeQR() {
if (!qrContent.value) {
ElMessage.error('请输入二维码内容')
logger.logWarn('Aime scan failed: QR content is empty')
return
}
logger.logInfo('Starting Aime QR scan', { qrContent: qrContent.value.substring(0, 20) + '...' })
isScanning.value = true
try {
const requestData = { qrContent: qrContent.value }
logger.logApiRequest('POST', '/api/aime_scan', requestData)
const { data } = await axios.post(`${apiBaseUrl.value}/aime_scan`, requestData)
logger.logApiResponse(200, data)
if (data.errorID) {
logger.logWarn('Aime scan failed', { errorID: data.errorID })
ElMessage.error(`扫描失败: 错误代码 ${data.errorID}`)
} else if (data.userID) {
inputUserId.value = data.userID
logger.logInfo('Aime scan successful', { userID: data.userID })
ElMessage.success('二维码扫描成功用户ID已填充')
} else {
logger.logWarn('Aime scan returned no userID')
ElMessage.error('扫描成功但未返回有效用户ID')
}
} catch (e: any) {
logger.logError('Aime scan failed with exception', e)
const message = e.response?.data?.detail || e.message
ElMessage.error(`扫描失败: ${message}`)
console.error(e)
} finally {
isScanning.value = false
}
}
// --- API Functions ---
const fetchFullProfile = () => {
if (!inputUserId.value) {
ElMessage.error('请输入用户ID');
logger.logWarn('Fetch full profile failed: User ID is empty');
return;
}
logger.logInfo('Fetching full profile', { userId: inputUserId.value });
performAction(async () => {
const url = `${apiBaseUrl.value}/user/all?userId=${inputUserId.value}`;
logger.logApiRequest('GET', url);
const { data } = await axios.get(url);
logger.logApiResponse(200, data);
if (data.error) throw new Error(data.error);
userData.value = data.upsertUserAll.userData[0];
previewData.value = null;
musicResults.value = null;
ElMessage.success('已加载完整用户信息');
logger.logInfo('Successfully loaded full profile', { userId: inputUserId.value });
});
};
const fetchPreview = () => {
if (!inputUserId.value) {
ElMessage.error('请输入用户ID');
logger.logWarn('Fetch preview failed: User ID is empty');
return;
}
logger.logInfo('Fetching user preview', { userId: inputUserId.value });
performAction(async () => {
const url = `${apiBaseUrl.value}/user/preview?userId=${inputUserId.value}`;
logger.logApiRequest('GET', url);
const { data } = await axios.get(url);
logger.logApiResponse(200, data);
if (data.error) throw new Error(data.error);
previewData.value = data;
userData.value = null;
musicResults.value = null;
ElMessage.success('已加载用户预览信息');
logger.logInfo('Successfully loaded user preview', { userId: inputUserId.value });
});
};
const fetchScores = () => {
logger.logInfo('Fetching user scores', { userId: inputUserId.value });
performAction(async () => {
const url = `${apiBaseUrl.value}/user/music_results?userId=${inputUserId.value}`;
logger.logApiRequest('GET', url);
const { data } = await axios.get(url);
logger.logApiResponse(200, data);
if (data.error) throw new Error(data.error);
musicResults.value = data.userMusicResults;
ElMessage.success(`已找到 ${musicResults.value?.length || 0} 条分数记录`);
logger.logInfo('Successfully loaded user scores', { userId: inputUserId.value, count: musicResults.value?.length || 0 });
})};
// --- Action Functions ---
const claimBonus = () => {
logger.logInfo('Claiming login bonus', { userId: inputUserId.value });
performAction(async () => {
const url = `${apiBaseUrl.value}/action/claim_login_bonus`;
const requestData = { userId: inputUserId.value };
logger.logApiRequest('POST', url, requestData);
const { data } = await axios.post(url, requestData);
logger.logApiResponse(200, data);
if (data.error) throw new Error(data.error);
ElMessage.success(data.message);
logger.logInfo('Successfully claimed login bonus', { userId: inputUserId.value });
})};
const doUnlockItem = () => {
logger.logInfo('Unlocking item', { userId: inputUserId.value, itemKind: itemKind.value, itemIds: itemIds.value });
performAction(async () => {
if (!itemKind.value || !itemIds.value) throw new Error('道具种类和ID不能为空');
const payload = { userId: inputUserId.value, itemKind: itemKind.value, itemIds: itemIds.value };
const url = `${apiBaseUrl.value}/action/unlock_item`;
logger.logApiRequest('POST', url, payload);
const { data } = await axios.post(url, payload);
logger.logApiResponse(200, data);
if (data.error) throw new Error(data.error);
ElMessage.success(data.message);
logger.logInfo('Successfully unlocked item', { userId: inputUserId.value, itemKind: itemKind.value });
})};
const uploadToFish = () => performAction(async () => {
if (!fishImportToken.value) throw new Error('水鱼查分器导入Token不能为空')
const payload = { userId: inputUserId.value, fishImportToken: fishImportToken.value }
const { data } = await axios.post(`${apiBaseUrl.value}/user/upload_to_diving_fish`, payload)
if (data.error) throw new Error(data.error)
ElMessage.success(data.message)
})
const buyTicket = () => performAction(async () => {
if (!ticketType.value) throw new Error('票券类型不能为空')
const payload = { userId: inputUserId.value, ticketType: ticketType.value }
const { data } = await axios.post(`${apiBaseUrl.value}/action/buy_ticket`, payload)
if (data.error) throw new Error(data.error)
ElMessage.success(data.message)
})
const forceLogout = () => performAction(async () => {
const { data } = await axios.post(`${apiBaseUrl.value}/action/force_logout`, { userId: inputUserId.value })
if (data.error) throw new Error(data.error)
ElMessage.info(data.message)
}, { title: '确认强制登出', message: '这会尝试将用户从当前登录的机台上强制登出,确定吗?' })
const wipeTickets = () => performAction(async () => {
const { data } = await axios.post(`${apiBaseUrl.value}/action/wipe_tickets`, { userId: inputUserId.value })
if (data.error) throw new Error(data.error)
ElMessage.success(data.message)
}, { title: '确认清空票券', message: '此操作会清空该用户的所有票券,确定吗?' })
const uploadScore = () => performAction(async () => {
const { musicId, levelId, achievement, dxScore } = scoreForm
if (!musicId || !levelId || !achievement || !dxScore) throw new Error('上传分数所需的所有字段均不能为空')
const payload = { ...scoreForm, userId: inputUserId.value }
const { data } = await axios.post(`${apiBaseUrl.value}/action/upload_score`, payload)
if (data.error) throw new Error(data.error)
ElMessage.success(data.message)
})
const deleteScore = () => performAction(async () => {
const { musicId, levelId } = scoreForm
if (!musicId || !levelId) throw new Error('删除分数需要乐曲ID和难度ID')
const payload = { musicId, levelId, userId: inputUserId.value }
const { data } = await axios.post(`${apiBaseUrl.value}/action/delete_score`, payload)
if (data.error) throw new Error(data.error)
ElMessage.success(data.message)
}, { title: '确认删除分数', message: `确定要删除乐曲 ${scoreForm.musicId} (难度 ${scoreForm.levelId}) 的分数记录吗?` })
const getAny = () => performAction(async () => {
if (!apiName.value) throw new Error('API名称不能为空')
const { data } = await axios.get(`${apiBaseUrl.value}/get_any?userId=${inputUserId.value}&apiName=${apiName.value}`)
if (data.error) throw new Error(data.error)
anyData.value = data
ElMessage.success(`已成功调用 ${apiName.value}`)
})
</script>
<template>
<el-space direction="vertical" alignment="start" :size="20" style="width: 100%;">
<el-card header="用户选择" style="width: 100%;">
<el-form :inline="true" @submit.prevent="fetchFullProfile">
<el-form-item label="用户ID">
<el-input v-model="inputUserId" placeholder="请输入用户ID" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchFullProfile" :loading="isLoading">获取完整信息</el-button>
<el-button @click="fetchPreview" :loading="isLoading">获取预览</el-button>
</el-form-item>
</el-form>
<el-divider />
<h3>Aime卡扫描</h3>
<el-form :inline="true" @submit.prevent="scanAimeQR">
<el-form-item label="二维码内容">
<el-input v-model="qrContent" placeholder="请输入Aime卡二维码内容" clearable style="width: 300px;" />
</el-form-item>
<el-form-item>
<el-button type="success" @click="scanAimeQR" :loading="isScanning">扫描并填充用户ID</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="card-container">
<el-card v-if="userData" header="完整用户信息" class="data-card">
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">{{ userData.userName }}</el-descriptions-item>
<el-descriptions-item label="Rating">{{ userData.playerRating }}</el-descriptions-item>
<el-descriptions-item label="最高Rating">{{ userData.highestRating }}</el-descriptions-item>
<el-descriptions-item label="游玩次数">{{ userData.playCount }}</el-descriptions-item>
<el-descriptions-item label="封禁状态">{{ userData.banState }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card v-if="previewData" header="用户预览" class="data-card">
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">{{ previewData.userName }}</el-descriptions-item>
<el-descriptions-item label="Rating">{{ previewData.playerRating }}</el-descriptions-item>
<el-descriptions-item label="是否在线">{{ previewData.isLogin }}</el-descriptions-item>
<el-descriptions-item label="封禁状态">{{ previewData.banState }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card v-if="inputUserId" header="玩家操作" class="data-card">
<el-collapse accordion>
<el-collapse-item title="通用操作" name="1">
<el-button type="success" @click="claimBonus" :loading="isLoading">领取登录奖励</el-button>
</el-collapse-item>
<el-collapse-item title="道具管理" name="2">
<p>解锁道具 (例如: 种类 10 为搭档)</p>
<el-form :inline="true" @submit.prevent="doUnlockItem">
<el-form-item><el-input v-model="itemKind" placeholder="道具种类" /></el-form-item>
<el-form-item><el-input v-model="itemIds" placeholder="道具ID (英文逗号分隔)" /></el-form-item>
<el-form-item><el-button type="warning" @click="doUnlockItem" :loading="isLoading">解锁</el-button></el-form-item>
</el-form>
</el-collapse-item>
<el-collapse-item title="票券管理" name="3">
<el-form :inline="true" @submit.prevent="buyTicket">
<el-form-item><el-input v-model="ticketType" placeholder="票券种类" /></el-form-item>
<el-form-item><el-button type="primary" @click="buyTicket" :loading="isLoading">购买</el-button></el-form-item>
</el-form>
</el-collapse-item>
<el-collapse-item title="分数管理" name="4">
<el-tabs type="border-card">
<el-tab-pane label="上传分数">
<el-form :inline="true" @submit.prevent="uploadScore">
<el-form-item><el-input v-model="scoreForm.musicId" placeholder="乐曲ID" /></el-form-item>
<el-form-item><el-input v-model="scoreForm.levelId" placeholder="难度 (0-4)" /></el-form-item>
<el-form-item><el-input v-model="scoreForm.achievement" placeholder="达成率 (例如: 1005000)" /></el-form-item>
<el-form-item><el-input v-model="scoreForm.dxScore" placeholder="DX分数" /></el-form-item>
<el-form-item><el-button type="primary" @click="uploadScore" :loading="isLoading">上传</el-button></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="删除分数">
<el-form :inline="true" @submit.prevent="deleteScore">
<el-form-item><el-input v-model="scoreForm.musicId" placeholder="乐曲ID" /></el-form-item>
<el-form-item><el-input v-model="scoreForm.levelId" placeholder="难度 (0-4)" /></el-form-item>
<el-form-item><el-button type="danger" @click="deleteScore" :loading="isLoading">删除</el-button></el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-collapse-item>
<el-collapse-item title="高级/危险操作" name="5">
<el-form :inline="true" @submit.prevent="uploadToFish">
<el-form-item label="上传至水鱼"><el-input v-model="fishImportToken" type="password" placeholder="水鱼查分器Token" show-password /></el-form-item>
<el-form-item><el-button type="info" @click="uploadToFish" :loading="isLoading">上传</el-button></el-form-item>
</el-form>
<el-divider />
<el-form :inline="true" @submit.prevent="getAny">
<el-form-item label="通用接口调用"><el-input v-model="apiName" placeholder="API名称 (例如: GetUserChargeApi)" /></el-form-item>
<el-form-item><el-button type="info" @click="getAny" :loading="isLoading">调用</el-button></el-form-item>
</el-form>
<el-card v-if="anyData" header="Result" style="margin-top: 10px;">
<pre>{{ JSON.stringify(anyData, null, 2) }}</pre>
</el-card>
<el-divider />
<el-button type="danger" @click="wipeTickets" :loading="isLoading">清空所有票券</el-button>
<el-button type="danger" @click="forceLogout" :loading="isLoading">强制登出</el-button>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
<el-card v-if="inputUserId" header="分数详情" style="width: 100%;">
<el-button @click="fetchScores" :loading="isLoading">获取所有分数</el-button>
<el-table v-if="musicResults" :data="musicResults" stripe style="width: 100%; margin-top: 20px;" height="400">
<el-table-column prop="musicId" label="乐曲ID" sortable />
<el-table-column prop="level" label="难度" sortable />
<el-table-column label="达成率" sortable>
<template #default="scope">
{{ (scope.row.achievement / 10000).toFixed(4) }}%
</template>
</el-table-column>
<el-table-column prop="playCount" label="游玩次数" sortable />
</el-table>
</el-card>
</el-space>
</template>
<style scoped>
.card-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.data-card {
width: 100%;
margin-bottom: 20px;
}
@media (min-width: 768px) {
.data-card {
width: 450px;
}
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
background-color: #f4f4f5;
padding: 10px;
border-radius: 4px;
}
</style>