mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-28 10:57:23 +08:00
feat: 新增歌单导入功能
添加歌单导入功能,支持通过链接、文本和元数据三种方式导入歌单 - 实现链接导入、文本导入和元数据导入三种方式 - 添加导入状态检查和显示功能
This commit is contained in:
@@ -27,6 +27,7 @@ export default {
|
|||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
|
back: 'Back',
|
||||||
copySuccess: 'Copied to clipboard',
|
copySuccess: 'Copied to clipboard',
|
||||||
copyFailed: 'Copy failed',
|
copyFailed: 'Copy failed',
|
||||||
validation: {
|
validation: {
|
||||||
|
|||||||
@@ -120,5 +120,65 @@ export default {
|
|||||||
addToPlaylistSuccess: 'Add to Playlist Success',
|
addToPlaylistSuccess: 'Add to Playlist Success',
|
||||||
operationFailed: 'Operation Failed',
|
operationFailed: 'Operation Failed',
|
||||||
songsAlreadyInPlaylist: 'Songs already in playlist'
|
songsAlreadyInPlaylist: 'Songs already in playlist'
|
||||||
|
},
|
||||||
|
playlist: {
|
||||||
|
import: {
|
||||||
|
button: 'Import Playlist',
|
||||||
|
title: 'Import Playlist',
|
||||||
|
description: 'Import playlists via metadata, text, or links',
|
||||||
|
linkTab: 'Import by Link',
|
||||||
|
textTab: 'Import by Text',
|
||||||
|
localTab: 'Import by Metadata',
|
||||||
|
linkPlaceholder: 'Enter playlist links, one per line',
|
||||||
|
textPlaceholder: 'Enter song information in format: Song Name Artist Name',
|
||||||
|
localPlaceholder: 'Enter song metadata in JSON format',
|
||||||
|
linkTips: 'Supported link sources:',
|
||||||
|
linkTip1: 'Copy links after sharing playlists to WeChat/Weibo/QQ',
|
||||||
|
linkTip2: 'Directly copy playlist/profile links',
|
||||||
|
linkTip3: 'Directly copy article links',
|
||||||
|
textTips: 'Enter song information, one song per line',
|
||||||
|
textFormat: 'Format: Song Name Artist Name',
|
||||||
|
localTips: 'Add song metadata',
|
||||||
|
localFormat: 'Format example:',
|
||||||
|
songNamePlaceholder: 'Song Name',
|
||||||
|
artistNamePlaceholder: 'Artist Name',
|
||||||
|
albumNamePlaceholder: 'Album Name',
|
||||||
|
addSongButton: 'Add Song',
|
||||||
|
addLinkButton: 'Add Link',
|
||||||
|
importToStarPlaylist: 'Import to My Favorite Music',
|
||||||
|
playlistNamePlaceholder: 'Enter playlist name',
|
||||||
|
importButton: 'Start Import',
|
||||||
|
emptyLinkWarning: 'Please enter playlist links',
|
||||||
|
emptyTextWarning: 'Please enter song information',
|
||||||
|
emptyLocalWarning: 'Please enter song metadata',
|
||||||
|
invalidJsonFormat: 'Invalid JSON format',
|
||||||
|
importSuccess: 'Import task created successfully',
|
||||||
|
importFailed: 'Import failed',
|
||||||
|
importStatus: 'Import Status',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
taskId: 'Task ID',
|
||||||
|
status: 'Status',
|
||||||
|
successCount: 'Success Count',
|
||||||
|
failReason: 'Failure Reason',
|
||||||
|
unknownError: 'Unknown error',
|
||||||
|
statusPending: 'Pending',
|
||||||
|
statusProcessing: 'Processing',
|
||||||
|
statusSuccess: 'Success',
|
||||||
|
statusFailed: 'Failed',
|
||||||
|
statusUnknown: 'Unknown',
|
||||||
|
taskList: 'Task List',
|
||||||
|
taskListTitle: 'Import Task List',
|
||||||
|
action: 'Action',
|
||||||
|
select: 'Select',
|
||||||
|
fetchTaskListFailed: 'Failed to fetch task list',
|
||||||
|
noTasks: 'No import tasks',
|
||||||
|
clearTasks: 'Clear Tasks',
|
||||||
|
clearTasksConfirmTitle: 'Confirm Clear',
|
||||||
|
clearTasksConfirmContent: 'Are you sure you want to clear all import task records? This action cannot be undone.',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
clearTasksSuccess: 'Task list cleared',
|
||||||
|
clearTasksFailed: 'Failed to clear task list'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default {
|
|||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
retry: '重试',
|
retry: '重试',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
|
back: '返回',
|
||||||
copySuccess: '已复制到剪贴板',
|
copySuccess: '已复制到剪贴板',
|
||||||
copyFailed: '复制失败',
|
copyFailed: '复制失败',
|
||||||
validation: {
|
validation: {
|
||||||
|
|||||||
@@ -118,5 +118,65 @@ export default {
|
|||||||
addToPlaylist: '添加到播放列表',
|
addToPlaylist: '添加到播放列表',
|
||||||
addToPlaylistSuccess: '添加到播放列表成功',
|
addToPlaylistSuccess: '添加到播放列表成功',
|
||||||
songsAlreadyInPlaylist: '歌曲已存在于播放列表中'
|
songsAlreadyInPlaylist: '歌曲已存在于播放列表中'
|
||||||
|
},
|
||||||
|
playlist: {
|
||||||
|
import: {
|
||||||
|
button: '歌单导入',
|
||||||
|
title: '歌单导入',
|
||||||
|
description: '支持通过元数据/文字/链接三种方式导入歌单',
|
||||||
|
linkTab: '链接导入',
|
||||||
|
textTab: '文字导入',
|
||||||
|
localTab: '元数据导入',
|
||||||
|
linkPlaceholder: '请输入歌单链接,每行一个',
|
||||||
|
textPlaceholder: '请输入歌曲信息,格式为:歌曲名 歌手名',
|
||||||
|
localPlaceholder: '请输入JSON格式的歌曲元数据',
|
||||||
|
linkTips: '支持的链接来源:',
|
||||||
|
linkTip1: '将歌单分享到微信/微博/QQ后复制链接',
|
||||||
|
linkTip2: '直接复制歌单/个人主页链接',
|
||||||
|
linkTip3: '直接复制文章链接',
|
||||||
|
textTips: '请输入歌曲信息,每行一首歌',
|
||||||
|
textFormat: '格式:歌曲名 歌手名',
|
||||||
|
localTips: '请添加歌曲元数据',
|
||||||
|
localFormat: '格式示例:',
|
||||||
|
songNamePlaceholder: '歌曲名称',
|
||||||
|
artistNamePlaceholder: '艺术家名称',
|
||||||
|
albumNamePlaceholder: '专辑名称',
|
||||||
|
addSongButton: '添加歌曲',
|
||||||
|
addLinkButton: '添加链接',
|
||||||
|
importToStarPlaylist: '导入到我喜欢的音乐',
|
||||||
|
playlistNamePlaceholder: '请输入歌单名称',
|
||||||
|
importButton: '开始导入',
|
||||||
|
emptyLinkWarning: '请输入歌单链接',
|
||||||
|
emptyTextWarning: '请输入歌曲信息',
|
||||||
|
emptyLocalWarning: '请输入歌曲元数据',
|
||||||
|
invalidJsonFormat: 'JSON格式不正确',
|
||||||
|
importSuccess: '导入任务创建成功',
|
||||||
|
importFailed: '导入失败',
|
||||||
|
importStatus: '导入状态',
|
||||||
|
refresh: '刷新',
|
||||||
|
taskId: '任务ID',
|
||||||
|
status: '状态',
|
||||||
|
successCount: '成功数量',
|
||||||
|
failReason: '失败原因',
|
||||||
|
unknownError: '未知错误',
|
||||||
|
statusPending: '等待处理',
|
||||||
|
statusProcessing: '处理中',
|
||||||
|
statusSuccess: '导入成功',
|
||||||
|
statusFailed: '导入失败',
|
||||||
|
statusUnknown: '未知状态',
|
||||||
|
taskList: '任务列表',
|
||||||
|
taskListTitle: '导入任务列表',
|
||||||
|
action: '操作',
|
||||||
|
select: '选择',
|
||||||
|
fetchTaskListFailed: '获取任务列表失败',
|
||||||
|
noTasks: '暂无导入任务',
|
||||||
|
clearTasks: '清除任务',
|
||||||
|
clearTasksConfirmTitle: '确认清除',
|
||||||
|
clearTasksConfirmContent: '确定要清除所有导入任务记录吗?此操作不可恢复。',
|
||||||
|
confirm: '确认',
|
||||||
|
cancel: '取消',
|
||||||
|
clearTasksSuccess: '任务列表已清除',
|
||||||
|
clearTasksFailed: '清除任务列表失败'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌单导入 - 元数据/文字/链接导入
|
||||||
|
* @param params 导入参数
|
||||||
|
*/
|
||||||
|
export function importPlaylist(params: {
|
||||||
|
local?: string;
|
||||||
|
text?: string;
|
||||||
|
link?: string;
|
||||||
|
importStarPlaylist?: boolean;
|
||||||
|
playlistName?: string;
|
||||||
|
}) {
|
||||||
|
return request.post('/playlist/import/name/task/create', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 歌单导入 - 任务状态
|
||||||
|
* @param id 任务ID
|
||||||
|
*/
|
||||||
|
export function getImportTaskStatus(id: string | number) {
|
||||||
|
return request({
|
||||||
|
url: '/playlist/import/task/status',
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'vue' {
|
|||||||
NBadge: typeof import('naive-ui')['NBadge']
|
NBadge: typeof import('naive-ui')['NBadge']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCarousel: typeof import('naive-ui')['NCarousel']
|
NCarousel: typeof import('naive-ui')['NCarousel']
|
||||||
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
|
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ const otherRouter = [
|
|||||||
back: true
|
back: true
|
||||||
},
|
},
|
||||||
component: () => import('@/views/music/MusicListPage.vue')
|
component: () => import('@/views/music/MusicListPage.vue')
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/playlist/import',
|
||||||
|
name: 'playlistImport',
|
||||||
|
meta: {
|
||||||
|
title: '歌单导入',
|
||||||
|
keepAlive: true,
|
||||||
|
back: true
|
||||||
|
},
|
||||||
|
component: () => import('@/views/playlist/ImportPlaylist.vue')
|
||||||
|
},
|
||||||
];
|
];
|
||||||
export default otherRouter;
|
export default otherRouter;
|
||||||
|
|||||||
@@ -0,0 +1,627 @@
|
|||||||
|
<template>
|
||||||
|
<div class="import-playlist-page">
|
||||||
|
<div class="import-header" :class="setAnimationClass('animate__fadeInLeft')">
|
||||||
|
<div class="import-header-left">
|
||||||
|
<h2>{{ t('comp.playlist.import.title') }}</h2>
|
||||||
|
<div class="import-desc">{{ t('comp.playlist.import.description') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="import-content" :class="setAnimationClass('animate__fadeInUp')">
|
||||||
|
<n-card class="import-card">
|
||||||
|
<n-tabs type="line" animated>
|
||||||
|
<!-- 链接导入 -->
|
||||||
|
<n-tab-pane name="link" :tab="t('comp.playlist.import.linkTab')">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="link-inputs">
|
||||||
|
<div v-for="(link, index) in linkInputs" :key="index" class="link-row">
|
||||||
|
<n-input
|
||||||
|
v-model:value="link.value"
|
||||||
|
:placeholder="t('comp.playlist.import.linkPlaceholder')"
|
||||||
|
class="link-input"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
quaternary
|
||||||
|
circle
|
||||||
|
type="error"
|
||||||
|
@click="removeLinkRow(index)"
|
||||||
|
v-if="linkInputs.length > 1"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-delete-bin-line"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="link-actions">
|
||||||
|
<n-button @click="addLinkRow" secondary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-add-line"></i>
|
||||||
|
</template>
|
||||||
|
{{ t('comp.playlist.import.addLinkButton') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="link-tips">
|
||||||
|
<p>{{ t('comp.playlist.import.linkTips') }}</p>
|
||||||
|
<ul>
|
||||||
|
<li>{{ t('comp.playlist.import.linkTip1') }}</li>
|
||||||
|
<li>{{ t('comp.playlist.import.linkTip2') }}</li>
|
||||||
|
<li>{{ t('comp.playlist.import.linkTip3') }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<n-checkbox v-model:checked="importToStarPlaylist">
|
||||||
|
{{ t('comp.playlist.import.importToStarPlaylist') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input
|
||||||
|
v-if="!importToStarPlaylist"
|
||||||
|
v-model:value="playlistName"
|
||||||
|
:placeholder="t('comp.playlist.import.playlistNamePlaceholder')"
|
||||||
|
class="playlist-name-input"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="importing"
|
||||||
|
:disabled="!isLinkInputValid"
|
||||||
|
@click="handleImportByLink"
|
||||||
|
>
|
||||||
|
{{ t('comp.playlist.import.importButton') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 文字导入 -->
|
||||||
|
<n-tab-pane name="text" :tab="t('comp.playlist.import.textTab')">
|
||||||
|
<div class="tab-content">
|
||||||
|
<n-input
|
||||||
|
v-model:value="textInput"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="t('comp.playlist.import.textPlaceholder')"
|
||||||
|
:rows="6"
|
||||||
|
/>
|
||||||
|
<div class="text-tips">
|
||||||
|
<p>{{ t('comp.playlist.import.textTips') }}</p>
|
||||||
|
<p class="text-format">{{ t('comp.playlist.import.textFormat') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<n-checkbox v-model:checked="importToStarPlaylist">
|
||||||
|
{{ t('comp.playlist.import.importToStarPlaylist') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input
|
||||||
|
v-if="!importToStarPlaylist"
|
||||||
|
v-model:value="playlistName"
|
||||||
|
:placeholder="t('comp.playlist.import.playlistNamePlaceholder')"
|
||||||
|
class="playlist-name-input"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="importing"
|
||||||
|
:disabled="!textInput.trim()"
|
||||||
|
@click="handleImportByText"
|
||||||
|
>
|
||||||
|
{{ t('comp.playlist.import.importButton') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 元数据导入 -->
|
||||||
|
<n-tab-pane name="local" :tab="t('comp.playlist.import.localTab')">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="metadata-inputs">
|
||||||
|
<div v-for="(item, index) in localMetadata" :key="index" class="metadata-row">
|
||||||
|
<n-input
|
||||||
|
v-model:value="item.name"
|
||||||
|
:placeholder="t('comp.playlist.import.songNamePlaceholder')"
|
||||||
|
class="metadata-input"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="item.artist"
|
||||||
|
:placeholder="t('comp.playlist.import.artistNamePlaceholder')"
|
||||||
|
class="metadata-input"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="item.album"
|
||||||
|
:placeholder="t('comp.playlist.import.albumNamePlaceholder')"
|
||||||
|
class="metadata-input"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
quaternary
|
||||||
|
circle
|
||||||
|
type="error"
|
||||||
|
@click="removeMetadataRow(index)"
|
||||||
|
v-if="localMetadata.length > 1"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-delete-bin-line"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-actions">
|
||||||
|
<n-button @click="addMetadataRow" secondary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-add-line"></i>
|
||||||
|
</template>
|
||||||
|
{{ t('comp.playlist.import.addSongButton') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="local-tips">
|
||||||
|
<p>{{ t('comp.playlist.import.localTips') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<n-checkbox v-model:checked="importToStarPlaylist">
|
||||||
|
{{ t('comp.playlist.import.importToStarPlaylist') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input
|
||||||
|
v-if="!importToStarPlaylist"
|
||||||
|
v-model:value="playlistName"
|
||||||
|
:placeholder="t('comp.playlist.import.playlistNamePlaceholder')"
|
||||||
|
class="playlist-name-input"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="importing"
|
||||||
|
:disabled="!isLocalMetadataValid"
|
||||||
|
@click="handleImportByLocal"
|
||||||
|
>
|
||||||
|
{{ t('comp.playlist.import.importButton') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 导入状态 -->
|
||||||
|
<n-card v-if="taskId" class="import-status-card">
|
||||||
|
<div class="status-header">
|
||||||
|
<h3>{{ t('comp.playlist.import.importStatus') }}</h3>
|
||||||
|
<n-button text @click="refreshStatus">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-refresh-line"></i>
|
||||||
|
</template>
|
||||||
|
{{ t('comp.playlist.import.refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<n-spin :show="checkingStatus">
|
||||||
|
<div class="status-content">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">{{ t('comp.playlist.import.taskId') }}:</span>
|
||||||
|
<span class="status-value">{{ taskId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">{{ t('comp.playlist.import.status') }}:</span>
|
||||||
|
<span class="status-value" :class="`status-${taskStatus}`">
|
||||||
|
{{ getStatusText(taskStatus) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="taskStatus === 'success'" class="status-item">
|
||||||
|
<span class="status-label">{{ t('comp.playlist.import.successCount') }}:</span>
|
||||||
|
<span class="status-value success-count">{{ successCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="taskStatus === 'failed'" class="status-item">
|
||||||
|
<span class="status-label">{{ t('comp.playlist.import.failReason') }}:</span>
|
||||||
|
<span class="status-value fail-reason">{{ failReason }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
import { importPlaylist, getImportTaskStatus } from '@/api/playlist';
|
||||||
|
import { setAnimationClass } from '@/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const linkInputs = ref([{ value: '' }]);
|
||||||
|
const textInput = ref('');
|
||||||
|
const localMetadata = ref([{ name: '', artist: '', album: '' }]);
|
||||||
|
const playlistName = ref('');
|
||||||
|
const importToStarPlaylist = ref(false);
|
||||||
|
|
||||||
|
// 链接相关函数
|
||||||
|
const addLinkRow = () => {
|
||||||
|
linkInputs.value.push({ value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLinkRow = (index: number) => {
|
||||||
|
linkInputs.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证链接是否有效
|
||||||
|
const isLinkInputValid = computed(() => {
|
||||||
|
return linkInputs.value.some(item => item.value.trim() !== '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 元数据相关函数
|
||||||
|
const addMetadataRow = () => {
|
||||||
|
localMetadata.value.push({ name: '', artist: '', album: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMetadataRow = (index: number) => {
|
||||||
|
localMetadata.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证元数据是否有效
|
||||||
|
const isLocalMetadataValid = computed(() => {
|
||||||
|
return localMetadata.value.some(item => item.name.trim() !== '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导入状态
|
||||||
|
const importing = ref(false);
|
||||||
|
const taskId = ref('');
|
||||||
|
const taskStatus = ref('');
|
||||||
|
const successCount = ref(0);
|
||||||
|
const failReason = ref('');
|
||||||
|
const checkingStatus = ref(false);
|
||||||
|
const statusCheckInterval = ref<number | null>(null);
|
||||||
|
|
||||||
|
// 处理链接导入
|
||||||
|
const handleImportByLink = async () => {
|
||||||
|
if (!isLinkInputValid.value) {
|
||||||
|
message.warning(t('comp.playlist.import.emptyLinkWarning'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
// 处理链接格式
|
||||||
|
const links = linkInputs.value
|
||||||
|
.filter(link => link.value.trim())
|
||||||
|
.map(link => link.value.trim());
|
||||||
|
|
||||||
|
const encodedLinks = JSON.stringify(links);
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
link: encodedLinks
|
||||||
|
};
|
||||||
|
|
||||||
|
if (importToStarPlaylist.value) {
|
||||||
|
params.importStarPlaylist = true;
|
||||||
|
} else if (playlistName.value) {
|
||||||
|
params.playlistName = playlistName.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await importPlaylist(params);
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
message.success(t('comp.playlist.import.importSuccess'));
|
||||||
|
taskId.value = res.data.data.taskId;
|
||||||
|
startStatusCheck();
|
||||||
|
} else {
|
||||||
|
message.error(res.data.message || t('comp.playlist.import.importFailed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入歌单失败:', error);
|
||||||
|
message.error(t('comp.playlist.import.importFailed'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文字导入
|
||||||
|
const handleImportByText = async () => {
|
||||||
|
if (!textInput.value.trim()) {
|
||||||
|
message.warning(t('comp.playlist.import.emptyTextWarning'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
const encodedText = encodeURIComponent(textInput.value);
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
text: encodedText
|
||||||
|
};
|
||||||
|
|
||||||
|
if (importToStarPlaylist.value) {
|
||||||
|
params.importStarPlaylist = true;
|
||||||
|
} else if (playlistName.value) {
|
||||||
|
params.playlistName = playlistName.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await importPlaylist(params);
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
message.success(t('comp.playlist.import.importSuccess'));
|
||||||
|
taskId.value = res.data.data.taskId;
|
||||||
|
startStatusCheck();
|
||||||
|
} else {
|
||||||
|
message.error(res.data.message || t('comp.playlist.import.importFailed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入歌单失败:', error);
|
||||||
|
message.error(t('comp.playlist.import.importFailed'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理元数据导入
|
||||||
|
const handleImportByLocal = async () => {
|
||||||
|
if (!isLocalMetadataValid.value) {
|
||||||
|
message.warning(t('comp.playlist.import.emptyLocalWarning'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
importing.value = true;
|
||||||
|
|
||||||
|
// 过滤掉空的行
|
||||||
|
const filteredData = localMetadata.value.filter(item => item.name.trim() !== '');
|
||||||
|
|
||||||
|
const encodedLocal = JSON.stringify(filteredData);
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
local: encodedLocal
|
||||||
|
};
|
||||||
|
|
||||||
|
if (importToStarPlaylist.value) {
|
||||||
|
params.importStarPlaylist = true;
|
||||||
|
} else if (playlistName.value) {
|
||||||
|
params.playlistName = playlistName.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await importPlaylist(params);
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
message.success(t('comp.playlist.import.importSuccess'));
|
||||||
|
taskId.value = res.data.data.taskId;
|
||||||
|
startStatusCheck();
|
||||||
|
} else {
|
||||||
|
message.error(res.data.message || t('comp.playlist.import.importFailed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入歌单失败:', error);
|
||||||
|
message.error(t('comp.playlist.import.importFailed'));
|
||||||
|
} finally {
|
||||||
|
importing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始检查任务状态
|
||||||
|
const startStatusCheck = () => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (statusCheckInterval.value) {
|
||||||
|
clearInterval(statusCheckInterval.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即检查一次
|
||||||
|
checkTaskStatus();
|
||||||
|
|
||||||
|
// 设置定时检查
|
||||||
|
statusCheckInterval.value = window.setInterval(() => {
|
||||||
|
checkTaskStatus();
|
||||||
|
}, 3000); // 每3秒检查一次
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查任务状态
|
||||||
|
const checkTaskStatus = async () => {
|
||||||
|
if (!taskId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkingStatus.value = true;
|
||||||
|
const res = await getImportTaskStatus(taskId.value);
|
||||||
|
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
// 新的API返回格式处理
|
||||||
|
if (res.data.data.tasks && res.data.data.tasks.length > 0) {
|
||||||
|
const taskData = res.data.data.tasks[0];
|
||||||
|
// 将API返回的status映射到组件内部使用的taskStatus
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
'PENDING': 'pending',
|
||||||
|
'PROCESSING': 'processing',
|
||||||
|
'COMPLETE': 'success',
|
||||||
|
'FAILED': 'failed'
|
||||||
|
};
|
||||||
|
|
||||||
|
taskStatus.value = statusMap[taskData.status] || 'pending';
|
||||||
|
|
||||||
|
if (taskStatus.value === 'success') {
|
||||||
|
successCount.value = taskData.succCount || 0;
|
||||||
|
// 成功后停止检查
|
||||||
|
if (statusCheckInterval.value) {
|
||||||
|
clearInterval(statusCheckInterval.value);
|
||||||
|
statusCheckInterval.value = null;
|
||||||
|
}
|
||||||
|
} else if (taskStatus.value === 'failed') {
|
||||||
|
failReason.value = taskData.msg || t('comp.playlist.import.unknownError');
|
||||||
|
// 失败后停止检查
|
||||||
|
if (statusCheckInterval.value) {
|
||||||
|
clearInterval(statusCheckInterval.value);
|
||||||
|
statusCheckInterval.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查任务状态失败:', error);
|
||||||
|
} finally {
|
||||||
|
checkingStatus.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动刷新状态
|
||||||
|
const refreshStatus = () => {
|
||||||
|
checkTaskStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return t('comp.playlist.import.statusPending');
|
||||||
|
case 'processing':
|
||||||
|
return t('comp.playlist.import.statusProcessing');
|
||||||
|
case 'success':
|
||||||
|
return t('comp.playlist.import.statusSuccess');
|
||||||
|
case 'failed':
|
||||||
|
return t('comp.playlist.import.statusFailed');
|
||||||
|
default:
|
||||||
|
return t('comp.playlist.import.statusUnknown');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果有任务ID,开始检查状态
|
||||||
|
if (taskId.value) {
|
||||||
|
startStatusCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清除定时器
|
||||||
|
if (statusCheckInterval.value) {
|
||||||
|
clearInterval(statusCheckInterval.value);
|
||||||
|
statusCheckInterval.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.import-playlist-page {
|
||||||
|
@apply h-full overflow-auto pr-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-header {
|
||||||
|
@apply flex justify-between items-center mb-6;
|
||||||
|
|
||||||
|
.import-header-left {
|
||||||
|
h2 {
|
||||||
|
@apply text-2xl font-bold text-gray-900 dark:text-white mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-desc {
|
||||||
|
@apply text-sm text-gray-500 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-content {
|
||||||
|
@apply space-y-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-card {
|
||||||
|
@apply rounded-lg;
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
@apply mt-4 space-y-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-tips, .text-tips, .local-tips {
|
||||||
|
@apply text-sm text-gray-500 dark:text-gray-400;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-disc pl-5 mt-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-format, .local-format {
|
||||||
|
@apply mt-2 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example {
|
||||||
|
@apply mt-2 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm overflow-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-inputs {
|
||||||
|
@apply space-y-3;
|
||||||
|
|
||||||
|
.link-row {
|
||||||
|
@apply flex items-center space-x-2;
|
||||||
|
|
||||||
|
.link-input {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-actions {
|
||||||
|
@apply mt-3 flex justify-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-inputs {
|
||||||
|
@apply space-y-3;
|
||||||
|
|
||||||
|
.metadata-row {
|
||||||
|
@apply flex items-center space-x-2;
|
||||||
|
|
||||||
|
.metadata-input {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-actions {
|
||||||
|
@apply mt-3 flex justify-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
@apply flex items-center space-x-4 mt-6;
|
||||||
|
|
||||||
|
.playlist-name-input {
|
||||||
|
@apply max-w-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-status-card {
|
||||||
|
@apply rounded-lg;
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
@apply flex justify-between items-center mb-4;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-lg font-medium text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
@apply space-y-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
@apply flex items-center;
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
@apply text-gray-500 dark:text-gray-400 w-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending, .status-processing {
|
||||||
|
@apply text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
@apply text-green-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-failed {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-count {
|
||||||
|
@apply text-green-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -28,7 +28,10 @@
|
|||||||
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
|
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
|
||||||
|
|
||||||
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
||||||
<div class="title">{{ t('user.playlist.created') }}</div>
|
<div class="title">
|
||||||
|
<div>{{ t('user.playlist.created') }}</div>
|
||||||
|
<div class="import-btn" @click="goToImportPlaylist" v-if="isElectron">{{ t('comp.playlist.import.button') }}</div>
|
||||||
|
</div>
|
||||||
<n-scrollbar>
|
<n-scrollbar>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in playList"
|
v-for="(item, index) in playList"
|
||||||
@@ -105,7 +108,7 @@ import { usePlayerStore } from '@/store/modules/player';
|
|||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import type { Playlist } from '@/type/listDetail';
|
import type { Playlist } from '@/type/listDetail';
|
||||||
import type { IUserDetail } from '@/type/user';
|
import type { IUserDetail } from '@/type/user';
|
||||||
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
import { getImgUrl, isElectron, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'User'
|
name: 'User'
|
||||||
@@ -126,6 +129,10 @@ const message = useMessage();
|
|||||||
|
|
||||||
const user = computed(() => userStore.user);
|
const user = computed(() => userStore.user);
|
||||||
|
|
||||||
|
const goToImportPlaylist = () => {
|
||||||
|
router.push('/playlist/import');
|
||||||
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
mounted.value = false;
|
mounted.value = false;
|
||||||
});
|
});
|
||||||
@@ -278,8 +285,13 @@ const showFollowList = () => {
|
|||||||
@apply bg-black bg-opacity-40;
|
@apply bg-black bg-opacity-40;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
@apply text-lg font-bold;
|
@apply text-lg font-bold flex items-center justify-between;
|
||||||
@apply text-gray-900 dark:text-white;
|
@apply text-gray-900 dark:text-white;
|
||||||
|
.import-btn {
|
||||||
|
@apply bg-light-100 font-normal rounded-lg px-2 py-1 text-opacity-70 text-sm hover:bg-light-200 hover:text-green-500 dark:bg-dark-200 dark:hover:bg-dark-300 dark:hover:text-green-400;
|
||||||
|
@apply cursor-pointer;
|
||||||
|
@apply transition-all duration-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
|
|||||||
Reference in New Issue
Block a user