mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
feat: 新增歌单导入功能
添加歌单导入功能,支持通过链接、文本和元数据三种方式导入歌单 - 实现链接导入、文本导入和元数据导入三种方式 - 添加导入状态检查和显示功能
This commit is contained in:
@@ -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']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCarousel: typeof import('naive-ui')['NCarousel']
|
||||
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
|
||||
@@ -76,6 +76,16 @@ const otherRouter = [
|
||||
back: true
|
||||
},
|
||||
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;
|
||||
|
||||
@@ -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="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>
|
||||
<div
|
||||
v-for="(item, index) in playList"
|
||||
@@ -105,7 +108,7 @@ import { usePlayerStore } from '@/store/modules/player';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import type { Playlist } from '@/type/listDetail';
|
||||
import type { IUserDetail } from '@/type/user';
|
||||
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
import { getImgUrl, isElectron, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'User'
|
||||
@@ -126,6 +129,10 @@ const message = useMessage();
|
||||
|
||||
const user = computed(() => userStore.user);
|
||||
|
||||
const goToImportPlaylist = () => {
|
||||
router.push('/playlist/import');
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mounted.value = false;
|
||||
});
|
||||
@@ -278,8 +285,13 @@ const showFollowList = () => {
|
||||
@apply bg-black bg-opacity-40;
|
||||
}
|
||||
.title {
|
||||
@apply text-lg font-bold;
|
||||
@apply text-lg font-bold flex items-center justify-between;
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user