mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-29 11:27:23 +08:00
✨ feat: 添加歌词界面样式配置功能
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
"@tailwindcss/postcss7-compat": "^2.2.4",
|
"@tailwindcss/postcss7-compat": "^2.2.4",
|
||||||
"@types/howler": "^2.2.12",
|
"@types/howler": "^2.2.12",
|
||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
|
"@types/tinycolor2": "^1.4.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"remixicon": "^4.2.0",
|
"remixicon": "^4.2.0",
|
||||||
"sass": "^1.82.0",
|
"sass": "^1.82.0",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
|
"tinycolor2": "^1.6.0",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2",
|
||||||
"unplugin-auto-import": "^0.18.2",
|
"unplugin-auto-import": "^0.18.2",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
|
|||||||
+22
-20
@@ -4,26 +4,28 @@ import Store from 'electron-store';
|
|||||||
import set from '../set.json';
|
import set from '../set.json';
|
||||||
import { defaultShortcuts } from './shortcuts';
|
import { defaultShortcuts } from './shortcuts';
|
||||||
|
|
||||||
interface StoreType {
|
type SetConfig = {
|
||||||
set: {
|
isProxy: boolean;
|
||||||
isProxy: boolean;
|
proxyConfig: {
|
||||||
noAnimate: boolean;
|
enable: boolean;
|
||||||
animationSpeed: number;
|
protocol: string;
|
||||||
author: string;
|
host: string;
|
||||||
authorUrl: string;
|
port: number;
|
||||||
musicApiPort: number;
|
|
||||||
closeAction: 'ask' | 'minimize' | 'close';
|
|
||||||
musicQuality: string;
|
|
||||||
fontFamily: string;
|
|
||||||
proxyConfig: {
|
|
||||||
enable: boolean;
|
|
||||||
protocol: string;
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
enableRealIP: boolean;
|
|
||||||
realIP: string;
|
|
||||||
};
|
};
|
||||||
|
enableRealIP: boolean;
|
||||||
|
realIP: string;
|
||||||
|
noAnimate: boolean;
|
||||||
|
animationSpeed: number;
|
||||||
|
author: string;
|
||||||
|
authorUrl: string;
|
||||||
|
musicApiPort: number;
|
||||||
|
closeAction: 'ask' | 'minimize' | 'close';
|
||||||
|
musicQuality: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontScope: 'global' | 'lyric';
|
||||||
|
};
|
||||||
|
interface StoreType {
|
||||||
|
set: SetConfig;
|
||||||
shortcuts: typeof defaultShortcuts;
|
shortcuts: typeof defaultShortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ export function initializeConfig() {
|
|||||||
store = new Store<StoreType>({
|
store = new Store<StoreType>({
|
||||||
name: 'config',
|
name: 'config',
|
||||||
defaults: {
|
defaults: {
|
||||||
set,
|
set: set as SetConfig,
|
||||||
shortcuts: defaultShortcuts
|
shortcuts: defaultShortcuts
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ body {
|
|||||||
border-radius: 0.5rem !important;
|
border-radius: 0.5rem !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
.n-popover:has(.transparent-popover ) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-panel transparent-popover">
|
||||||
|
<div class="settings-title">页面设置</div>
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>纯净模式</span>
|
||||||
|
<n-switch v-model:value="config.pureModeEnabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>隐藏封面</span>
|
||||||
|
<n-switch v-model:value="config.hideCover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>居中显示</span>
|
||||||
|
<n-switch v-model:value="config.centerLyrics" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>显示翻译</span>
|
||||||
|
<n-switch v-model:value="config.showTranslation" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>隐藏播放栏</span>
|
||||||
|
<n-switch v-model:value="config.hidePlayBar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-slider">
|
||||||
|
<span>字体大小</span>
|
||||||
|
<n-slider
|
||||||
|
v-model:value="config.fontSize"
|
||||||
|
:step="1"
|
||||||
|
:min="12"
|
||||||
|
:max="32"
|
||||||
|
:marks="{
|
||||||
|
12: '小',
|
||||||
|
22: '中',
|
||||||
|
32: '大'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-slider">
|
||||||
|
<span>文字间距</span>
|
||||||
|
<n-slider
|
||||||
|
v-model:value="config.letterSpacing"
|
||||||
|
:step="0.2"
|
||||||
|
:min="-2"
|
||||||
|
:max="10"
|
||||||
|
:marks="{
|
||||||
|
'-2': '紧凑',
|
||||||
|
0: '默认',
|
||||||
|
10: '宽松'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-slider">
|
||||||
|
<span>行高</span>
|
||||||
|
<n-slider
|
||||||
|
v-model:value="config.lineHeight"
|
||||||
|
:step="0.1"
|
||||||
|
:min="1"
|
||||||
|
:max="3"
|
||||||
|
:marks="{
|
||||||
|
1: '紧凑',
|
||||||
|
1.5: '默认',
|
||||||
|
3: '宽松'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-item">
|
||||||
|
<span>背景主题</span>
|
||||||
|
<n-radio-group v-model:value="config.theme" name="theme">
|
||||||
|
<n-radio value="default">默认</n-radio>
|
||||||
|
<n-radio value="light">亮色</n-radio>
|
||||||
|
<n-radio value="dark">暗色</n-radio>
|
||||||
|
</n-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
interface LyricConfig {
|
||||||
|
hideCover: boolean;
|
||||||
|
centerLyrics: boolean;
|
||||||
|
fontSize: number;
|
||||||
|
letterSpacing: number;
|
||||||
|
lineHeight: number;
|
||||||
|
showTranslation: boolean;
|
||||||
|
theme: 'default' | 'light' | 'dark';
|
||||||
|
hidePlayBar: boolean;
|
||||||
|
pureModeEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = ref<LyricConfig>({
|
||||||
|
hideCover: false,
|
||||||
|
centerLyrics: false,
|
||||||
|
fontSize: 22,
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 2,
|
||||||
|
showTranslation: true,
|
||||||
|
theme: 'default',
|
||||||
|
hidePlayBar: false,
|
||||||
|
pureModeEnabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['themeChange']);
|
||||||
|
|
||||||
|
// 监听配置变化并保存到本地存储
|
||||||
|
watch(
|
||||||
|
() => config.value,
|
||||||
|
(newConfig) => {
|
||||||
|
updateCSSVariables(newConfig);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
watch(
|
||||||
|
() => config.value.theme,
|
||||||
|
(newTheme) => {
|
||||||
|
emit('themeChange', newTheme);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新 CSS 变量
|
||||||
|
const updateCSSVariables = (config: LyricConfig) => {
|
||||||
|
document.documentElement.style.setProperty('--lyric-font-size', `${config.fontSize}px`);
|
||||||
|
document.documentElement.style.setProperty('--lyric-letter-spacing', `${config.letterSpacing}px`);
|
||||||
|
document.documentElement.style.setProperty('--lyric-line-height', config.lineHeight.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载保存的配置
|
||||||
|
onMounted(() => {
|
||||||
|
const savedConfig = localStorage.getItem('music-full-config');
|
||||||
|
if (savedConfig) {
|
||||||
|
config.value = { ...config.value, ...JSON.parse(savedConfig) };
|
||||||
|
updateCSSVariables(config.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
config
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settings-panel {
|
||||||
|
@apply p-4 w-72 rounded-lg relative overflow-hidden backdrop-blur-lg bg-black/10;
|
||||||
|
.settings-title {
|
||||||
|
@apply text-base font-bold mb-4;
|
||||||
|
color: var(--text-color-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
@apply space-y-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
@apply flex items-center justify-between;
|
||||||
|
span {
|
||||||
|
@apply text-sm;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slider {
|
||||||
|
@apply space-y-2;
|
||||||
|
@apply mb-10 !important;
|
||||||
|
span {
|
||||||
|
@apply text-sm;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改 slider 字体颜色
|
||||||
|
:deep(.n-slider-mark) {
|
||||||
|
color: var(--text-color-primary) !important;
|
||||||
|
}
|
||||||
|
// 修改 radio 字体颜色
|
||||||
|
:deep(.n-radio__label) {
|
||||||
|
color: var(--text-color-active) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 底部音乐播放 -->
|
<!-- 底部音乐播放 -->
|
||||||
<play-bar v-if="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
|
<play-bar v-show="isPlay" :style="isMobile && store.state.musicFull ? 'bottom: 0;' : ''" />
|
||||||
<!-- 下载管理抽屉 -->
|
<!-- 下载管理抽屉 -->
|
||||||
<download-drawer v-if="isElectron" />
|
<download-drawer v-if="isElectron" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,9 +7,29 @@
|
|||||||
:to="`#layout-main`"
|
:to="`#layout-main`"
|
||||||
:z-index="9998"
|
:z-index="9998"
|
||||||
>
|
>
|
||||||
<div id="drawer-target">
|
<div id="drawer-target" :class="[config.theme]">
|
||||||
<div class="drawer-back"></div>
|
|
||||||
<div
|
<div
|
||||||
|
class="control-btn absolute top-8 left-8"
|
||||||
|
:class="{ 'pure-mode': config.pureModeEnabled }"
|
||||||
|
@click="isVisible = false"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="!config.hideCover"
|
||||||
class="music-img"
|
class="music-img"
|
||||||
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
|
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
|
||||||
>
|
>
|
||||||
@@ -44,16 +64,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-content">
|
<div class="music-content" :class="{ center: config.centerLyrics && config.hideCover }">
|
||||||
<n-layout
|
<n-layout
|
||||||
ref="lrcSider"
|
ref="lrcSider"
|
||||||
class="music-lrc"
|
class="music-lrc"
|
||||||
style="height: 60vh"
|
:style="{
|
||||||
|
height: config.hidePlayBar ? '85vh' : '65vh',
|
||||||
|
width: config.hideCover ? '50vw' : '500px'
|
||||||
|
}"
|
||||||
:native-scrollbar="false"
|
:native-scrollbar="false"
|
||||||
@mouseover="mouseOverLayout"
|
@mouseover="mouseOverLayout"
|
||||||
@mouseleave="mouseLeaveLayout"
|
@mouseleave="mouseLeaveLayout"
|
||||||
>
|
>
|
||||||
<div ref="lrcContainer">
|
<!-- 歌曲信息 -->
|
||||||
|
|
||||||
|
<div ref="lrcContainer" class="music-lrc-container">
|
||||||
|
<div
|
||||||
|
v-if="config.hideCover"
|
||||||
|
class="music-info-header"
|
||||||
|
:style="{ textAlign: config.centerLyrics ? 'center' : 'left' }"
|
||||||
|
>
|
||||||
|
<div class="music-info-name">{{ playMusic.name }}</div>
|
||||||
|
<div class="music-info-singer">
|
||||||
|
<span
|
||||||
|
v-for="(item, index) in artistList"
|
||||||
|
:key="index"
|
||||||
|
class="cursor-pointer hover:text-green-500"
|
||||||
|
@click="handleArtistClick(item.id)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
{{ index < artistList.length - 1 ? ' / ' : '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in lrcArray"
|
v-for="(item, index) in lrcArray"
|
||||||
:id="`music-lrc-text-${index}`"
|
:id="`music-lrc-text-${index}`"
|
||||||
@@ -63,7 +106,9 @@
|
|||||||
@click="setAudioTime(index)"
|
@click="setAudioTime(index)"
|
||||||
>
|
>
|
||||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||||
<div class="music-lrc-text-tr">{{ item.trText }}</div>
|
<div v-show="config.showTranslation" class="music-lrc-text-tr">
|
||||||
|
{{ item.trText }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无歌词 -->
|
<!-- 无歌词 -->
|
||||||
@@ -84,9 +129,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import LyricSettings from '@/components/lyric/LyricSettings.vue';
|
||||||
import {
|
import {
|
||||||
artistList,
|
artistList,
|
||||||
lrcArray,
|
lrcArray,
|
||||||
@@ -106,6 +152,56 @@ const lrcContainer = ref<HTMLElement | null>(null);
|
|||||||
const currentBackground = ref('');
|
const currentBackground = ref('');
|
||||||
const animationFrame = ref<number | null>(null);
|
const animationFrame = ref<number | null>(null);
|
||||||
const isDark = ref(false);
|
const isDark = ref(false);
|
||||||
|
const showStickyHeader = ref(false);
|
||||||
|
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
|
||||||
|
|
||||||
|
interface LyricConfig {
|
||||||
|
hideCover: boolean;
|
||||||
|
centerLyrics: boolean;
|
||||||
|
fontSize: number;
|
||||||
|
letterSpacing: number;
|
||||||
|
lineHeight: number;
|
||||||
|
showTranslation: boolean;
|
||||||
|
theme: 'default' | 'light' | 'dark';
|
||||||
|
hidePlayBar: boolean;
|
||||||
|
pureModeEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 computed 配置
|
||||||
|
const config = ref<LyricConfig>({
|
||||||
|
hideCover: false,
|
||||||
|
centerLyrics: false,
|
||||||
|
fontSize: 22,
|
||||||
|
letterSpacing: 0,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
showTranslation: true,
|
||||||
|
theme: 'default',
|
||||||
|
hidePlayBar: false,
|
||||||
|
pureModeEnabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听设置组件的配置变化
|
||||||
|
watch(
|
||||||
|
() => lyricSettingsRef.value?.config,
|
||||||
|
(newConfig) => {
|
||||||
|
if (newConfig) {
|
||||||
|
config.value = newConfig;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听本地配置变化,保存到 localStorage
|
||||||
|
watch(
|
||||||
|
() => config.value,
|
||||||
|
(newConfig) => {
|
||||||
|
localStorage.setItem('music-full-config', JSON.stringify(newConfig));
|
||||||
|
if (lyricSettingsRef.value) {
|
||||||
|
lyricSettingsRef.value.config = newConfig;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -118,6 +214,11 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const themeMusic = {
|
||||||
|
light: 'linear-gradient(to bottom, #ffffff, #f5f5f5)',
|
||||||
|
dark: 'linear-gradient(to bottom, #1a1a1a, #000000)'
|
||||||
|
};
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
const isVisible = computed({
|
const isVisible = computed({
|
||||||
@@ -126,18 +227,17 @@ const isVisible = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 歌词滚动方法
|
// 歌词滚动方法
|
||||||
const lrcScroll = (behavior = 'smooth', top: null | number = null) => {
|
const lrcScroll = (behavior: ScrollBehavior = 'smooth') => {
|
||||||
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`) as HTMLElement;
|
||||||
if (isVisible.value && !isMouse.value && nowEl && lrcContainer.value) {
|
if (isVisible.value && !isMouse.value && nowEl && lrcSider.value) {
|
||||||
if (top !== null) {
|
const containerHeight = lrcSider.value.$el.clientHeight;
|
||||||
lrcSider.value.scrollTo({ top, behavior });
|
const elementTop = nowEl.offsetTop;
|
||||||
} else {
|
const scrollTop = elementTop - containerHeight / 2 + nowEl.clientHeight / 2;
|
||||||
const containerRect = lrcContainer.value.getBoundingClientRect();
|
|
||||||
const nowElRect = nowEl.getBoundingClientRect();
|
lrcSider.value.scrollTo({
|
||||||
const relativeTop = nowElRect.top - containerRect.top;
|
top: scrollTop,
|
||||||
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
|
behavior
|
||||||
lrcSider.value.scrollTo({ top: scrollTop, behavior });
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,6 +249,7 @@ const mouseOverLayout = () => {
|
|||||||
}
|
}
|
||||||
isMouse.value = true;
|
isMouse.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mouseLeaveLayout = () => {
|
const mouseLeaveLayout = () => {
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
return;
|
return;
|
||||||
@@ -174,41 +275,51 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setTextColors = (background: string) => {
|
||||||
|
if (!background) {
|
||||||
|
textColors.value = getTextColors();
|
||||||
|
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));
|
||||||
|
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||||
|
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新文字颜色
|
||||||
|
textColors.value = getTextColors(background);
|
||||||
|
isDark.value = textColors.value.active === '#000000';
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--hover-bg-color',
|
||||||
|
getHoverBackgroundColor(isDark.value)
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
||||||
|
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
||||||
|
|
||||||
|
// 处理背景颜色动画
|
||||||
|
if (currentBackground.value) {
|
||||||
|
if (animationFrame.value) {
|
||||||
|
cancelAnimationFrame(animationFrame.value);
|
||||||
|
}
|
||||||
|
const result = animateGradient(currentBackground.value, background, (gradient) => {
|
||||||
|
currentBackground.value = gradient;
|
||||||
|
});
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
animationFrame.value = result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentBackground.value = background;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 监听背景变化
|
// 监听背景变化
|
||||||
watch(
|
watch(
|
||||||
() => props.background,
|
() => props.background,
|
||||||
(newBg) => {
|
(newBg) => {
|
||||||
if (!newBg) {
|
if (config.value.theme === 'default') {
|
||||||
textColors.value = getTextColors();
|
setTextColors(newBg);
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--hover-bg-color',
|
|
||||||
getHoverBackgroundColor(false)
|
|
||||||
);
|
|
||||||
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
|
||||||
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBackground.value) {
|
|
||||||
if (animationFrame.value) {
|
|
||||||
cancelAnimationFrame(animationFrame.value);
|
|
||||||
}
|
|
||||||
animationFrame.value = animateGradient(currentBackground.value, newBg, (gradient) => {
|
|
||||||
currentBackground.value = gradient;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
currentBackground.value = newBg;
|
setTextColors(themeMusic[config.value.theme] || props.background);
|
||||||
}
|
}
|
||||||
|
|
||||||
textColors.value = getTextColors(newBg);
|
|
||||||
isDark.value = textColors.value.active === '#000000';
|
|
||||||
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--hover-bg-color',
|
|
||||||
getHoverBackgroundColor(isDark.value)
|
|
||||||
);
|
|
||||||
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
|
|
||||||
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
@@ -290,8 +401,87 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 监听配置变化并保存到本地存储
|
||||||
|
watch(
|
||||||
|
() => config.value,
|
||||||
|
(newConfig) => {
|
||||||
|
localStorage.setItem('music-full-config', JSON.stringify(newConfig));
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听滚动事件
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!lrcSider.value || !config.value.hideCover) return;
|
||||||
|
const { scrollTop } = lrcSider.value.$el;
|
||||||
|
showStickyHeader.value = scrollTop > 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加滚动监听
|
||||||
|
onMounted(() => {
|
||||||
|
if (lrcSider.value?.$el) {
|
||||||
|
lrcSider.value.$el.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除滚动监听
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (animationFrame.value) {
|
||||||
|
cancelAnimationFrame(animationFrame.value);
|
||||||
|
}
|
||||||
|
if (lrcSider.value?.$el) {
|
||||||
|
lrcSider.value.$el.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听字体大小变化
|
||||||
|
watch(
|
||||||
|
() => config.value.fontSize,
|
||||||
|
(newSize) => {
|
||||||
|
document.documentElement.style.setProperty('--lyric-font-size', `${newSize}px`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听主题变化
|
||||||
|
watch(
|
||||||
|
() => config.value.theme,
|
||||||
|
(newTheme) => {
|
||||||
|
const newBackground = themeMusic[newTheme] || props.background;
|
||||||
|
setTextColors(newBackground);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加文字间距监听
|
||||||
|
watch(
|
||||||
|
() => config.value.letterSpacing,
|
||||||
|
(newSpacing) => {
|
||||||
|
document.documentElement.style.setProperty('--lyric-letter-spacing', `${newSpacing}px`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加行高监听
|
||||||
|
watch(
|
||||||
|
() => config.value.lineHeight,
|
||||||
|
(newLineHeight) => {
|
||||||
|
document.documentElement.style.setProperty('--lyric-line-height', newLineHeight.toString());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载保存的配置
|
||||||
|
onMounted(() => {
|
||||||
|
const savedConfig = localStorage.getItem('music-full-config');
|
||||||
|
if (savedConfig) {
|
||||||
|
config.value = { ...config.value, ...JSON.parse(savedConfig) };
|
||||||
|
}
|
||||||
|
if (lrcSider.value?.$el) {
|
||||||
|
lrcSider.value.$el.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
lrcScroll
|
lrcScroll,
|
||||||
|
config
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -318,7 +508,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
#drawer-target {
|
#drawer-target {
|
||||||
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
|
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full py-8;
|
||||||
animation-duration: 300ms;
|
animation-duration: 300ms;
|
||||||
|
|
||||||
.music-img {
|
.music-img {
|
||||||
@@ -326,12 +516,23 @@ defineExpose({
|
|||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
.img {
|
.img {
|
||||||
@apply rounded-xl w-full h-full shadow-2xl;
|
@apply rounded-xl w-full h-full shadow-2xl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-content {
|
.music-content {
|
||||||
@apply flex flex-col justify-center items-center relative;
|
@apply flex flex-col justify-center items-center relative;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
@apply w-full;
|
||||||
|
.music-lrc {
|
||||||
|
@apply w-full max-w-3xl mx-auto;
|
||||||
|
}
|
||||||
|
.music-lrc-text {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@apply font-bold text-2xl pb-1 pt-4;
|
@apply font-bold text-2xl pb-1 pt-4;
|
||||||
@@ -346,15 +547,46 @@ defineExpose({
|
|||||||
display: none;
|
display: none;
|
||||||
@apply flex justify-center items-center;
|
@apply flex justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.music-lrc-container {
|
||||||
|
padding-top: 30vh;
|
||||||
|
}
|
||||||
|
|
||||||
.music-lrc {
|
.music-lrc {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 550px;
|
height: 550px;
|
||||||
|
position: relative;
|
||||||
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0%,
|
||||||
|
black 10%,
|
||||||
|
black 90%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
.music-info-header {
|
||||||
|
@apply mb-8;
|
||||||
|
|
||||||
|
.music-info-name {
|
||||||
|
@apply text-4xl font-bold mb-2;
|
||||||
|
color: var(--text-color-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-info-singer {
|
||||||
|
@apply text-base;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-text {
|
&-text {
|
||||||
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
@apply text-2xl cursor-pointer font-bold px-2 py-4;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
font-size: var(--lyric-font-size, 22px) !important;
|
||||||
|
letter-spacing: var(--lyric-letter-spacing, 0) !important;
|
||||||
|
line-height: var(--lyric-line-height, 2) !important;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
background-clip: text !important;
|
background-clip: text !important;
|
||||||
@@ -415,11 +647,49 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
#drawer-target {
|
#drawer-target {
|
||||||
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full pb-8;
|
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full py-8;
|
||||||
animation-duration: 300ms;
|
animation-duration: 300ms;
|
||||||
|
|
||||||
.music-lrc-text {
|
.music-lrc-text {
|
||||||
font-family: var(--current-font-family);
|
font-family: var(--current-font-family);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
@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);
|
||||||
|
|
||||||
|
i {
|
||||||
|
@apply text-xl;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="music-play-bar"
|
class="music-play-bar"
|
||||||
:class="
|
:class="[
|
||||||
setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')
|
setAnimationClass('animate__bounceInUp'),
|
||||||
"
|
musicFullVisible ? 'play-bar-opcity' : '',
|
||||||
|
musicFullVisible && MusicFullRef?.config?.hidePlayBar
|
||||||
|
? 'animate__animated animate__slideOutDown'
|
||||||
|
: ''
|
||||||
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
color: musicFullVisible
|
color: musicFullVisible
|
||||||
? textColors.theme === 'dark'
|
? textColors.theme === 'dark'
|
||||||
@@ -25,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
||||||
<n-image
|
<n-image
|
||||||
:src="getImgUrl(playMusic?.picUrl, '500y500')"
|
:src="getImgUrl(playMusic?.picUrl, '100y100')"
|
||||||
class="play-bar-img"
|
class="play-bar-img"
|
||||||
lazy
|
lazy
|
||||||
preview-disabled
|
preview-disabled
|
||||||
@@ -164,10 +168,10 @@ import {
|
|||||||
sound,
|
sound,
|
||||||
textColors
|
textColors
|
||||||
} from '@/hooks/MusicHook';
|
} from '@/hooks/MusicHook';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';
|
||||||
import { showShortcutToast } from '@/utils/shortcutToast';
|
import { showShortcutToast } from '@/utils/shortcutToast';
|
||||||
import { audioService } from '@/services/audioService';
|
|
||||||
|
|
||||||
import MusicFull from './MusicFull.vue';
|
import MusicFull from './MusicFull.vue';
|
||||||
|
|
||||||
@@ -292,7 +296,7 @@ const playMusicEvent = async () => {
|
|||||||
store.commit('setPlayMusic', true);
|
store.commit('setPlayMusic', true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error',error)
|
console.log('error', error);
|
||||||
if (play.value) {
|
if (play.value) {
|
||||||
store.commit('nextPlay');
|
store.commit('nextPlay');
|
||||||
}
|
}
|
||||||
@@ -386,6 +390,16 @@ if (isElectron) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听播放栏显示状态
|
||||||
|
watch(
|
||||||
|
() => MusicFullRef.value?.config?.hidePlayBar,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && musicFullVisible.value) {
|
||||||
|
// 使用 animate.css 动画,不需要手动设置样式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -399,6 +413,16 @@ if (isElectron) {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
animation-duration: 0.5s !important;
|
animation-duration: 0.5s !important;
|
||||||
|
|
||||||
|
&.play-bar-opcity {
|
||||||
|
@apply bg-transparent !important;
|
||||||
|
box-shadow: 0 0 20px 5px #0000001d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.animate__slideOutDown {
|
||||||
|
animation-duration: 0.3s !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.music-content {
|
.music-content {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
@apply ml-4;
|
@apply ml-4;
|
||||||
@@ -413,11 +437,6 @@ if (isElectron) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-bar-opcity {
|
|
||||||
@apply bg-transparent !important;
|
|
||||||
box-shadow: 0 0 20px 5px #0000001d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-bar-img {
|
.play-bar-img {
|
||||||
@apply w-14 h-14 rounded-2xl;
|
@apply w-14 h-14 rounded-2xl;
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-142
@@ -1,8 +1,17 @@
|
|||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
interface IColor {
|
interface IColor {
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ITextColors {
|
||||||
|
primary: string;
|
||||||
|
active: string;
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
|
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
|
||||||
try {
|
try {
|
||||||
const primaryColor = await getImagePrimaryColor(imageSrc);
|
const primaryColor = await getImagePrimaryColor(imageSrc);
|
||||||
@@ -96,126 +105,43 @@ const getAverageColor = (data: Uint8ClampedArray): number[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generateGradientBackground = (color: string): string => {
|
const generateGradientBackground = (color: string): string => {
|
||||||
const [r, g, b] = color.match(/\d+/g)?.map(Number) || [0, 0, 0];
|
const tc = tinycolor(color);
|
||||||
const [h, s, l] = rgbToHsl(r, g, b);
|
const hsl = tc.toHsl();
|
||||||
|
|
||||||
// 增加亮度和暗度的差异
|
// 增加亮度和暗度的差异
|
||||||
const lightL = Math.min(l + 0.2, 0.95);
|
const lightColor = tinycolor({ h: hsl.h, s: hsl.s * 0.8, l: Math.min(hsl.l + 0.2, 0.95) });
|
||||||
const darkL = Math.max(l - 0.3, 0.05);
|
const midColor = tinycolor({ h: hsl.h, s: hsl.s, l: hsl.l });
|
||||||
const midL = (lightL + darkL) / 2;
|
const darkColor = tinycolor({
|
||||||
|
h: hsl.h,
|
||||||
|
s: Math.min(hsl.s * 1.2, 1),
|
||||||
|
l: Math.max(hsl.l - 0.3, 0.05)
|
||||||
|
});
|
||||||
|
|
||||||
// 调整饱和度以增强效果
|
return `linear-gradient(to bottom, ${lightColor.toRgbString()} 0%, ${midColor.toRgbString()} 50%, ${darkColor.toRgbString()} 100%)`;
|
||||||
const lightS = Math.min(s * 0.8, 1);
|
|
||||||
const darkS = Math.min(s * 1.2, 1);
|
|
||||||
|
|
||||||
const [lightR, lightG, lightB] = hslToRgb(h, lightS, lightL);
|
|
||||||
const [midR, midG, midB] = hslToRgb(h, s, midL);
|
|
||||||
const [darkR, darkG, darkB] = hslToRgb(h, darkS, darkL);
|
|
||||||
|
|
||||||
const lightColor = `rgb(${lightR}, ${lightG}, ${lightB})`;
|
|
||||||
const midColor = `rgb(${midR}, ${midG}, ${midB})`;
|
|
||||||
const darkColor = `rgb(${darkR}, ${darkG}, ${darkB})`;
|
|
||||||
|
|
||||||
// 使用三个颜色点创建更丰富的渐变
|
|
||||||
return `linear-gradient(to bottom, ${lightColor} 0%, ${midColor} 50%, ${darkColor} 100%)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions (unchanged)
|
|
||||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
|
||||||
r /= 255;
|
|
||||||
g /= 255;
|
|
||||||
b /= 255;
|
|
||||||
const max = Math.max(r, g, b);
|
|
||||||
const min = Math.min(r, g, b);
|
|
||||||
let h = 0;
|
|
||||||
let s;
|
|
||||||
const l = (max + min) / 2;
|
|
||||||
|
|
||||||
if (max === min) {
|
|
||||||
h = s = 0;
|
|
||||||
} else {
|
|
||||||
const d = max - min;
|
|
||||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
||||||
switch (max) {
|
|
||||||
case r:
|
|
||||||
h = (g - b) / d + (g < b ? 6 : 0);
|
|
||||||
break;
|
|
||||||
case g:
|
|
||||||
h = (b - r) / d + 2;
|
|
||||||
break;
|
|
||||||
case b:
|
|
||||||
h = (r - g) / d + 4;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
h /= 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, s, l];
|
|
||||||
}
|
|
||||||
|
|
||||||
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
||||||
let r;
|
|
||||||
let g;
|
|
||||||
let b;
|
|
||||||
|
|
||||||
if (s === 0) {
|
|
||||||
r = g = b = l;
|
|
||||||
} else {
|
|
||||||
const hue2rgb = (p: number, q: number, t: number) => {
|
|
||||||
if (t < 0) t += 1;
|
|
||||||
if (t > 1) t -= 1;
|
|
||||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
||||||
if (t < 1 / 2) return q;
|
|
||||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
|
|
||||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
||||||
const p = 2 * l - q;
|
|
||||||
r = hue2rgb(p, q, h + 1 / 3);
|
|
||||||
g = hue2rgb(p, q, h);
|
|
||||||
b = hue2rgb(p, q, h - 1 / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新的接口
|
|
||||||
interface ITextColors {
|
|
||||||
primary: string;
|
|
||||||
active: string;
|
|
||||||
theme: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新的函数
|
|
||||||
export const calculateBrightness = (r: number, g: number, b: number): number => {
|
|
||||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseGradient = (gradientStr: string) => {
|
export const parseGradient = (gradientStr: string) => {
|
||||||
const matches = gradientStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/g);
|
if (!gradientStr) return [];
|
||||||
if (!matches) return [];
|
|
||||||
return matches.map((rgb) => {
|
// 处理非渐变色
|
||||||
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
|
if (!gradientStr.startsWith('linear-gradient')) {
|
||||||
return { r, g, b };
|
const color = tinycolor(gradientStr);
|
||||||
|
if (color.isValid()) {
|
||||||
|
const rgb = color.toRgb();
|
||||||
|
return [{ r: rgb.r, g: rgb.g, b: rgb.b }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理渐变色,支持 rgb、rgba 和十六进制颜色
|
||||||
|
const colorMatches = gradientStr.match(/(?:(?:rgb|rgba)\([^)]+\)|#[0-9a-fA-F]{3,8})/g) || [];
|
||||||
|
return colorMatches.map((color) => {
|
||||||
|
const tc = tinycolor(color);
|
||||||
|
const rgb = tc.toRgb();
|
||||||
|
return { r: rgb.r, g: rgb.g, b: rgb.b };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const interpolateRGB = (start: number, end: number, progress: number) => {
|
|
||||||
return Math.round(start + (end - start) * progress);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGradientString = (
|
|
||||||
colors: { r: number; g: number; b: number }[],
|
|
||||||
percentages = [0, 50, 100]
|
|
||||||
) => {
|
|
||||||
return `linear-gradient(to bottom, ${colors
|
|
||||||
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
|
|
||||||
.join(', ')})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextColors = (gradient: string = ''): ITextColors => {
|
export const getTextColors = (gradient: string = ''): ITextColors => {
|
||||||
const defaultColors = {
|
const defaultColors = {
|
||||||
primary: 'rgba(255, 255, 255, 0.54)',
|
primary: 'rgba(255, 255, 255, 0.54)',
|
||||||
@@ -226,11 +152,12 @@ export const getTextColors = (gradient: string = ''): ITextColors => {
|
|||||||
if (!gradient) return defaultColors;
|
if (!gradient) return defaultColors;
|
||||||
|
|
||||||
const colors = parseGradient(gradient);
|
const colors = parseGradient(gradient);
|
||||||
|
console.log('colors', colors);
|
||||||
if (!colors.length) return defaultColors;
|
if (!colors.length) return defaultColors;
|
||||||
|
|
||||||
const mainColor = colors[1] || colors[0];
|
const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0];
|
||||||
const brightness = calculateBrightness(mainColor.r, mainColor.g, mainColor.b);
|
const tc = tinycolor(mainColor);
|
||||||
const isDark = brightness > 0.6;
|
const isDark = tc.getBrightness() > 155; // tinycolor 的亮度范围是 0-255
|
||||||
|
|
||||||
return {
|
return {
|
||||||
primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',
|
primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',
|
||||||
@@ -243,35 +170,130 @@ export const getHoverBackgroundColor = (isDark: boolean): string => {
|
|||||||
return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)';
|
return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const animateGradient = (
|
export const animateGradient = (() => {
|
||||||
oldGradient: string,
|
let currentAnimation: number | null = null;
|
||||||
newGradient: string,
|
let isAnimating = false;
|
||||||
onUpdate: (gradient: string) => void,
|
let lastProgress = 0;
|
||||||
duration = 1000
|
|
||||||
) => {
|
|
||||||
const startColors = parseGradient(oldGradient);
|
|
||||||
const endColors = parseGradient(newGradient);
|
|
||||||
if (startColors.length !== endColors.length) return null;
|
|
||||||
|
|
||||||
const startTime = performance.now();
|
const validateColors = (colors: ReturnType<typeof parseGradient>) => {
|
||||||
|
return colors.every(
|
||||||
const animate = (currentTime: number) => {
|
(color) =>
|
||||||
const elapsed = currentTime - startTime;
|
typeof color.r === 'number' &&
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
typeof color.g === 'number' &&
|
||||||
|
typeof color.b === 'number' &&
|
||||||
const currentColors = startColors.map((startColor, i) => ({
|
!Number.isNaN(color.r) &&
|
||||||
r: interpolateRGB(startColor.r, endColors[i].r, progress),
|
!Number.isNaN(color.g) &&
|
||||||
g: interpolateRGB(startColor.g, endColors[i].g, progress),
|
!Number.isNaN(color.b)
|
||||||
b: interpolateRGB(startColor.b, endColors[i].b, progress)
|
);
|
||||||
}));
|
|
||||||
|
|
||||||
onUpdate(createGradientString(currentColors));
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
return requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return requestAnimationFrame(animate);
|
const easeInOutCubic = (x: number): number => {
|
||||||
|
return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const animate = (
|
||||||
|
oldGradient: string,
|
||||||
|
newGradient: string,
|
||||||
|
onUpdate: (gradient: string) => void,
|
||||||
|
duration = 300
|
||||||
|
) => {
|
||||||
|
// 如果新旧渐变色相同,不执行动画
|
||||||
|
if (oldGradient === newGradient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在动画中,取消当前动画
|
||||||
|
if (currentAnimation !== null) {
|
||||||
|
cancelAnimationFrame(currentAnimation);
|
||||||
|
currentAnimation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析颜色
|
||||||
|
const startColors = parseGradient(oldGradient);
|
||||||
|
const endColors = parseGradient(newGradient);
|
||||||
|
|
||||||
|
// 验证颜色数组
|
||||||
|
if (
|
||||||
|
!startColors.length ||
|
||||||
|
!endColors.length ||
|
||||||
|
!validateColors(startColors) ||
|
||||||
|
!validateColors(endColors)
|
||||||
|
) {
|
||||||
|
console.warn('Invalid color values detected');
|
||||||
|
onUpdate(newGradient); // 直接更新到目标颜色
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果颜色数量不匹配,直接更新到目标颜色
|
||||||
|
if (startColors.length !== endColors.length) {
|
||||||
|
onUpdate(newGradient);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnimating = true;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const animateFrame = (currentTime: number) => {
|
||||||
|
if (!isAnimating) return null;
|
||||||
|
|
||||||
|
const elapsed = currentTime - startTime;
|
||||||
|
const rawProgress = Math.min(elapsed / duration, 1);
|
||||||
|
// 使用缓动函数使动画更平滑
|
||||||
|
const progress = easeInOutCubic(rawProgress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用上一帧的进度来平滑过渡
|
||||||
|
const effectiveProgress = lastProgress + (progress - lastProgress) * 0.6;
|
||||||
|
lastProgress = effectiveProgress;
|
||||||
|
|
||||||
|
const currentColors = startColors.map((startColor, i) => {
|
||||||
|
const start = tinycolor(startColor);
|
||||||
|
const end = tinycolor(endColors[i]);
|
||||||
|
return tinycolor.mix(start, end, effectiveProgress * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const gradientString = createGradientString(
|
||||||
|
currentColors.map((c) => {
|
||||||
|
const rgb = c.toRgb();
|
||||||
|
return { r: rgb.r, g: rgb.g, b: rgb.b };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onUpdate(gradientString);
|
||||||
|
|
||||||
|
if (rawProgress < 1) {
|
||||||
|
currentAnimation = requestAnimationFrame(animateFrame);
|
||||||
|
return currentAnimation;
|
||||||
|
}
|
||||||
|
// 确保最终颜色正确
|
||||||
|
onUpdate(newGradient);
|
||||||
|
isAnimating = false;
|
||||||
|
currentAnimation = null;
|
||||||
|
lastProgress = 0;
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Animation error:', error);
|
||||||
|
onUpdate(newGradient);
|
||||||
|
isAnimating = false;
|
||||||
|
currentAnimation = null;
|
||||||
|
lastProgress = 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
currentAnimation = requestAnimationFrame(animateFrame);
|
||||||
|
return currentAnimation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用更短的防抖时间
|
||||||
|
return useDebounceFn(animate, 50);
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const createGradientString = (
|
||||||
|
colors: { r: number; g: number; b: number }[],
|
||||||
|
percentages = [0, 50, 100]
|
||||||
|
) => {
|
||||||
|
return `linear-gradient(to bottom, ${colors
|
||||||
|
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
|
||||||
|
.join(', ')})`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user