Compare commits

...

2 Commits

Author SHA1 Message Date
sqj
29dfa45791 updateVersion 2025-08-28 21:36:16 +08:00
sqj
c2fcb25686 feat:优化部分ui设计,考虑目前Ai悬浮球功能不完善,新增Ai悬浮球全局隐藏功能 2025-08-28 21:34:58 +08:00
11 changed files with 328 additions and 289 deletions

View File

@@ -1,11 +1,11 @@
{ {
"name": "ceru-music", "name": "ceru-music",
"version": "1.1.6", "version": "1.1.7",
"description": "一款简洁优雅的音乐播放器", "description": "一款简洁优雅的音乐播放器",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "sqj,wldss,star", "author": "sqj,wldss,star",
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": "https://electron-vite.org", "homepage": "https://ceru.docs.shiqianjiang.cn",
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint --cache . --fix", "lint": "eslint --cache . --fix",
@@ -97,4 +97,4 @@
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.3" "vue-tsc": "^3.0.3"
} }
} }

View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default'] FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default'] FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default'] GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
@@ -20,9 +21,26 @@ declare module 'vue' {
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default'] SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default'] ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default'] SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
TAlert: typeof import('tdesign-vue-next')['Alert']
TAside: typeof import('tdesign-vue-next')['Aside']
TBadge: typeof import('tdesign-vue-next')['Badge']
TButton: typeof import('tdesign-vue-next')['Button']
TCard: typeof import('tdesign-vue-next')['Card']
TContent: typeof import('tdesign-vue-next')['Content']
TDialog: typeof import('tdesign-vue-next')['Dialog']
ThemeDemo: typeof import('./src/components/ThemeDemo.vue')['default'] ThemeDemo: typeof import('./src/components/ThemeDemo.vue')['default']
ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default'] ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default']
TIcon: typeof import('tdesign-vue-next')['Icon']
TImage: typeof import('tdesign-vue-next')['Image']
TInput: typeof import('tdesign-vue-next')['Input']
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default'] TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
TLayout: typeof import('tdesign-vue-next')['Layout']
TLoading: typeof import('tdesign-vue-next')['Loading']
TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
TSlider: typeof import('tdesign-vue-next')['Slider']
TSwitch: typeof import('tdesign-vue-next')['Switch']
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default'] UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default'] UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default']
UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default'] UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default']

View File

