mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
Merge branch 'main' into feat/dislike-improvement
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div
|
||||
ref="coverContainer"
|
||||
class="cover-3d-container relative cursor-pointer"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mouseenter="handleMouseEnter"
|
||||
>
|
||||
<div ref="coverImage" class="cover-wrapper" :style="coverTransformStyle">
|
||||
<n-image :src="src" class="cover-image" lazy preview-disabled :object-fit="objectFit" />
|
||||
<div class="cover-shine" :style="shineStyle"></div>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<i class="ri-loader-4-line loading-icon"></i>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
loading?: boolean;
|
||||
maxTilt?: number;
|
||||
scale?: number;
|
||||
shineIntensity?: number;
|
||||
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
maxTilt: 12,
|
||||
scale: 1.03,
|
||||
shineIntensity: 0.25,
|
||||
objectFit: 'cover',
|
||||
disabled: false
|
||||
});
|
||||
|
||||
// 3D视差效果相关
|
||||
const coverContainer = ref<HTMLElement | null>(null);
|
||||
const coverImage = ref<HTMLElement | null>(null);
|
||||
const mouseX = ref(0.5);
|
||||
const mouseY = ref(0.5);
|
||||
const isHovering = ref(false);
|
||||
const rafId = ref<number | null>(null);
|
||||
|
||||
// 3D视差效果计算
|
||||
const coverTransformStyle = computed(() => {
|
||||
if (!isHovering.value || props.disabled) {
|
||||
return {
|
||||
transform: 'perspective(1000px) rotateX(0deg) rotateY(0deg) scale(1)',
|
||||
transition: 'transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
};
|
||||
}
|
||||
|
||||
const tiltX = Math.round((mouseY.value - 0.5) * props.maxTilt * 100) / 100;
|
||||
const tiltY = Math.round((mouseX.value - 0.5) * -props.maxTilt * 100) / 100;
|
||||
|
||||
return {
|
||||
transform: `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale(${props.scale})`,
|
||||
transition: 'none'
|
||||
};
|
||||
});
|
||||
|
||||
// 光泽效果计算
|
||||
const shineStyle = computed(() => {
|
||||
if (!isHovering.value || props.disabled) {
|
||||
return {
|
||||
opacity: 0,
|
||||
background: 'transparent',
|
||||
transition: 'opacity 0.3s ease-out'
|
||||
};
|
||||
}
|
||||
|
||||
const shineX = Math.round(mouseX.value * 100);
|
||||
const shineY = Math.round(mouseY.value * 100);
|
||||
|
||||
return {
|
||||
opacity: props.shineIntensity,
|
||||
background: `radial-gradient(200px circle at ${shineX}% ${shineY}%, rgba(255,255,255,0.3), transparent 50%)`,
|
||||
transition: 'none'
|
||||
};
|
||||
});
|
||||
|
||||
// 使用 requestAnimationFrame 优化鼠标事件
|
||||
const updateMousePosition = (x: number, y: number) => {
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value);
|
||||
}
|
||||
|
||||
rafId.value = requestAnimationFrame(() => {
|
||||
// 只在位置有显著变化时更新,减少不必要的重绘
|
||||
const deltaX = Math.abs(mouseX.value - x);
|
||||
const deltaY = Math.abs(mouseY.value - y);
|
||||
|
||||
if (deltaX > 0.01 || deltaY > 0.01) {
|
||||
mouseX.value = x;
|
||||
mouseY.value = y;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 3D视差效果的鼠标事件处理
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!coverContainer.value || !isHovering.value || props.disabled) return;
|
||||
|
||||
const rect = coverContainer.value.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
|
||||
const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height));
|
||||
|
||||
updateMousePosition(x, y);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!props.disabled) {
|
||||
isHovering.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value);
|
||||
rafId.value = null;
|
||||
}
|
||||
// 平滑回到中心位置
|
||||
updateMousePosition(0.5, 0.5);
|
||||
};
|
||||
|
||||
// 清理资源
|
||||
onBeforeUnmount(() => {
|
||||
if (rafId.value) {
|
||||
cancelAnimationFrame(rafId.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cover-3d-container {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
|
||||
/* 3D视差效果样式 */
|
||||
.cover-wrapper {
|
||||
@apply relative w-full h-full rounded-xl overflow-hidden;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0); /* 强制硬件加速 */
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
@apply w-full h-full;
|
||||
border-radius: inherit;
|
||||
transform: translateZ(0); /* 强制硬件加速 */
|
||||
}
|
||||
|
||||
.cover-shine {
|
||||
@apply absolute inset-0 pointer-events-none rounded-xl;
|
||||
mix-blend-mode: overlay;
|
||||
z-index: 1;
|
||||
will-change: background, opacity;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* 为封面容器添加阴影效果 */
|
||||
.cover-3d-container:hover .cover-wrapper {
|
||||
filter: drop-shadow(0 15px 30px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
@apply absolute inset-0 flex items-center justify-center rounded-xl;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 48px;
|
||||
color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 移动端禁用3D效果 */
|
||||
@media (max-width: 768px) {
|
||||
.cover-wrapper {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.cover-shine {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,24 +9,22 @@
|
||||
>
|
||||
<div id="drawer-target" :class="[config.theme]">
|
||||
<div
|
||||
class="control-btn absolute top-8 left-8"
|
||||
class="control-buttons-container absolute top-8 left-8 right-8"
|
||||
:class="{ 'pure-mode': config.pureModeEnabled }"
|
||||
@click="closeMusicFull"
|
||||
>
|
||||
<i class="ri-arrow-down-s-line"></i>
|
||||
</div>
|
||||
<div class="control-btn" @click="closeMusicFull">
|
||||
<i class="ri-arrow-down-s-line"></i>
|
||||
</div>
|
||||
|
||||
<n-popover trigger="click" placement="bottom">
|
||||
<template #trigger>
|
||||
<div
|
||||
class="control-btn absolute top-8 right-8"
|
||||
:class="{ 'pure-mode': config.pureModeEnabled }"
|
||||
>
|
||||
<i class="ri-settings-3-line"></i>
|
||||
</div>
|
||||
</template>
|
||||
<lyric-settings ref="lyricSettingsRef" />
|
||||
</n-popover>
|
||||
<n-popover trigger="click" placement="bottom">
|
||||
<template #trigger>
|
||||
<div class="control-btn">
|
||||
<i class="ri-settings-3-line"></i>
|
||||
</div>
|
||||
</template>
|
||||
<lyric-settings ref="lyricSettingsRef" />
|
||||
</n-popover>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!config.hideCover"
|
||||
@@ -34,17 +32,15 @@
|
||||
:class="{ 'only-cover': config.hideLyrics }"
|
||||
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
|
||||
>
|
||||
<div class="img-container relative">
|
||||
<n-image
|
||||
<div class="img-container">
|
||||
<cover3-d
|
||||
ref="PicImgRef"
|
||||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
||||
class="img"
|
||||
lazy
|
||||
preview-disabled
|
||||
:loading="playMusic?.playLoading"
|
||||
:max-tilt="12"
|
||||
:scale="1.03"
|
||||
:shine-intensity="0.25"
|
||||
/>
|
||||
<div v-if="playMusic?.playLoading" class="loading-overlay">
|
||||
<i class="ri-loader-4-line loading-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-info">
|
||||
<div class="music-content-name">{{ playMusic.name }}</div>
|
||||
@@ -151,6 +147,7 @@ import { useDebounceFn } from '@vueuse/core';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Cover3D from '@/components/cover/Cover3D.vue';
|
||||
import LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue';
|
||||
import LyricSettings from '@/components/lyric/LyricSettings.vue';
|
||||
import SimplePlayBar from '@/components/player/SimplePlayBar.vue';
|
||||
@@ -183,10 +180,8 @@ const isDark = ref(false);
|
||||
const showStickyHeader = ref(false);
|
||||
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
|
||||
|
||||
// 移除 computed 配置
|
||||
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
|
||||
|
||||
// 监听设置组件的配置变化
|
||||
watch(
|
||||
() => lyricSettingsRef.value?.config,
|
||||
(newConfig) => {
|
||||
@@ -525,10 +520,12 @@ defineExpose({
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-back {
|
||||
@apply absolute bg-cover bg-center;
|
||||
z-index: -1;
|
||||
@@ -561,10 +558,6 @@ defineExpose({
|
||||
@apply w-[50vh] h-[50vh] mb-8;
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
|
||||
.music-info {
|
||||
@apply text-center w-[600px];
|
||||
|
||||
@@ -584,10 +577,6 @@ defineExpose({
|
||||
@apply relative w-full h-full;
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply rounded-xl w-full h-full shadow-2xl transition-all duration-300;
|
||||
}
|
||||
|
||||
.music-info {
|
||||
@apply w-full mt-4;
|
||||
|
||||
@@ -610,9 +599,11 @@ defineExpose({
|
||||
|
||||
&.center {
|
||||
@apply w-auto;
|
||||
|
||||
.music-lrc {
|
||||
@apply w-full max-w-3xl mx-auto;
|
||||
}
|
||||
|
||||
.music-lrc-text {
|
||||
@apply text-center;
|
||||
}
|
||||
@@ -697,24 +688,30 @@ defineExpose({
|
||||
.mobile {
|
||||
#drawer-target {
|
||||
@apply flex-col p-4 pt-8 justify-start;
|
||||
|
||||
.music-img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.music-lrc {
|
||||
height: calc(100vh - 260px) !important;
|
||||
width: 100vw;
|
||||
|
||||
span {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.hover-text {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.music-lrc-text {
|
||||
@apply text-xl text-center;
|
||||
}
|
||||
}
|
||||
|
||||
.music-content {
|
||||
@apply h-[calc(100vh-120px)];
|
||||
width: 100vw !important;
|
||||
@@ -751,8 +748,30 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons-container {
|
||||
@apply flex justify-between items-start z-[9999];
|
||||
|
||||
&.pure-mode {
|
||||
@apply pointer-events-auto; /* 容器需要能接收hover事件 */
|
||||
|
||||
.control-btn {
|
||||
@apply opacity-0 transition-all duration-300;
|
||||
pointer-events: none; /* 按钮隐藏时不接收事件 */
|
||||
}
|
||||
|
||||
&:hover .control-btn {
|
||||
@apply opacity-100;
|
||||
pointer-events: auto; /* hover时按钮可以点击 */
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.pure-mode) .control-btn {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999];
|
||||
@apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300;
|
||||
background: rgba(142, 142, 142, 0.192);
|
||||
backdrop-filter: blur(12px);
|
||||
|
||||
@@ -761,48 +780,16 @@ defineExpose({
|
||||
color: var(--text-color-active);
|
||||
}
|
||||
|
||||
&.pure-mode {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
|
||||
&:not(:hover) {
|
||||
i {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(126, 121, 121, 0.2);
|
||||
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
@apply absolute inset-0 flex items-center justify-center rounded-xl;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 48px;
|
||||
color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.lyric-correction {
|
||||
/* 仅在 hover 歌词区域时显示 */
|
||||
.music-lrc:hover & {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
|
||||
@@ -10,56 +10,84 @@
|
||||
>
|
||||
<n-space vertical>
|
||||
<p>{{ t('settings.playback.musicSourcesDesc') }}</p>
|
||||
|
||||
<n-checkbox-group v-model:value="selectedSources">
|
||||
<n-grid :cols="2" :x-gap="12" :y-gap="8">
|
||||
<n-grid-item v-for="source in musicSourceOptions" :key="source.value">
|
||||
<!-- 遍历常规音源 -->
|
||||
<n-grid-item v-for="source in regularMusicSources" :key="source.value">
|
||||
<n-checkbox :value="source.value">
|
||||
{{ source.label }}
|
||||
<template v-if="source.value === 'gdmusic'">
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-icon size="16" class="ml-1 text-blue-500 cursor-help">
|
||||
<i class="ri-information-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('settings.playback.gdmusicInfo') }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
{{ t('settings.playback.sourceLabels.' + source.value) }}
|
||||
<n-tooltip v-if="source.value === 'gdmusic'">
|
||||
<template #trigger>
|
||||
<n-icon size="16" class="ml-1 text-blue-500 cursor-help">
|
||||
<i class="ri-information-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('settings.playback.gdmusicInfo') }}
|
||||
</n-tooltip>
|
||||
</n-checkbox>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 单独处理自定义API选项 -->
|
||||
<n-grid-item>
|
||||
<n-checkbox value="custom" :disabled="!settingsStore.setData.customApiPlugin">
|
||||
{{ t('settings.playback.sourceLabels.custom') }}
|
||||
<n-tooltip v-if="!settingsStore.setData.customApiPlugin">
|
||||
<template #trigger>
|
||||
<n-icon size="16" class="ml-1 text-gray-400 cursor-help">
|
||||
<i class="ri-question-line"></i>
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('settings.playback.customApi.enableHint') }}
|
||||
</n-tooltip>
|
||||
</n-checkbox>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-checkbox-group>
|
||||
<div v-if="selectedSources.length === 0" class="text-red-500 text-sm">
|
||||
{{ t('settings.playback.musicSourcesWarning') }}
|
||||
</div>
|
||||
|
||||
<!-- GD音乐台设置 -->
|
||||
<div
|
||||
v-if="selectedSources.includes('gdmusic')"
|
||||
class="mt-4 border-t pt-4 border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<h3 class="text-base font-medium mb-2">GD音乐台(music.gdstudio.xyz)设置</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz)
|
||||
</p>
|
||||
<!-- 分割线 -->
|
||||
<div class="mt-4 border-t pt-4 border-gray-200 dark:border-gray-700"></div>
|
||||
|
||||
<!-- 自定义API导入区域 -->
|
||||
<div>
|
||||
<h3 class="text-base font-medium mb-2">
|
||||
{{ t('settings.playback.customApi.sectionTitle') }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-4">
|
||||
<n-button @click="importPlugin" size="small">{{
|
||||
t('settings.playback.customApi.importConfig')
|
||||
}}</n-button>
|
||||
<p v-if="settingsStore.setData.customApiPluginName" class="text-sm">
|
||||
{{ t('settings.playback.customApi.currentSource') }}:
|
||||
<span class="font-semibold">{{ settingsStore.setData.customApiPluginName }}</span>
|
||||
</p>
|
||||
<p v-else class="text-sm text-gray-500">
|
||||
{{ t('settings.playback.customApi.notImported') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineProps, ref, watch } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useSettingsStore } from '@/store';
|
||||
import { type Platform } from '@/types/music';
|
||||
|
||||
// 扩展 Platform 类型以包含 'custom'
|
||||
type ExtendedPlatform = Platform | 'custom';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sources: {
|
||||
type: Array as () => Platform[],
|
||||
type: Array as () => ExtendedPlatform[],
|
||||
default: () => ['migu', 'kugou', 'pyncmd', 'bilibili']
|
||||
}
|
||||
});
|
||||
@@ -67,17 +95,49 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:show', 'update:sources']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const message = useMessage();
|
||||
const visible = ref(props.show);
|
||||
const selectedSources = ref<Platform[]>(props.sources);
|
||||
const selectedSources = ref<ExtendedPlatform[]>(props.sources);
|
||||
|
||||
const musicSourceOptions = ref([
|
||||
{ label: 'MG', value: 'migu' },
|
||||
{ label: 'KG', value: 'kugou' },
|
||||
{ label: 'pyncmd', value: 'pyncmd' },
|
||||
{ label: 'Bilibili', value: 'bilibili' },
|
||||
{ label: 'GD音乐台', value: 'gdmusic' }
|
||||
// 将常规音源和自定义音源分开定义
|
||||
const regularMusicSources = ref([
|
||||
{ value: 'migu' },
|
||||
{ value: 'kugou' },
|
||||
{ value: 'pyncmd' },
|
||||
{ value: 'bilibili' },
|
||||
{ value: 'gdmusic' }
|
||||
]);
|
||||
|
||||
const importPlugin = async () => {
|
||||
try {
|
||||
const result = await window.api.importCustomApiPlugin();
|
||||
if (result && result.name && result.content) {
|
||||
settingsStore.setCustomApiPlugin(result);
|
||||
message.success(t('settings.playback.customApi.importSuccess', { name: result.name }));
|
||||
// 导入成功后,如果用户还没勾选,则自动勾选上
|
||||
if (!selectedSources.value.includes('custom')) {
|
||||
selectedSources.value.push('custom');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(t('settings.playback.customApi.importFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
|
||||
// 监听自定义插件内容的变化。如果用户清除了插件,要确保 'custom' 选项被取消勾选
|
||||
watch(
|
||||
() => settingsStore.setData.customApiPlugin,
|
||||
(newPluginContent) => {
|
||||
if (!newPluginContent) {
|
||||
const index = selectedSources.value.indexOf('custom');
|
||||
if (index > -1) {
|
||||
selectedSources.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 同步外部show属性变化
|
||||
watch(
|
||||
() => props.show,
|
||||
@@ -108,11 +168,9 @@ const handleConfirm = () => {
|
||||
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||
const valuesToEmit =
|
||||
selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;
|
||||
|
||||
emit('update:sources', valuesToEmit);
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 取消时还原为props传入的初始值
|
||||
selectedSources.value = [...props.sources];
|
||||
|
||||
Reference in New Issue
Block a user