feat:优化全屏歌词界面 添加背景和宽度设置

This commit is contained in:
alger
2025-12-19 00:14:24 +08:00
parent af9117ee5f
commit e2ebbe12e4
8 changed files with 1366 additions and 414 deletions

View File

@@ -223,6 +223,7 @@ export default {
display: 'Display',
interface: 'Interface',
typography: 'Typography',
background: 'Background',
mobile: 'Mobile'
},
pureMode: 'Pure Mode',
@@ -257,6 +258,7 @@ export default {
default: 'Default',
loose: 'Loose'
},
contentWidth: 'Content Width',
mobileLayout: 'Mobile Layout',
layoutOptions: {
default: 'Default',
@@ -270,7 +272,46 @@ export default {
full: 'Full Screen'
},
lyricLines: 'Lyric Lines',
mobileUnavailable: 'This setting is only available on mobile devices'
mobileUnavailable: 'This setting is only available on mobile devices',
// Background settings
background: {
useCustomBackground: 'Use Custom Background',
backgroundMode: 'Background Mode',
modeOptions: {
solid: 'Solid',
gradient: 'Gradient',
image: 'Image',
css: 'Custom CSS'
},
solidColor: 'Select Color',
presetColors: 'Preset Colors',
customColor: 'Custom Color',
gradientEditor: 'Gradient Editor',
gradientColors: 'Gradient Colors',
gradientDirection: 'Gradient Direction',
directionOptions: {
toBottom: 'Top to Bottom',
toRight: 'Left to Right',
toBottomRight: 'Top Left to Bottom Right',
angle45: '45 Degrees',
toTop: 'Bottom to Top',
toLeft: 'Right to Left'
},
addColor: 'Add Color',
removeColor: 'Remove Color',
imageUpload: 'Upload Image',
imagePreview: 'Image Preview',
clearImage: 'Clear Image',
imageBlur: 'Blur',
imageBrightness: 'Brightness',
customCss: 'Custom CSS Style',
customCssPlaceholder: 'Enter CSS style, e.g.: background: linear-gradient(...)',
customCssHelp: 'Supports any CSS background property',
reset: 'Reset to Default',
fileSizeLimit: 'Image size limit: 20MB',
invalidImageFormat: 'Invalid image format',
imageTooLarge: 'Image too large, please select an image smaller than 20MB'
}
},
translationEngine: 'Lyric Translation Engine',
translationEngineOptions: {

View File

@@ -222,6 +222,7 @@ export default {
display: '表示',
interface: 'インターフェース',
typography: 'テキスト',
background: '背景',
mobile: 'モバイル'
},
pureMode: 'ピュアモード',
@@ -256,6 +257,7 @@ export default {
default: 'デフォルト',
loose: 'ゆったり'
},
contentWidth: 'コンテンツ幅',
mobileLayout: 'モバイルレイアウト',
layoutOptions: {
default: 'デフォルト',
@@ -269,7 +271,46 @@ export default {
full: 'フルスクリーン'
},
lyricLines: '歌詞行数',
mobileUnavailable: 'この設定はモバイルでのみ利用可能です'
mobileUnavailable: 'この設定はモバイルでのみ利用可能です',
// 背景設定
background: {
useCustomBackground: 'カスタム背景を使用',
backgroundMode: '背景モード',
modeOptions: {
solid: '単色',
gradient: 'グラデーション',
image: '画像',
css: 'カスタム CSS'
},
solidColor: '色を選択',
presetColors: 'プリセットカラー',
customColor: 'カスタムカラー',
gradientEditor: 'グラデーションエディター',
gradientColors: 'グラデーションカラー',
gradientDirection: 'グラデーション方向',
directionOptions: {
toBottom: '上から下',
toRight: '左から右',
toBottomRight: '左上から右下',
angle45: '45度',
toTop: '下から上',
toLeft: '右から左'
},
addColor: '色を追加',
removeColor: '色を削除',
imageUpload: '画像をアップロード',
imagePreview: '画像プレビュー',
clearImage: '画像をクリア',
imageBlur: 'ぼかし',
imageBrightness: '明るさ',
customCss: 'カスタム CSS スタイル',
customCssPlaceholder: 'CSSスタイルを入力、例: background: linear-gradient(...)',
customCssHelp: '任意のCSS background プロパティをサポート',
reset: 'デフォルトにリセット',
fileSizeLimit: '画像サイズ制限: 20MB',
invalidImageFormat: '無効な画像形式',
imageTooLarge: '画像が大きすぎます。20MB未満の画像を選択してください'
}
},
translationEngine: '歌詞翻訳エンジン',
translationEngineOptions: {

View File

@@ -223,6 +223,7 @@ export default {
display: '표시',
interface: '인터페이스',
typography: '텍스트',
background: '배경',
mobile: '모바일'
},
pureMode: '순수 모드',
@@ -257,6 +258,7 @@ export default {
default: '기본',
loose: '넓음'
},
contentWidth: '콘텐츠 너비',
mobileLayout: '모바일 레이아웃',
layoutOptions: {
default: '기본',
@@ -270,7 +272,46 @@ export default {
full: '전체화면'
},
lyricLines: '가사 줄 수',
mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다'
mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다',
// 배경 설정
background: {
useCustomBackground: '사용자 정의 배경 사용',
backgroundMode: '배경 모드',
modeOptions: {
solid: '단색',
gradient: '그라데이션',
image: '이미지',
css: '사용자 정의 CSS'
},
solidColor: '색상 선택',
presetColors: '프리셋 색상',
customColor: '사용자 정의 색상',
gradientEditor: '그라데이션 편집기',
gradientColors: '그라데이션 색상',
gradientDirection: '그라데이션 방향',
directionOptions: {
toBottom: '위에서 아래로',
toRight: '왼쪽에서 오른쪽으로',
toBottomRight: '왼쪽 위에서 오른쪽 아래로',
angle45: '45도',
toTop: '아래에서 위로',
toLeft: '오른쪽에서 왼쪽으로'
},
addColor: '색상 추가',
removeColor: '색상 제거',
imageUpload: '이미지 업로드',
imagePreview: '이미지 미리보기',
clearImage: '이미지 지우기',
imageBlur: '흐림',
imageBrightness: '밝기',
customCss: '사용자 정의 CSS 스타일',
customCssPlaceholder: 'CSS 스타일 입력, 예: background: linear-gradient(...)',
customCssHelp: '모든 CSS background 속성 지원',
reset: '기본값으로 재설정',
fileSizeLimit: '이미지 크기 제한: 20MB',
invalidImageFormat: '잘못된 이미지 형식',
imageTooLarge: '이미지가 너무 큽니다. 20MB 미만의 이미지를 선택하세요'
}
},
translationEngine: '가사 번역 엔진',
translationEngineOptions: {

View File

@@ -220,6 +220,7 @@ export default {
display: '显示',
interface: '界面',
typography: '文字',
background: '背景',
mobile: '移动端'
},
pureMode: '纯净模式',
@@ -254,6 +255,7 @@ export default {
default: '默认',
loose: '宽松'
},
contentWidth: '内容区宽度',
mobileLayout: '移动端布局',
layoutOptions: {
default: '默认',
@@ -267,7 +269,46 @@ export default {
full: '全屏'
},
lyricLines: '歌词行数',
mobileUnavailable: '此设置仅在移动端可用'
mobileUnavailable: '此设置仅在移动端可用',
// 背景设置
background: {
useCustomBackground: '使用自定义背景',
backgroundMode: '背景模式',
modeOptions: {
solid: '纯色',
gradient: '渐变',
image: '图片',
css: '自定义 CSS'
},
solidColor: '选择颜色',
presetColors: '预设颜色',
customColor: '自定义颜色',
gradientEditor: '渐变编辑器',
gradientColors: '渐变颜色',
gradientDirection: '渐变方向',
directionOptions: {
toBottom: '上到下',
toRight: '左到右',
toBottomRight: '左上到右下',
angle45: '45度',
toTop: '下到上',
toLeft: '右到左'
},
addColor: '添加颜色',
removeColor: '移除颜色',
imageUpload: '上传图片',
imagePreview: '图片预览',
clearImage: '清除图片',
imageBlur: '模糊度',
imageBrightness: '明暗度',
customCss: '自定义 CSS 样式',
customCssPlaceholder: '输入 CSS 样式,如: background: linear-gradient(...)',
customCssHelp: '支持任意 CSS background 属性',
reset: '重置为默认',
fileSizeLimit: '图片大小限制: 20MB',
invalidImageFormat: '无效的图片格式',
imageTooLarge: '图片过大,请选择小于 20MB 的图片'
}
},
translationEngine: '歌詞翻譯引擎',
translationEngineOptions: {

View File

@@ -217,6 +217,7 @@ export default {
display: '顯示',
interface: '介面',
typography: '文字',
background: '背景',
mobile: '行動端'
},
pureMode: '純淨模式',
@@ -244,6 +245,66 @@ export default {
compact: '緊湊',
default: '預設',
loose: '寬鬆'
},
lineHeight: '行高',
lineHeightMarks: {
compact: '緊湊',
default: '預設',
loose: '寬鬆'
},
contentWidth: '內容區寬度',
mobileLayout: '行動端佈局',
layoutOptions: {
default: '預設',
ios: 'iOS 風格',
android: 'Android 風格'
},
mobileCoverStyle: '封面風格',
coverOptions: {
record: '唱片',
square: '方形',
full: '全螢幕'
},
lyricLines: '歌詞行數',
mobileUnavailable: '此設定僅在行動端可用',
// 背景設定
background: {
useCustomBackground: '使用自訂背景',
backgroundMode: '背景模式',
modeOptions: {
solid: '純色',
gradient: '漸層',
image: '圖片',
css: '自訂 CSS'
},
solidColor: '選擇顏色',
presetColors: '預設顏色',
customColor: '自訂顏色',
gradientEditor: '漸層編輯器',
gradientColors: '漸層顏色',
gradientDirection: '漸層方向',
directionOptions: {
toBottom: '上到下',
toRight: '左到右',
toBottomRight: '左上到右下',
angle45: '45度',
toTop: '下到上',
toLeft: '右到左'
},
addColor: '新增顏色',
removeColor: '移除顏色',
imageUpload: '上傳圖片',
imagePreview: '圖片預覽',
clearImage: '清除圖片',
imageBlur: '模糊度',
imageBrightness: '明暗度',
customCss: '自訂 CSS 樣式',
customCssPlaceholder: '輸入 CSS 樣式,如: background: linear-gradient(...)',
customCssHelp: '支援任意 CSS background 屬性',
reset: '重設為預設',
fileSizeLimit: '圖片大小限制: 20MB',
invalidImageFormat: '無效的圖片格式',
imageTooLarge: '圖片過大,請選擇小於 20MB 的圖片'
}
},
themeColor: {
@@ -271,6 +332,46 @@ export default {
none: '關閉',
opencc: 'OpenCC 繁化'
},
shortcutSettings: {
title: '快捷鍵設定',
shortcut: '快捷鍵',
shortcutDesc: '自訂快捷鍵',
shortcutConflict: '快捷鍵衝突',
inputPlaceholder: '點擊輸入快捷鍵',
resetShortcuts: '恢復預設',
disableAll: '全部停用',
enableAll: '全部啟用',
togglePlay: '播放/暫停',
prevPlay: '上一首',
nextPlay: '下一首',
volumeUp: '增加音量',
volumeDown: '減少音量',
toggleFavorite: '收藏/取消收藏',
toggleWindow: '顯示/隱藏視窗',
scopeGlobal: '全域',
scopeApp: '應用程式內',
enabled: '已啟用',
disabled: '已停用',
messages: {
resetSuccess: '已恢復預設快捷鍵,請記得儲存',
conflict: '存在快捷鍵衝突,請重新設定',
saveSuccess: '快捷鍵設定已儲存',
saveError: '快捷鍵儲存失敗,請重試',
cancelEdit: '已取消修改',
disableAll: '已停用所有快捷鍵,請記得儲存',
enableAll: '已啟用所有快捷鍵,請記得儲存'
}
},
remoteControl: {
title: '遠端控制',
enable: '啟用遠端控制',
port: '服務連接埠',
allowedIps: '允許的 IP 位址',
addIp: '新增 IP',
emptyListHint: '空白清單表示允許所有 IP 存取',
saveSuccess: '遠端控制設定已儲存',
accessInfo: '遠端控制存取位址:'
},
cookie: {
title: 'Cookie設定',
description: '請輸入網易雲音樂的Cookie',

View File

@@ -1,116 +1,365 @@
<template>
<div class="settings-panel transparent-popover">
<div class="settings-title">{{ t('settings.lyricSettings.title') }}</div>
<div class="settings-content">
<n-tabs type="line" animated size="small">
<!-- 显示设置 -->
<n-tab-pane :name="'display'" :tab="t('settings.lyricSettings.tabs.display')">
<div class="tab-content">
<div class="settings-grid">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.pureMode') }}</span>
<n-switch v-model:value="config.pureModeEnabled" />
<div
class="w-96 rounded-2xl bg-white/5 backdrop-blur-3xl border border-white/10 shadow-2xl overflow-hidden"
>
<!-- 标题栏 -->
<div class="px-6 py-4 border-b border-white/5">
<h2 class="text-lg font-semibold tracking-tight" style="color: var(--text-color-active)">
{{ t('settings.lyricSettings.title') }}
</h2>
</div>
<!-- 标签页导航 -->
<div class="px-4 pt-3 pb-2">
<div class="flex gap-1 p-1 bg-black/20 rounded-xl">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',
activeTab === tab.key
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/30'
: 'hover:bg-white/5'
]"
:style="activeTab !== tab.key ? 'color: var(--text-color-primary); opacity: 0.7' : ''"
>
{{ tab.label }}
</button>
</div>
</div>
<!-- 内容区域 -->
<div
class="px-4 pb-4 max-h-[500px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent"
>
<!-- 显示设置 -->
<div v-show="activeTab === 'display'" class="space-y-3 pt-3">
<div class="setting-item">
<span>{{ t('settings.lyricSettings.pureMode') }}</span>
<input type="checkbox" v-model="config.pureModeEnabled" class="toggle-switch" />
</div>
<div class="setting-item">
<span>{{ t('settings.lyricSettings.hideCover') }}</span>
<input type="checkbox" v-model="config.hideCover" class="toggle-switch" />
</div>
<div class="setting-item">
<span>{{ t('settings.lyricSettings.centerDisplay') }}</span>
<input type="checkbox" v-model="config.centerLyrics" class="toggle-switch" />
</div>
<div class="setting-item">
<span>{{ t('settings.lyricSettings.showTranslation') }}</span>
<input type="checkbox" v-model="config.showTranslation" class="toggle-switch" />
</div>
<div class="setting-item">
<span>{{ t('settings.lyricSettings.hideLyrics') }}</span>
<input type="checkbox" v-model="config.hideLyrics" class="toggle-switch" />
</div>
</div>
<!-- 界面设置 -->
<div v-show="activeTab === 'interface'" class="space-y-4 pt-3">
<div class="setting-item">
<span>{{ t('settings.lyricSettings.showMiniPlayBar') }}</span>
<input type="checkbox" v-model="showMiniPlayBar" class="toggle-switch" />
</div>
<div class="slider-group">
<label class="slider-label">{{ t('settings.lyricSettings.contentWidth') }}</label>
<input
type="range"
v-model.number="config.contentWidth"
min="50"
max="100"
step="5"
class="slider-emerald"
/>
<div class="slider-marks">
<span>50%</span>
<span>75%</span>
<span>100%</span>
</div>
</div>
</div>
<!-- 文字设置 -->
<div v-show="activeTab === 'typography'" class="space-y-4 pt-3">
<div class="slider-group">
<label class="slider-label">{{ t('settings.lyricSettings.fontSize') }}</label>
<input
type="range"
v-model.number="config.fontSize"
min="12"
max="32"
step="1"
class="slider-emerald"
/>
<div class="slider-marks">
<span>{{ t('settings.lyricSettings.fontSizeMarks.small') }}</span>
<span>{{ t('settings.lyricSettings.fontSizeMarks.medium') }}</span>
<span>{{ t('settings.lyricSettings.fontSizeMarks.large') }}</span>
</div>
</div>
<div class="slider-group">
<label class="slider-label">{{ t('settings.lyricSettings.letterSpacing') }}</label>
<input
type="range"
v-model.number="config.letterSpacing"
min="-2"
max="10"
step="0.2"
class="slider-emerald"
/>
<div class="slider-marks">
<span>{{ t('settings.lyricSettings.letterSpacingMarks.compact') }}</span>
<span>{{ t('settings.lyricSettings.letterSpacingMarks.default') }}</span>
<span>{{ t('settings.lyricSettings.letterSpacingMarks.loose') }}</span>
</div>
</div>
<div class="slider-group">
<label class="slider-label">{{ t('settings.lyricSettings.lineHeight') }}</label>
<input
type="range"
v-model.number="config.lineHeight"
min="1"
max="3"
step="0.1"
class="slider-emerald"
/>
<div class="slider-marks">
<span>{{ t('settings.lyricSettings.lineHeightMarks.compact') }}</span>
<span>{{ t('settings.lyricSettings.lineHeightMarks.default') }}</span>
<span>{{ t('settings.lyricSettings.lineHeightMarks.loose') }}</span>
</div>
</div>
</div>
<!-- 背景设置 -->
<div v-show="activeTab === 'background'" class="space-y-4 pt-3">
<div class="setting-item">
<span>{{ t('settings.lyricSettings.background.useCustomBackground') }}</span>
<input type="checkbox" v-model="config.useCustomBackground" class="toggle-switch" />
</div>
<!-- 主题选择 -->
<div v-if="!config.useCustomBackground" class="radio-group">
<label class="radio-label">{{ t('settings.lyricSettings.backgroundTheme') }}</label>
<div class="space-y-2">
<label class="radio-item">
<input type="radio" v-model="config.theme" value="default" class="radio-input" />
<span>{{ t('settings.lyricSettings.themeOptions.default') }}</span>
</label>
<label class="radio-item">
<input type="radio" v-model="config.theme" value="light" class="radio-input" />
<span>{{ t('settings.lyricSettings.themeOptions.light') }}</span>
</label>
<label class="radio-item">
<input type="radio" v-model="config.theme" value="dark" class="radio-input" />
<span>{{ t('settings.lyricSettings.themeOptions.dark') }}</span>
</label>
</div>
</div>
<!-- 背景模式选择 -->
<div v-if="config.useCustomBackground" class="radio-group">
<label class="radio-label">{{
t('settings.lyricSettings.background.backgroundMode')
}}</label>
<div class="grid grid-cols-2 gap-2">
<label class="radio-item-compact">
<input
type="radio"
v-model="config.backgroundMode"
value="solid"
class="radio-input"
/>
<span>{{ t('settings.lyricSettings.background.modeOptions.solid') }}</span>
</label>
<label class="radio-item-compact">
<input
type="radio"
v-model="config.backgroundMode"
value="gradient"
class="radio-input"
/>
<span>{{ t('settings.lyricSettings.background.modeOptions.gradient') }}</span>
</label>
<label class="radio-item-compact">
<input
type="radio"
v-model="config.backgroundMode"
value="image"
class="radio-input"
/>
<span>{{ t('settings.lyricSettings.background.modeOptions.image') }}</span>
</label>
<label class="radio-item-compact">
<input type="radio" v-model="config.backgroundMode" value="css" class="radio-input" />
<span>{{ t('settings.lyricSettings.background.modeOptions.css') }}</span>
</label>
</div>
</div>
<!-- 纯色模式 -->
<div
v-if="config.useCustomBackground && config.backgroundMode === 'solid'"
class="color-picker-group"
>
<label class="color-picker-label">{{
t('settings.lyricSettings.background.solidColor')
}}</label>
<input type="color" v-model="config.solidColor" class="color-picker" />
</div>
<!-- 渐变模式 -->
<div
v-if="config.useCustomBackground && config.backgroundMode === 'gradient'"
class="space-y-3"
>
<label class="color-picker-label">{{
t('settings.lyricSettings.background.gradientEditor')
}}</label>
<div class="flex flex-wrap gap-2">
<div v-for="(_, index) in config.gradientColors.colors" :key="index" class="relative">
<input
type="color"
v-model="config.gradientColors.colors[index]"
class="color-picker-small"
/>
<button
v-if="config.gradientColors.colors.length > 2"
@click="removeGradientColor(index)"
class="absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center rounded-full bg-red-500 text-white text-xs hover:bg-red-600 transition-colors"
>
<i class="ri-close-line"></i>
</button>
</div>
</div>
<button
v-if="config.gradientColors.colors.length < 5"
@click="addGradientColor"
class="w-full py-2 px-4 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 transition-colors text-sm font-medium flex items-center justify-center gap-2"
style="color: var(--text-color-active)"
>
<i class="ri-add-line"></i>
{{ t('settings.lyricSettings.background.addColor') }}
</button>
<div class="select-group">
<label class="select-label">{{
t('settings.lyricSettings.background.gradientDirection')
}}</label>
<select v-model="config.gradientColors.direction" class="select-input">
<option v-for="opt in gradientDirectionOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</div>
<!-- 图片模式 -->
<div
v-if="config.useCustomBackground && config.backgroundMode === 'image'"
class="space-y-3"
>
<label class="color-picker-label">{{
t('settings.lyricSettings.background.imageUpload')
}}</label>
<input
type="file"
accept="image/*"
@change="handleImageChange"
class="hidden"
ref="fileInput"
/>
<button
@click="fileInput?.click()"
class="w-full py-2 px-4 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 transition-colors text-sm font-medium flex items-center justify-center gap-2"
style="color: var(--text-color-active)"
>
<i class="ri-image-add-line"></i>
{{ t('settings.lyricSettings.background.imageUpload') }}
</button>
<div v-if="config.backgroundImage" class="space-y-3">
<div class="relative rounded-lg overflow-hidden border border-white/10">
<img
:src="config.backgroundImage"
class="w-full max-h-40 object-cover"
alt="Preview"
/>
<button
@click="clearBackgroundImage"
class="absolute top-2 right-2 p-2 rounded-lg bg-red-500/80 text-white hover:bg-red-500 transition-colors"
>
<i class="ri-delete-bin-line"></i>
</button>
</div>
<div class="slider-group">
<label class="slider-label">{{
t('settings.lyricSettings.background.imageBlur')
}}</label>
<input
type="range"
v-model.number="config.imageBlur"
min="0"
max="20"
step="1"
class="slider-emerald"
/>
<div class="slider-marks">
<span>0</span>
<span>10</span>
<span>20px</span>
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideCover') }}</span>
<n-switch v-model:value="config.hideCover" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.centerDisplay') }}</span>
<n-switch v-model:value="config.centerLyrics" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.showTranslation') }}</span>
<n-switch v-model:value="config.showTranslation" />
</div>
<div class="settings-item">
<span>{{ t('settings.lyricSettings.hideLyrics') }}</span>
<n-switch v-model:value="config.hideLyrics" />
</div>
<div class="slider-group">
<label class="slider-label">{{
t('settings.lyricSettings.background.imageBrightness')
}}</label>
<input
type="range"
v-model.number="config.imageBrightness"
min="0"
max="200"
step="5"
class="slider-emerald"
/>
<div class="slider-marks">
<span></span>
<span>正常</span>
<span></span>
</div>
</div>
</div>
</n-tab-pane>
<!-- 界面设置 -->
<n-tab-pane :name="'interface'" :tab="t('settings.lyricSettings.tabs.interface')">
<div class="tab-content">
<div class="settings-grid">
<div class="settings-item">
<span>{{ t('settings.lyricSettings.showMiniPlayBar') }}</span>
<n-switch v-model:value="showMiniPlayBar" />
</div>
</div>
<div class="theme-section">
<div class="section-title">{{ t('settings.lyricSettings.backgroundTheme') }}</div>
<n-radio-group v-model:value="config.theme" name="theme" class="theme-radio-group">
<n-space>
<n-radio value="default">{{
t('settings.lyricSettings.themeOptions.default')
}}</n-radio>
<n-radio value="light">{{
t('settings.lyricSettings.themeOptions.light')
}}</n-radio>
<n-radio value="dark">{{
t('settings.lyricSettings.themeOptions.dark')
}}</n-radio>
</n-space>
</n-radio-group>
</div>
</div>
</n-tab-pane>
<p class="text-xs" style="color: var(--text-color-primary); opacity: 0.5">
{{ t('settings.lyricSettings.background.fileSizeLimit') }}
</p>
</div>
<!-- 文字设置 -->
<n-tab-pane :name="'typography'" :tab="t('settings.lyricSettings.tabs.typography')">
<div class="tab-content">
<div class="slider-section">
<div class="slider-item">
<span>{{ t('settings.lyricSettings.fontSize') }}</span>
<n-slider
v-model:value="config.fontSize"
:step="1"
:min="12"
:max="32"
:marks="{
12: t('settings.lyricSettings.fontSizeMarks.small'),
22: t('settings.lyricSettings.fontSizeMarks.medium'),
32: t('settings.lyricSettings.fontSizeMarks.large')
}"
/>
</div>
<div class="slider-item">
<span>{{ t('settings.lyricSettings.letterSpacing') }}</span>
<n-slider
v-model:value="config.letterSpacing"
:step="0.2"
:min="-2"
:max="10"
:marks="{
'-2': t('settings.lyricSettings.letterSpacingMarks.compact'),
0: t('settings.lyricSettings.letterSpacingMarks.default'),
10: t('settings.lyricSettings.letterSpacingMarks.loose')
}"
/>
</div>
<div class="slider-item">
<span>{{ t('settings.lyricSettings.lineHeight') }}</span>
<n-slider
v-model:value="config.lineHeight"
:step="0.1"
:min="1"
:max="3"
:marks="{
1: t('settings.lyricSettings.lineHeightMarks.compact'),
1.5: t('settings.lyricSettings.lineHeightMarks.default'),
3: t('settings.lyricSettings.lineHeightMarks.loose')
}"
/>
</div>
</div>
</div>
</n-tab-pane>
</n-tabs>
<!-- CSS 模式 -->
<div v-if="config.useCustomBackground && config.backgroundMode === 'css'" class="space-y-2">
<label class="color-picker-label">{{
t('settings.lyricSettings.background.customCss')
}}</label>
<textarea
v-model="config.customCss"
:placeholder="t('settings.lyricSettings.background.customCssPlaceholder')"
rows="4"
class="w-full px-3 py-2 bg-black/20 border border-white/10 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/50 font-mono"
style="color: var(--text-color-primary)"
></textarea>
<p class="text-xs" style="color: var(--text-color-primary); opacity: 0.5">
{{ t('settings.lyricSettings.background.customCssHelp') }}
</p>
</div>
</div>
</div>
</div>
</template>
@@ -124,26 +373,82 @@ import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
const { t } = useI18n();
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
const emit = defineEmits(['themeChange']);
const message = window.$message;
const activeTab = ref('display');
const fileInput = ref<HTMLInputElement>();
const tabs = computed(() => [
{ key: 'display', label: t('settings.lyricSettings.tabs.display') },
{ key: 'interface', label: t('settings.lyricSettings.tabs.interface') },
{ key: 'typography', label: t('settings.lyricSettings.tabs.typography') },
{ key: 'background', label: t('settings.lyricSettings.tabs.background') }
]);
// 显示mini播放栏开关
const showMiniPlayBar = computed({
get: () => !config.value.hideMiniPlayBar,
set: (value: boolean) => {
if (value) {
// 显示mini播放栏隐藏普通播放栏
config.value.hideMiniPlayBar = false;
config.value.hidePlayBar = true;
} else {
// 显示普通播放栏隐藏mini播放栏
config.value.hideMiniPlayBar = true;
config.value.hidePlayBar = false;
}
config.value.hideMiniPlayBar = !value;
config.value.hidePlayBar = value;
}
});
const gradientDirectionOptions = computed(() => [
{ label: t('settings.lyricSettings.background.directionOptions.toBottom'), value: 'to bottom' },
{ label: t('settings.lyricSettings.background.directionOptions.toTop'), value: 'to top' },
{ label: t('settings.lyricSettings.background.directionOptions.toRight'), value: 'to right' },
{ label: t('settings.lyricSettings.background.directionOptions.toLeft'), value: 'to left' },
{
label: t('settings.lyricSettings.background.directionOptions.toBottomRight'),
value: 'to bottom right'
},
{ label: t('settings.lyricSettings.background.directionOptions.angle45'), value: '45deg' }
]);
const addGradientColor = () => {
if (config.value.gradientColors.colors.length < 5) {
config.value.gradientColors.colors.push('#666666');
}
};
const removeGradientColor = (index: number) => {
if (config.value.gradientColors.colors.length > 2) {
config.value.gradientColors.colors.splice(index, 1);
}
};
const handleImageChange = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
message?.error(t('settings.lyricSettings.background.invalidImageFormat'));
return;
}
if (file.size > 20 * 1024 * 1024) {
message?.error(t('settings.lyricSettings.background.imageTooLarge'));
return;
}
const reader = new FileReader();
reader.onload = (e) => {
config.value.backgroundImage = e.target?.result as string;
};
reader.readAsDataURL(file);
};
const clearBackgroundImage = () => {
config.value.backgroundImage = undefined;
if (fileInput.value) {
fileInput.value.value = '';
}
};
watch(
() => config.value,
(newConfig) => {
localStorage.setItem('music-full-config', JSON.stringify(newConfig));
updateCSSVariables(newConfig);
},
{ deep: true }
@@ -175,98 +480,304 @@ defineExpose({
});
</script>
<style scoped lang="scss">
.settings-panel {
@apply p-4 w-80 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 {
:deep(.n-tabs-nav) {
@apply mb-3;
}
:deep(.n-tab-pane) {
@apply p-0;
}
:deep(.n-tabs-tab) {
@apply text-xs;
color: var(--text-color-primary);
&.n-tabs-tab--active {
color: var(--text-color-active);
}
}
:deep(.n-tabs-tab-wrapper) {
@apply pb-0;
}
:deep(.n-tabs-pane-wrapper) {
@apply px-2;
}
:deep(.n-tabs-bar) {
background-color: var(--text-color-active);
}
}
.tab-content {
@apply py-2;
}
.settings-grid {
@apply grid grid-cols-1 gap-3;
}
.settings-item {
@apply flex items-center justify-between;
span {
@apply text-sm;
color: var(--text-color-primary);
}
}
.section-title {
@apply text-sm font-medium mb-2;
color: var(--text-color-primary);
}
.theme-section {
@apply mt-4;
}
.slider-section {
@apply space-y-6;
}
.slider-item {
@apply space-y-2 mb-10 !important;
span {
@apply text-sm;
color: var(--text-color-primary);
}
}
.theme-radio-group {
@apply flex;
}
<style scoped>
/* 设置项 */
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
transition: all 0.2s;
font-size: 14px;
color: var(--text-color-primary);
}
:deep(.n-slider-mark) {
color: var(--text-color-primary) !important;
.setting-item:hover {
background: rgba(255, 255, 255, 0.06);
}
:deep(.n-radio__label) {
color: var(--text-color-active) !important;
@apply text-xs;
/* 切换开关 */
.toggle-switch {
appearance: none;
width: 44px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
position: relative;
cursor: pointer;
transition: all 0.3s;
}
.mobile-unavailable {
@apply text-center py-4 text-gray-500 text-sm;
.toggle-switch::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
left: 2px;
top: 2px;
transition: all 0.3s;
}
.toggle-switch:checked {
background: #10b981;
}
.toggle-switch:checked::before {
left: 22px;
}
/* 滑块组 */
.slider-group {
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.slider-label {
display: block;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-color-primary);
opacity: 0.7;
margin-bottom: 12px;
}
.slider-emerald {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
outline: none;
appearance: none;
}
.slider-emerald::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: #10b981;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
}
.slider-emerald::-moz-range-thumb {
width: 16px;
height: 16px;
background: #10b981;
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
}
.slider-marks {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 11px;
color: var(--text-color-primary);
opacity: 0.5;
}
/* 单选框组 */
.radio-group {
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.radio-label {
display: block;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-color-primary);
opacity: 0.7;
margin-bottom: 12px;
}
.radio-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: var(--text-color-primary);
}
.radio-item:hover {
background: rgba(255, 255, 255, 0.05);
}
/* 紧凑版单选项(用于横向布局) */
.radio-item-compact {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
color: var(--text-color-primary);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.radio-item-compact:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.1);
}
.radio-input {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid var(--text-color-primary);
opacity: 0.4;
border-radius: 50%;
margin-right: 12px;
position: relative;
cursor: pointer;
flex-shrink: 0;
}
.radio-input:checked {
border-color: #10b981;
opacity: 1;
}
.radio-input:checked::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
left: 2px;
top: 2px;
}
/* 颜色选择器 */
.color-picker-group {
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.color-picker-label {
display: block;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-color-primary);
opacity: 0.7;
margin-bottom: 12px;
}
.color-picker {
width: 100%;
height: 48px;
border: none;
border-radius: 8px;
cursor: pointer;
background: transparent;
}
.color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker::-webkit-color-swatch {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
/* 小尺寸颜色选择器(用于渐变) */
.color-picker-small {
width: 56px;
height: 56px;
border: none;
border-radius: 12px;
cursor: pointer;
background: transparent;
}
.color-picker-small::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker-small::-webkit-color-swatch {
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
}
/* 下拉选择 */
.select-group {
padding: 16px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.select-label {
display: block;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-color-primary);
opacity: 0.7;
margin-bottom: 12px;
}
.select-input {
width: 100%;
padding: 10px 12px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--text-color-primary);
font-size: 14px;
cursor: pointer;
outline: none;
}
.select-input:focus {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
/* 滚动条 */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -3,19 +3,34 @@
v-model:show="isVisible"
height="100%"
placement="bottom"
:style="{ background: currentBackground || background }"
:style="drawerBaseStyle"
:to="`#layout-main`"
:z-index="9998"
>
<div id="drawer-target" :class="[config.theme]">
<!-- 背景层用于图片模糊和明暗效果 -->
<div
v-if="
config.useCustomBackground && config.backgroundMode === 'image' && config.backgroundImage
"
class="background-layer"
:style="backgroundImageStyle"
></div>
<div id="drawer-target" :class="[config.theme]" class="relative z-10">
<!-- 左侧关闭按钮 -->
<div
class="control-buttons-container absolute top-8 left-8 right-8"
class="control-left absolute top-8 left-8 z-[9999]"
:class="{ 'pure-mode': config.pureModeEnabled }"
>
<div class="control-btn" @click="closeMusicFull">
<i class="ri-arrow-down-s-line"></i>
</div>
</div>
<!-- 右侧功能按钮组 -->
<div
class="control-right absolute top-8 right-8 z-[9999]"
:class="{ 'pure-mode': config.pureModeEnabled }"
>
<n-popover trigger="click" placement="bottom">
<template #trigger>
<div class="control-btn">
@@ -24,82 +39,40 @@
</template>
<lyric-settings ref="lyricSettingsRef" />
</n-popover>
</div>
<div
v-if="!config.hideCover"
class="music-img"
:class="{ 'only-cover': config.hideLyrics }"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
>
<div class="img-container">
<cover3-d
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '500y500')"
:loading="playMusic?.playLoading"
:max-tilt="12"
:scale="1.03"
:shine-intensity="0.25"
/>
</div>
<div class="music-info">
<div class="music-content-name" v-html="playMusic.name"></div>
<div class="music-content-singer">
<n-ellipsis
class="text-ellipsis"
line-clamp="2"
:tooltip="{
contentStyle: { maxWidth: '600px' },
zIndex: 99999
}"
>
<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>
</n-ellipsis>
</div>
<simple-play-bar
v-if="!config.hideMiniPlayBar"
class="mt-4"
:pure-mode-enabled="config.pureModeEnabled"
:isDark="textColors.theme === 'dark'"
/>
<div class="control-btn" @click="toggleFullScreen">
<i :class="isFullScreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"></i>
</div>
</div>
<div
class="music-content"
:class="{
center: config.centerLyrics,
hide: config.hideLyrics
}"
>
<n-layout
ref="lrcSider"
class="music-lrc"
:style="{
height: config.hidePlayBar ? '85vh' : '65vh',
width: isMobile ? '100vw' : config.hideCover ? '50vw' : '500px'
}"
:native-scrollbar="false"
@mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout"
<div class="content-wrapper" :style="{ width: `${config.contentWidth}%` }">
<!-- 左侧:封面区域 -->
<div
v-if="!config.hideCover"
class="left-side"
:class="{ 'only-cover': config.hideLyrics }"
>
<!-- 歌曲信息 -->
<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" v-html="playMusic.name"></div>
<div class="music-info-singer">
<div class="img-container">
<cover3-d
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '500y500')"
:loading="playMusic?.playLoading"
:max-tilt="12"
:scale="1.03"
:shine-intensity="0.25"
/>
</div>
<div class="music-info">
<div class="music-content-name" v-html="playMusic.name"></div>
<div class="music-content-singer">
<n-ellipsis
class="text-ellipsis"
line-clamp="2"
:tooltip="{
contentStyle: { maxWidth: '600px' },
zIndex: 99999
}"
>
<span
v-for="(item, index) in artistList"
:key="index"
@@ -109,53 +82,99 @@
{{ item.name }}
{{ index < artistList.length - 1 ? ' / ' : '' }}
</span>
</div>
</div>
<!-- 无时间戳歌词提示 -->
<div v-if="!supportAutoScroll" class="music-lrc-text no-scroll-tip">
<span>{{ t('player.lrc.noAutoScroll') }}</span>
</div>
<div
v-for="(item, index) in lrcArray"
:id="`music-lrc-text-${index}`"
:key="index"
class="music-lrc-text"
:class="{
'now-text': index === nowIndex,
'hover-text': item.text && item.startTime !== -1
}"
@click="item.startTime !== -1 ? setAudioTime(index) : null"
>
<!-- 逐字歌词显示 -->
<div
v-if="item.hasWordByWord && item.words && item.words.length > 0"
class="word-by-word-lyric"
>
<template v-for="(word, wordIndex) in item.words" :key="wordIndex">
<span class="lyric-word" :style="getWordStyle(index, wordIndex, word)">
{{ word.text }} </span
><span class="lyric-word" v-if="word.space">&nbsp;</span></template
>
</div>
<!-- 普通歌词显示 -->
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
<div v-show="config.showTranslation" class="music-lrc-text-tr">
{{ item.trText }}
</div>
</div>
<!-- 无歌词 -->
<div v-if="!lrcArray.length" class="music-lrc-text">
<span>{{ t('player.lrc.noLrc') }}</span>
</n-ellipsis>
</div>
<simple-play-bar
v-if="!config.hideMiniPlayBar"
class="mt-4"
:pure-mode-enabled="config.pureModeEnabled"
:isDark="textColors.theme === 'dark'"
/>
</div>
<!-- 歌词右下角矫正按钮组件 -->
<lyric-correction-control
v-if="!isMobile"
:correction-time="correctionTime"
@adjust="adjustCorrectionTime"
/>
</n-layout>
</div>
<!-- 右侧:歌词区域 -->
<div
class="right-side"
:class="{
center: config.centerLyrics,
hide: config.hideLyrics,
'full-width': config.hideCover
}"
>
<n-layout
ref="lrcSider"
class="music-lrc"
:native-scrollbar="false"
@mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout"
>
<!-- 歌曲信息 -->
<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" v-html="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 v-if="!supportAutoScroll" class="music-lrc-text no-scroll-tip">
<span>{{ t('player.lrc.noAutoScroll') }}</span>
</div>
<div
v-for="(item, index) in lrcArray"
:id="`music-lrc-text-${index}`"
:key="index"
class="music-lrc-text"
:class="{
'now-text': index === nowIndex,
'hover-text': item.text && item.startTime !== -1
}"
@click="item.startTime !== -1 ? setAudioTime(index) : null"
>
<!-- 逐字歌词显示 -->
<div
v-if="item.hasWordByWord && item.words && item.words.length > 0"
class="word-by-word-lyric"
>
<template v-for="(word, wordIndex) in item.words" :key="wordIndex">
<span class="lyric-word" :style="getWordStyle(index, wordIndex, word)">
{{ word.text }} </span
><span class="lyric-word" v-if="word.space">&nbsp;</span></template
>
</div>
<!-- 普通歌词显示 -->
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
<div v-show="config.showTranslation" class="music-lrc-text-tr">
{{ item.trText }}
</div>
</div>
<!-- 无歌词 -->
<div v-if="!lrcArray.length" class="music-lrc-text">
<span>{{ t('player.lrc.noLrc') }}</span>
</div>
</div>
<!-- 歌词右下角矫正按钮组件 -->
<lyric-correction-control
v-if="!isMobile"
:correction-time="correctionTime"
@adjust="adjustCorrectionTime"
/>
</n-layout>
</div>
</div>
</div>
</n-drawer>
@@ -197,9 +216,57 @@ const lrcContainer = ref<HTMLElement | null>(null);
const currentBackground = ref('');
const animationFrame = ref<number | null>(null);
const isDark = ref(false);
// 计算自定义背景样式
const customBackgroundStyle = computed(() => {
if (!config.value.useCustomBackground) {
return null;
}
switch (config.value.backgroundMode) {
case 'solid':
return config.value.solidColor;
case 'gradient': {
const { colors, direction } = config.value.gradientColors;
return `linear-gradient(${direction}, ${colors.join(', ')})`;
}
case 'image':
if (!config.value.backgroundImage) return null;
// 构建完整的背景样式,包括滤镜效果
return config.value.backgroundImage;
case 'css':
return config.value.customCss || null;
default:
return null;
}
});
// drawer 基础样式(非图片模式)
const drawerBaseStyle = computed(() => {
// 图片模式时不设置背景,使用单独的背景层
if (config.value.useCustomBackground && config.value.backgroundMode === 'image') {
return { background: 'transparent' };
}
// 其他模式正常设置背景
if (config.value.useCustomBackground && customBackgroundStyle.value) {
return { background: customBackgroundStyle.value };
}
return { background: currentBackground.value || props.background };
});
// 背景图片层样式(只在图片模式下使用)
const backgroundImageStyle = computed(() => {
const blur = config.value.imageBlur || 0;
const brightness = config.value.imageBrightness || 100;
return {
backgroundImage: `url(${config.value.backgroundImage})`,
filter: `blur(${blur}px) brightness(${brightness}%)`
};
});
const showStickyHeader = ref(false);
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
const isSongChanging = ref(false);
const isFullScreen = ref(false);
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
@@ -355,7 +422,12 @@ const setTextColors = (background: string) => {
watch(
() => props.background,
(newBg) => {
if (config.value.theme === 'default') {
if (config.value.useCustomBackground) {
// 使用自定义背景时,根据自定义背景计算文字颜色
if (customBackgroundStyle.value) {
setTextColors(customBackgroundStyle.value);
}
} else if (config.value.theme === 'default') {
setTextColors(newBg);
} else {
setTextColors(themeMusic[config.value.theme] || props.background);
@@ -364,6 +436,24 @@ watch(
{ immediate: true }
);
// 监听自定义背景配置变化
watch(
() => [config.value.useCustomBackground, customBackgroundStyle.value] as const,
([useCustom, customBg]) => {
if (useCustom && customBg && typeof customBg === 'string') {
setTextColors(customBg);
} else {
// 回退到主题模式
if (config.value.theme === 'default') {
setTextColors(props.background);
} else {
setTextColors(themeMusic[config.value.theme] || props.background);
}
}
},
{ deep: true }
);
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
const getLrcStyle = (index: number) => {
@@ -519,18 +609,45 @@ const handleScroll = () => {
const playerStore = usePlayerStore();
const closeMusicFull = () => {
// 退出全屏模式
if (isFullScreen.value && document.fullscreenElement) {
document.exitFullscreen();
}
isVisible.value = false;
playerStore.setMusicFull(false);
};
// 添加滚动监听
// 全屏切换方法
const toggleFullScreen = async () => {
try {
if (!document.fullscreenElement) {
// 进入全屏
await document.documentElement.requestFullscreen();
isFullScreen.value = true;
} else {
// 退出全屏
await document.exitFullscreen();
isFullScreen.value = false;
}
} catch (error) {
console.error('全屏切换失败:', error);
}
};
// 监听全屏状态变化
const handleFullScreenChange = () => {
isFullScreen.value = !!document.fullscreenElement;
};
// 添加滚动监听和全屏状态监听
onMounted(() => {
if (lrcSider.value?.$el) {
lrcSider.value.$el.addEventListener('scroll', handleScroll);
}
document.addEventListener('fullscreenchange', handleFullScreenChange);
});
// 移除滚动监听
// 移除滚动监听和全屏状态监听
onBeforeUnmount(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value);
@@ -538,6 +655,11 @@ onBeforeUnmount(() => {
if (lrcSider.value?.$el) {
lrcSider.value.$el.removeEventListener('scroll', handleScroll);
}
document.removeEventListener('fullscreenchange', handleFullScreenChange);
// 退出全屏模式
if (document.fullscreenElement) {
document.exitFullscreen();
}
});
// 监听字体大小变化
@@ -621,6 +743,18 @@ defineExpose({
}
}
.background-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
z-index: 0;
}
.drawer-back {
@apply absolute bg-cover bg-center;
z-index: -1;
@@ -635,127 +769,138 @@ defineExpose({
}
#drawer-target {
@apply top-0 left-0 absolute overflow-hidden rounded px-24 flex items-center justify-center w-full h-full py-8;
@apply top-0 left-0 absolute overflow-hidden rounded w-full h-full;
animation-duration: 300ms;
.music-img {
@apply flex-1 flex justify-center mr-16 flex-col items-center;
max-width: 360px;
max-height: 360px;
.content-wrapper {
@apply grid items-center mx-auto h-full;
grid-template-columns: minmax(300px, 40%) 1fr;
gap: 4rem;
max-width: 1600px;
padding: 2rem;
transition: width 0.3s ease;
@media (max-width: 1024px) {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
gap: 2rem;
}
}
.left-side {
@apply flex flex-col items-center justify-center h-full;
transition: all 0.3s ease;
&.only-cover {
@apply mr-0 flex-initial;
max-width: none;
max-height: none;
@apply col-span-2;
.img-container {
@apply w-[50vh] h-[50vh] mb-8;
@apply w-[60vh] aspect-square;
}
.music-info {
@apply text-center w-[600px];
.music-content-name {
@apply text-4xl mb-4 line-clamp-2;
color: var(--text-color-active);
}
.music-content-singer {
@apply text-xl mb-8 opacity-80;
color: var(--text-color-primary);
}
@apply max-w-[800px];
}
}
.img-container {
@apply relative w-full h-full;
@apply relative w-[45vh] mb-8 aspect-square;
max-width: 100%;
}
.music-info {
@apply w-full mt-4;
@apply w-full text-center max-w-[400px];
.music-content-name {
@apply text-2xl font-bold;
@apply text-3xl font-bold mb-2 line-clamp-2;
color: var(--text-color-active);
}
.music-content-singer {
@apply text-base mt-2 opacity-80;
@apply text-lg opacity-80;
color: var(--text-color-primary);
}
}
}
.music-content {
@apply flex flex-col justify-center items-center relative;
width: 500px;
transition: all 0.3s ease;
.right-side {
@apply flex flex-col justify-center h-full relative overflow-hidden;
&.full-width {
@apply col-span-2;
}
&.center {
@apply w-auto;
.music-lrc {
@apply w-full max-w-3xl mx-auto;
@apply w-full mx-auto text-center;
}
.music-lrc-text {
@apply text-center;
transform-origin: center center;
}
.word-by-word-lyric {
@apply justify-center;
}
}
&.hide {
@apply hidden;
}
}
.music-content-time {
display: none;
@apply flex justify-center items-center;
}
.music-lrc {
@apply w-full h-full bg-transparent;
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 15%,
black 85%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
black 15%,
black 85%,
transparent 100%
);
.music-lrc-container {
padding-top: 30vh;
.music-lrc-text:last-child {
margin-bottom: 200px;
}
}
.music-info-header {
@apply mb-8;
.music-lrc {
background-color: inherit;
width: 500px;
height: 550px;
position: relative;
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-name {
@apply text-4xl font-bold mb-2 line-clamp-2;
color: var(--text-color-active);
}
.music-info-header {
@apply mb-8;
.music-info-name {
@apply text-4xl font-bold mb-2 line-clamp-2;
color: var(--text-color-active);
}
.music-info-singer {
@apply text-base;
color: var(--text-color-primary);
.music-info-singer {
@apply text-xl opacity-80;
color: var(--text-color-primary);
}
}
}
&-text {
@apply text-2xl cursor-pointer font-bold px-2 py-4;
.music-lrc-container {
padding: 50vh 0;
min-height: 100%;
}
.music-lrc-text {
@apply text-2xl cursor-pointer font-bold px-4 py-3;
font-family: var(--current-font-family);
transition: all 0.3s ease;
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;
opacity: 0.6;
transform-origin: left center;
&.now-text {
opacity: 1;
transform: scale(1.05);
}
&.no-scroll-tip {
@apply text-base opacity-60 cursor-default py-2;
@@ -819,7 +964,11 @@ defineExpose({
.mobile {
#drawer-target {
@apply flex-col p-4 pt-8 justify-start;
@apply p-4 pt-8;
.content-wrapper {
@apply flex-col justify-start p-0;
}
.music-img {
display: none;
@@ -856,21 +1005,13 @@ defineExpose({
}
// 添加全局字体样式
// 字体设置已移至上方或不再需要单独的 drawer-target
:root {
--current-font-family:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
}
#drawer-target {
@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;
.music-lrc-text {
font-family: var(--current-font-family);
}
}
.close-btn {
opacity: 0.3;
transition: opacity 0.3s ease;
@@ -880,20 +1021,19 @@ defineExpose({
}
}
.control-buttons-container {
@apply flex justify-between items-start z-[9999];
.control-left,
.control-right {
&.pure-mode {
@apply pointer-events-auto; /* 容器需要能接收hover事件 */
@apply pointer-events-auto;
.control-btn {
@apply opacity-0 transition-all duration-300;
pointer-events: none; /* 按钮隐藏时不接收事件 */
pointer-events: none;
}
&:hover .control-btn {
@apply opacity-100;
pointer-events: auto; /* hover时按钮可以点击 */
pointer-events: auto;
}
}
@@ -902,6 +1042,10 @@ defineExpose({
}
}
.control-right {
@apply flex items-center gap-2;
}
.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);
@@ -927,4 +1071,10 @@ defineExpose({
pointer-events: auto !important;
}
}
/* 移除 Popover padding */
:deep(.n-popover) {
padding: 0 !important;
background-color: transparent !important;
}
</style>

View File

@@ -11,10 +11,23 @@ export interface LyricConfig {
pureModeEnabled: boolean;
hideMiniPlayBar: boolean;
hideLyrics: boolean;
contentWidth: number; // 内容区域宽度百分比
// 移动端配置
mobileLayout: 'default' | 'ios' | 'android';
mobileCoverStyle: 'record' | 'square' | 'full';
mobileShowLyricLines: number;
// 背景自定义功能
useCustomBackground: boolean; // 是否使用自定义背景
backgroundMode: 'solid' | 'gradient' | 'image' | 'css'; // 背景模式
solidColor: string; // 纯色背景颜色值
gradientColors: {
colors: string[]; // 渐变颜色数组
direction: string; // 渐变方向
};
backgroundImage?: string; // 图片背景 (Base64 或 URL)
imageBlur: number; // 图片模糊度 (0-20px)
imageBrightness: number; // 图片明暗度 (0-200%, 100为正常)
customCss?: string; // 自定义 CSS 样式
}
export const DEFAULT_LYRIC_CONFIG: LyricConfig = {
@@ -29,12 +42,25 @@ export const DEFAULT_LYRIC_CONFIG: LyricConfig = {
hideMiniPlayBar: false,
pureModeEnabled: false,
hideLyrics: false,
contentWidth: 100, // 默认100%宽度
// 移动端默认配置
mobileLayout: 'ios',
mobileCoverStyle: 'full',
mobileShowLyricLines: 3,
// 翻译引擎: 'none' or 'opencc'
translationEngine: 'none'
translationEngine: 'none',
// 背景自定义功能默认值
useCustomBackground: false,
backgroundMode: 'solid',
solidColor: '#1a1a1a',
gradientColors: {
colors: ['#1a1a1a', '#000000'],
direction: 'to bottom'
},
backgroundImage: undefined,
imageBlur: 0,
imageBrightness: 100,
customCss: undefined
};
export interface ILyric {