@@ -5,9 +5,12 @@ import DOMPurify from 'dompurify'
import { Loading as TLoading } from 'tdesign-vue-next' import { Loading as TLoading } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail' import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useSettingsStore } from '@renderer/store/Settings'
const userStore = LocalUserDetailStore() const userStore = LocalUserDetailStore()
const settingsStore = useSettingsStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const { settings } = storeToRefs(settingsStore)
const ball = ref<HTMLElement | null>(null) const ball = ref<HTMLElement | null>(null)
const ballClass = ref('hidden-right') // 默认半隐藏 const ballClass = ref('hidden-right') // 默认半隐藏
@@ -30,7 +33,7 @@ const windowSize = ref({ width: 0, height: 0 }) // 窗口尺寸
// 显示悬浮球 // 显示悬浮球
// 悬浮球可见性控制 // 悬浮球可见性控制
const isFloatBallVisible = ref(true) const isFloatBallVisible = ref(settings.value.showFloatBall !== false) // 默认显示除非明确设置为false
const isHovering = ref(false) const isHovering = ref(false)
const showBall = () => { const showBall = () => {
@@ -42,6 +45,7 @@ const showBall = () => {
const closeBall = (e: MouseEvent) => { const closeBall = (e: MouseEvent) => {
e.stopPropagation() // 阻止事件冒泡 e.stopPropagation() // 阻止事件冒泡
isFloatBallVisible.value = false isFloatBallVisible.value = false
settingsStore.updateSettings({ showFloatBall: false })
} }
// 鼠标进入悬浮球 // 鼠标进入悬浮球
@@ -60,7 +64,7 @@ const handleMouseLeave = () => {
const startAutoHide = () => { const startAutoHide = () => {
clearTimer() clearTimer()
timer = window.setTimeout(() => { timer = window.setTimeout(() => {
ballClass.value = 'hidden-right' ballClass.value = isOnLeft.value ? 'hidden-left' : 'hidden-right'
}, 3000) // 3 秒没操作缩回去 }, 3000) // 3 秒没操作缩回去
} }
@@ -119,8 +123,8 @@ const handleMouseUp = () => {
document.removeEventListener('mouseup', handleMouseUp) document.removeEventListener('mouseup', handleMouseUp)
if (hasDragged.value) { if (hasDragged.value) {
// 自动吸边逻辑考虑外层容器尺寸120px // 自动吸边逻辑
const centerX = ballPosition.value.x + 60 // 外层容器中心点 const centerX = ballPosition.value.x + 60 // 悬浮球中心点
const screenCenter = windowSize.value.width / 2 const screenCenter = windowSize.value.width / 2
if (centerX < screenCenter) { if (centerX < screenCenter) {
@@ -134,8 +138,9 @@ const handleMouseUp = () => {
isOnLeft.value = false isOnLeft.value = false
ballClass.value = 'hidden-right' ballClass.value = 'hidden-right'
} }
// 保存位置到本地存储
// 重新开启自动隐藏 saveBallPosition()
clearTimer()
startAutoHide() startAutoHide()
} }
} }
@@ -316,6 +321,15 @@ watch(
{ immediate: false } { immediate: false }
) )
// 监听设置变化,更新悬浮球可见性
watch(
() => settings.value.showFloatBall,
(newValue) => {
isFloatBallVisible.value = newValue
},
{ immediate: true }
)
// 更新窗口尺寸 // 更新窗口尺寸
const updateWindowSize = () => { const updateWindowSize = () => {
windowSize.value = { windowSize.value = {
@@ -324,8 +338,39 @@ const updateWindowSize = () => {
} }
} }
// 初始化悬浮球位置 // 保存悬浮球位置到本地存储
const initBallPosition = () => { const saveBallPosition = () => {
const positionData = {
x: ballPosition.value.x,
y: ballPosition.value.y,
isOnLeft: isOnLeft.value
}
localStorage.setItem('floatBallPosition', JSON.stringify(positionData))
}
// 从本地存储加载悬浮球位置
const loadBallPosition = () => {
try {
const savedPosition = localStorage.getItem('floatBallPosition')
if (savedPosition) {
const positionData = JSON.parse(savedPosition)
ballPosition.value = {
x: positionData.x,
y: positionData.y
}
isOnLeft.value = positionData.isOnLeft
} else {
// 如果没有保存过位置,使用默认位置
setDefaultPosition()
}
} catch (error) {
console.error('加载悬浮球位置失败:', error)
setDefaultPosition()
}
}
// 设置默认位置
const setDefaultPosition = () => {
updateWindowSize() updateWindowSize()
ballPosition.value = { ballPosition.value = {
x: windowSize.value.width - 126, // 考虑外层容器尺寸120px x: windowSize.value.width - 126, // 考虑外层容器尺寸120px
@@ -334,10 +379,20 @@ const initBallPosition = () => {
isOnLeft.value = false isOnLeft.value = false
} }
// 初始化悬浮球位置
const initBallPosition = () => {
updateWindowSize()
loadBallPosition()
}
// 定义 handleResize 函数 // 定义 handleResize 函数
const handleResize = () => { const handleResize = () => {
updateWindowSize() updateWindowSize()
initBallPosition() // 保证悬浮球不超出边界
const maxX = windowSize.value.width - 120
const maxY = windowSize.value.height - 196
ballPosition.value.x = Math.max(0, Math.min(ballPosition.value.x, maxX))
ballPosition.value.y = Math.max(0, Math.min(ballPosition.value.y, maxY))
} }
onMounted(() => { onMounted(() => {
@@ -351,6 +406,7 @@ onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp) document.removeEventListener('mouseup', handleMouseUp)
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
saveBallPosition() // 保存位置
}) })
</script> </script>

View File

@@ -530,6 +530,7 @@ watch(
// 全屏展示相关 // 全屏展示相关
const toggleFullPlay = () => { const toggleFullPlay = () => {
if (!songInfo.value.songmid) return
showFullPlay.value = !showFullPlay.value showFullPlay.value = !showFullPlay.value
} }
@@ -670,22 +671,24 @@ const handleProgressDragStart = (event: MouseEvent) => {
const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number }>({ const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number }>({
songmid: null, songmid: null,
hash: '', hash: '',
singer: 'CeruMusic', name: '欢迎使用CeruMusic 🎉',
name: '未知歌曲', singer: '可以配置音源插件来播放你的歌曲',
albumName: '', albumName: '',
albumId: 0, albumId: 0,
source: '', source: '',
interval: '00:00', interval: '00:00',
img: 'https://oss.shiqianjiang.cn//storage/default/20250723/mmexport1744732a2f8406e483442888d29521de63ca4f98bc085a2.jpeg', img: '',
lrc: null, lrc: null,
types: [], types: [],
_types: {}, _types: {},
typeUrl: {} typeUrl: {}
}) })
const maincolor = ref('rgba(0, 0, 0, 1)') const maincolor = ref('var(--td-brand-color-5)')
const startmaincolor = ref('rgba(0, 0, 0, 1)') const startmaincolor = ref('rgba(0, 0, 0, 1)')
const contrastTextColor = ref('rgba(0, 0, 0, .8)') const contrastTextColor = ref('rgba(0, 0, 0, .8)')
const hoverColor = ref('rgba(0,0,0,1)') const hoverColor = ref('var(--td-brand-color-5)')
const playbg = ref('var(--td-brand-color-2)')
const playbghover = ref('var(--td-brand-color-3)')
async function setColor() { async function setColor() {
console.log('主题色刷新') console.log('主题色刷新')
const color = await extractDominantColor(songInfo.value.img) const color = await extractDominantColor(songInfo.value.img)
@@ -694,8 +697,18 @@ async function setColor() {
startmaincolor.value = `rgba(${color.r},${color.g},${color.b},.2)` startmaincolor.value = `rgba(${color.r},${color.g},${color.b},.2)`
contrastTextColor.value = await getBestContrastTextColorWithOpacity(songInfo.value.img, 0.6) contrastTextColor.value = await getBestContrastTextColorWithOpacity(songInfo.value.img, 0.6)
hoverColor.value = await getBestContrastTextColorWithOpacity(songInfo.value.img, 1) hoverColor.value = await getBestContrastTextColorWithOpacity(songInfo.value.img, 1)
playbg.value = 'rgba(255,255,255,0.2)'
playbghover.value = 'rgba(255,255,255,0.33)'
} }
watch(songInfo, setColor, { deep: true, immediate: true }) watch(
songInfo,
async (newVal) => {
if (newVal.img) {
await setColor()
}
},
{ deep: true, immediate: true }
)
// onMounted(setColor) // onMounted(setColor)
</script> </script>
@@ -718,7 +731,7 @@ watch(songInfo, setColor, { deep: true, immediate: true })
<div class="player-content"> <div class="player-content">
<!-- 左侧封面和歌曲信息 --> <!-- 左侧封面和歌曲信息 -->
<div class="left-section"> <div class="left-section">
<div class="album-cover"> <div class="album-cover" v-show="songInfo.img">
<img :src="songInfo.img" alt="专辑封面" /> <img :src="songInfo.img" alt="专辑封面" />
</div> </div>
@@ -793,14 +806,16 @@ watch(songInfo, setColor, { deep: true, immediate: true })
<!-- 播放列表按钮 --> <!-- 播放列表按钮 -->
<t-tooltip content="播放列表"> <t-tooltip content="播放列表">
<t-button <t-badge :count="list.length" :maxCount="99" color="#aaa">
class="control-btn" <t-button
shape="circle" class="control-btn"
variant="text" shape="circle"
@click.stop="togglePlaylist" variant="text"
> @click.stop="togglePlaylist"
<liebiao style="width: 1.5em; height: 1.5em" /> >
</t-button> <liebiao style="width: 1.5em; height: 1.5em" />
</t-button>
</t-badge>
</t-tooltip> </t-tooltip>
</div> </div>
</div> </div>
@@ -999,7 +1014,7 @@ watch(songInfo, setColor, { deep: true, immediate: true })
.song-name { .song-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 700;
color: v-bind(hoverColor); color: v-bind(hoverColor);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -1044,7 +1059,7 @@ watch(songInfo, setColor, { deep: true, immediate: true })
} }
&.play-btn { &.play-btn {
background-color: #ffffff27; background-color: v-bind(playbg);
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
border-radius: 50%; border-radius: 50%;
@@ -1061,7 +1076,7 @@ watch(songInfo, setColor, { deep: true, immediate: true })
} }
&:hover { &:hover {
background-color: #ffffff62; background-color: v-bind(playbghover);
color: v-bind(contrastTextColor); color: v-bind(contrastTextColor);
} }
} }

View File

@@ -0,0 +1,81 @@
<template>
<div class="float-ball-settings">
<t-card hover-shadow title="AI悬浮球设置">
<div class="card-body">
<div class="setting-item">
<span class="setting-label">显示AI悬浮球</span>
<t-switch v-model="showFloatBall" @change="handleFloatBallToggle" />
</div>
<div class="setting-description">
<p>开启后AI悬浮球将显示在应用界面上您可以随时与AI助手交流</p>
<p>关闭后AI悬浮球将被隐藏您可以随时在此处重新开启</p>
</div>
</div>
</t-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useSettingsStore } from '@renderer/store/Settings'
import { storeToRefs } from 'pinia'
const settingsStore = useSettingsStore()
const { settings } = storeToRefs(settingsStore)
// 悬浮球显示状态
const showFloatBall = ref(settings.value.showFloatBall !== false)
// 处理悬浮球开关切换
const handleFloatBallToggle = (val: boolean) => {
settingsStore.updateSettings({ showFloatBall: val })
}
// 监听设置变化
watch(
() => settings.value.showFloatBall,
(newValue) => {
showFloatBall.value = newValue !== false
}
)
onMounted(() => {
// 确保初始值与存储中的值一致
showFloatBall.value = settings.value.showFloatBall !== false
})
</script>
<style scoped>
.float-ball-settings {
width: 100%;
}
.card-body {
padding: 16px 0;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.setting-label {
font-size: 16px;
font-weight: 500;
color: var(--td-text-color-primary);
}
.setting-description {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--td-border-level-1-color);
}
.setting-description p {
margin: 8px 0;
font-size: 14px;
color: var(--td-text-color-secondary);
}
</style>

View File

@@ -1,195 +0,0 @@
<template>
<div class="theme-demo">
<div class="demo-header">
<h1 class="demo-title">主题切换演示</h1>
<ThemeSelector />
</div>
<div class="demo-content">
<div class="demo-card">
<h3 class="card-title">主要功能</h3>
<p class="card-text">这是一个现代化的主题切换组件支持多种预设主题色</p>
<button class="btn btn-primary">主要按钮</button>
</div>
<div class="demo-card">
<h3 class="card-title">设计特点</h3>
<ul class="feature-list">
<li>简约美观的界面设计</li>
<li>平滑的过渡动画效果</li>
<li>响应式布局适配</li>
<li>深色模式支持</li>
</ul>
</div>
<div class="demo-card">
<h3 class="card-title">交互元素</h3>
<div class="demo-controls">
<input type="text" class="form-control" placeholder="输入框示例" />
<button class="btn btn-secondary">次要按钮</button>
<a href="#" class="demo-link">链接示例</a>
</div>
</div>
</div>
</div>
</template>
<script setup>
import ThemeSelector from './ThemeSelector.vue'
</script>
<style scoped>
.theme-demo {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.demo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid var(--td-component-border);
}
.demo-title {
font-size: 28px;
font-weight: 700;
color: var(--td-text-color-primary);
margin: 0;
}
.demo-content {
display: grid;
gap: 24px;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.demo-card {
background: var(--td-bg-color-container);
border: 1px solid var(--td-component-border);
border-radius: var(--td-radius-large);
padding: 24px;
box-shadow: var(--td-shadow-1);
}
.card-title {
font-size: 20px;
font-weight: 600;
color: var(--td-text-color-primary);
margin: 0 0 12px 0;
}
.card-text {
color: var(--td-text-color-secondary);
line-height: 1.6;
margin-bottom: 16px;
}
.feature-list {
list-style: none;
padding: 0;
margin: 0;
}
.feature-list li {
color: var(--td-text-color-secondary);
padding: 8px 0;
position: relative;
padding-left: 20px;
}
.feature-list li::before {
content: '•';
color: var(--td-brand-color);
font-weight: bold;
position: absolute;
left: 0;
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn {
padding: 10px 16px;
border-radius: var(--td-radius-medium);
border: 1px solid transparent;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background-color: var(--td-brand-color);
border-color: var(--td-brand-color);
color: white;
}
.btn-primary:hover {
background-color: var(--td-brand-color-hover);
border-color: var(--td-brand-color-hover);
}
.btn-secondary {
background-color: var(--td-bg-color-component);
border-color: var(--td-component-border);
color: var(--td-text-color-primary);
}
.btn-secondary:hover {
background-color: var(--td-bg-color-component-hover);
border-color: var(--td-component-border);
}
.form-control {
padding: 10px 12px;
border: 1px solid var(--td-component-border);
border-radius: var(--td-radius-medium);
background-color: var(--td-bg-color-container);
color: var(--td-text-color-primary);
font-size: 14px;
transition: all 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--td-brand-color);
box-shadow: 0 0 0 3px var(--td-brand-color-light);
}
.demo-link {
color: var(--td-brand-color);
text-decoration: none;
font-weight: 500;
}
.demo-link:hover {
color: var(--td-brand-color-hover);
text-decoration: underline;
}
@media (max-width: 640px) {
.demo-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.demo-content {
grid-template-columns: 1fr;
}
.demo-controls {
gap: 16px;
}
}
</style>

View File

@@ -5,20 +5,18 @@
<h3>正在下载更新</h3> <h3>正在下载更新</h3>
<p v-if="downloadState.updateInfo">版本 {{ downloadState.updateInfo.name }}</p> <p v-if="downloadState.updateInfo">版本 {{ downloadState.updateInfo.name }}</p>
</div> </div>
<div class="progress-content"> <div class="progress-content">
<div class="progress-bar-container"> <div class="progress-bar-container">
<div class="progress-bar"> <div class="progress-bar">
<div <div
class="progress-fill" class="progress-fill"
:style="{ width: `${downloadState.progress.percent}%` }" :style="{ width: `${downloadState.progress.percent}%` }"
></div> ></div>
</div> </div>
<div class="progress-text"> <div class="progress-text">{{ Math.round(downloadState.progress.percent) }}%</div>
{{ Math.round(downloadState.progress.percent) }}%
</div>
</div> </div>
<div class="progress-details"> <div class="progress-details">
<div class="download-info"> <div class="download-info">
<span>已下载: {{ formatBytes(downloadState.progress.transferred) }}</span> <span>已下载: {{ formatBytes(downloadState.progress.transferred) }}</span>
@@ -46,46 +44,52 @@ let speedInterval: NodeJS.Timeout | null = null
const calculateSpeed = () => { const calculateSpeed = () => {
const currentTime = Date.now() const currentTime = Date.now()
const currentTransferred = downloadState.progress.transferred const currentTransferred = downloadState.progress.transferred
if (lastTime > 0) { if (lastTime > 0) {
const timeDiff = (currentTime - lastTime) / 1000 // 秒 const timeDiff = (currentTime - lastTime) / 1000 // 秒
const sizeDiff = currentTransferred - lastTransferred // 字节 const sizeDiff = currentTransferred - lastTransferred // 字节
if (timeDiff > 0) { if (timeDiff > 0) {
downloadSpeed.value = sizeDiff / timeDiff downloadSpeed.value = sizeDiff / timeDiff
} }
} }
lastTransferred = currentTransferred lastTransferred = currentTransferred
lastTime = currentTime lastTime = currentTime
} }
// 监听下载进度变化 // 监听下载进度变化
watch(() => downloadState.progress.transferred, () => { watch(
calculateSpeed() () => downloadState.progress.transferred,
}) () => {
calculateSpeed()
}
)
// 开始监听时重置速度计算 // 开始监听时重置速度计算
watch(() => downloadState.isDownloading, (isDownloading) => { watch(
if (isDownloading) { () => downloadState.isDownloading,
lastTransferred = 0 (isDownloading) => {
lastTime = 0 if (isDownloading) {
downloadSpeed.value = 0 lastTransferred = 0
lastTime = 0
// 每秒更新一次速度显示 downloadSpeed.value = 0
speedInterval = setInterval(() => {
if (!downloadState.isDownloading) { // 每秒更新一次速度显示
downloadSpeed.value = 0 speedInterval = setInterval(() => {
if (!downloadState.isDownloading) {
downloadSpeed.value = 0
}
}, 1000)
} else {
if (speedInterval) {
clearInterval(speedInterval)
speedInterval = null
} }
}, 1000) downloadSpeed.value = 0
} else {
if (speedInterval) {
clearInterval(speedInterval)
speedInterval = null
} }
downloadSpeed.value = 0
} }
}) )
onUnmounted(() => { onUnmounted(() => {
if (speedInterval) { if (speedInterval) {
@@ -96,11 +100,11 @@ onUnmounted(() => {
// 格式化字节大小 // 格式化字节大小
const formatBytes = (bytes: number): string => { const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B' if (bytes === 0) return '0 B'
const k = 1024 const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB'] const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
} }
</script> </script>
@@ -168,14 +172,14 @@ const formatBytes = (bytes: number): string => {
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, #0052d9, #266fe8); background: linear-gradient(90deg, var(--td-brand-color-5), var(--td-brand-color-3));
border-radius: 4px; border-radius: 4px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.progress-text { .progress-text {
font-weight: 600; font-weight: 600;
color: #0052d9; color: var(--td-brand-color-6);
min-width: 40px; min-width: 40px;
text-align: right; text-align: right;
} }
@@ -195,7 +199,7 @@ const formatBytes = (bytes: number): string => {
.download-speed { .download-speed {
text-align: center; text-align: center;
color: #0052d9; color: var(--td-brand-color-4);
font-weight: 500; font-weight: 500;
} }
@@ -205,21 +209,21 @@ const formatBytes = (bytes: number): string => {
background: #2d2d2d; background: #2d2d2d;
color: #fff; color: #fff;
} }
.progress-header h3 { .progress-header h3 {
color: #fff; color: #fff;
} }
.progress-header p { .progress-header p {
color: #ccc; color: #ccc;
} }
.progress-bar { .progress-bar {
background-color: #404040; background-color: #404040;
} }
.progress-details { .progress-details {
color: #ccc; color: #ccc;
} }
} }
</style> </style>

View File

@@ -20,7 +20,8 @@ export const LocalUserDetailStore = defineStore('Local', () => {
topBarStyle: false, topBarStyle: false,
mainColor: '#00DAC0', mainColor: '#00DAC0',
volume: 80, volume: 80,
currentTime: 0 currentTime: 0,
selectSources: 'wy'
} }
localStorage.setItem('userInfo', JSON.stringify(userInfo.value)) localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
} }

View File

@@ -0,0 +1,50 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface SettingsState {
showFloatBall: boolean
}
export const useSettingsStore = defineStore('settings', () => {
// 从本地存储加载设置,如果没有则使用默认值
const loadSettings = (): SettingsState => {
try {
const savedSettings = localStorage.getItem('appSettings')
if (savedSettings) {
return JSON.parse(savedSettings)
}
} catch (error) {
console.error('加载设置失败:', error)
}
// 默认设置
return {
showFloatBall: true
}
}
const settings = ref<SettingsState>(loadSettings())
// 保存设置到本地存储
const saveSettings = () => {
localStorage.setItem('appSettings', JSON.stringify(settings.value))
}
// 更新设置
const updateSettings = (newSettings: Partial<SettingsState>) => {
settings.value = { ...settings.value, ...newSettings }
saveSettings()
}
// 切换悬浮球显示状态
const toggleFloatBall = () => {
settings.value.showFloatBall = !settings.value.showFloatBall
saveSettings()
}
return {
settings,
updateSettings,
toggleFloatBall
}
})

View File

@@ -12,8 +12,8 @@ type PlaylistEvents = {
// 创建全局事件总线 // 创建全局事件总线
const emitter = mitt<PlaylistEvents>() const emitter = mitt<PlaylistEvents>()
// 将事件总线挂载到全局 // 将事件总线挂载到全局
;(window as any).musicEmitter = emitter ; (window as any).musicEmitter = emitter
const qualityMap: Record<string, string> = { const qualityMap: Record<string, string> = {
'128k': '标准音质', '128k': '标准音质',
'192k': '高品音质', '192k': '高品音质',
@@ -87,8 +87,12 @@ export async function addToPlaylistAndPlay(
} }
await MessagePlugin.success('已添加到播放列表并开始播放') await MessagePlugin.success('已添加到播放列表并开始播放')
} catch (error) { } catch (error: any) {
console.error('播放失败:', error) console.error('播放失败:', error)
if (error.message) {
await MessagePlugin.error('播放失败了: ' + error.message)
return
}
await MessagePlugin.error('播放失败了,可能还没有版权') await MessagePlugin.error('播放失败了,可能还没有版权')
} }
} }

View File

@@ -53,6 +53,7 @@ const clearAPIKey = (): void => {
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
import MusicCache from '@renderer/components/Settings/MusicCache.vue' import MusicCache from '@renderer/components/Settings/MusicCache.vue'
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
const router = useRouter() const router = useRouter()
const goPlugin = () => { const goPlugin = () => {
router.push('/plugins') router.push('/plugins')
@@ -181,7 +182,7 @@ const getCurrentSourceName = () => {
<div class="settings-container"> <div class="settings-container">
<div class="settings-content"> <div class="settings-content">
<div class="settings-header"> <div class="settings-header">
<h2>标题栏控制组件演示</h2> <h2>设置你的顶部控制栏</h2>
<p>这里展示了两种不同风格的标题栏控制按钮</p> <p>这里展示了两种不同风格的标题栏控制按钮</p>
</div> </div>
@@ -289,6 +290,8 @@ const getCurrentSourceName = () => {
</div> </div>
</div> </div>
</div> </div>
<AIFloatBallSettings></AIFloatBallSettings>
</div> </div>
<!-- 播放列表管理部分 --> <!-- 播放列表管理部分 -->
@@ -430,7 +433,9 @@ const getCurrentSourceName = () => {
<MusicCache></MusicCache> <MusicCache></MusicCache>
</div> </div>
</div> </div>
<div class="demo-section">
<Versions></Versions>
</div>
<div class="demo-section"> <div class="demo-section">
<h3>功能说明</h3> <h3>功能说明</h3>
@@ -508,7 +513,7 @@ const getCurrentSourceName = () => {
} }
.settings-content { .settings-content {
max-width: 800px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
background: #fff; background: #fff;
padding: 2rem; padding: 2rem;
@@ -574,7 +579,7 @@ const getCurrentSourceName = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.75rem 1rem; padding: 14px 1rem;
background: #f6f6f6; background: #f6f6f6;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
@@ -602,7 +607,7 @@ const getCurrentSourceName = () => {
.iconfont { .iconfont {
font-size: 1.25rem; font-size: 1.25rem;
color: #f97316; color: var(--td-brand-color);
margin-top: 0.125rem; margin-top: 0.125rem;
} }
@@ -1015,23 +1020,23 @@ const getCurrentSourceName = () => {
} }
// 响应式设计 // 响应式设计
@media (max-width: 768px) { // @media (max-width: 768px) {
.music-config-container { // .music-config-container {
.source-cards { // .source-cards {
grid-template-columns: 1fr; // grid-template-columns: 1fr;
} // }
.config-status { // .config-status {
grid-template-columns: 1fr; // grid-template-columns: 1fr;
} // }
} // }
.plugin-prompt { // .plugin-prompt {
flex-direction: column; // flex-direction: column;
text-align: center; // text-align: center;
gap: 1.5rem; // gap: 1.5rem;
} // }
} // }
// 动画效果 // 动画效果
@keyframes fadeInUp { @keyframes fadeInUp {