diff --git a/package.json b/package.json index afc5c20a..80a2cacc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.6.8", + "version": "2.6.9", "private": true, "type": "module", "bin": "dist/service.js", diff --git a/src/pages/dashboard.vue b/src/pages/dashboard.vue index cd822977..5d0a9276 100644 --- a/src/pages/dashboard.vue +++ b/src/pages/dashboard.vue @@ -10,6 +10,7 @@ import { useDynamicButton } from '@/composables/useDynamicButton' import { useI18n } from 'vue-i18n' import { VCardActions } from 'vuetify/components' import { usePWA } from '@/composables/usePWA' +import { getItemColor, initializeItemColors } from '@/utils/colorUtils' // 国际化 const { t } = useI18n() @@ -161,6 +162,18 @@ const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({}) // 弹窗 const dialog = ref(false) +// 为每个项目生成随机颜色 +const itemColors = ref<{ [key: string]: string }>({}) + +// 初始化颜色 +function initializeColors() { + initializeItemColors(dashboardConfigs.value, item => buildPluginDashboardId(item.id, item.key)) + dashboardConfigs.value.forEach(item => { + const itemId = buildPluginDashboardId(item.id, item.key) + itemColors.value[itemId] = getItemColor(itemId) + }) +} + // 使用动态按钮钩子 useDynamicButton({ icon: 'mdi-view-dashboard-edit', @@ -286,6 +299,11 @@ async function getPluginDashboard(id: string, key: string) { dashboardConfigs.value[index] = res } else { dashboardConfigs.value.push(res) + // 为新增的插件仪表板生成颜色 + const pluginDashboardId = buildPluginDashboardId(id, key) + if (!itemColors.value[pluginDashboardId]) { + itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId) + } // 排序 sortDashboardConfigs() } @@ -322,6 +340,7 @@ function dragOrderEnd() { onBeforeMount(async () => { await loadDashboardConfig() + initializeColors() getPluginDashboardMeta() }) @@ -390,6 +409,7 @@ onDeactivated(() => { :class="{ 'enabled': enableConfig[buildPluginDashboardId(item.id, item.key)], }" + :style="{ '--item-color': itemColors[buildPluginDashboardId(item.id, item.key)] }" @click=" enableConfig[buildPluginDashboardId(item.id, item.key)] = !enableConfig[buildPluginDashboardId(item.id, item.key)] @@ -444,8 +464,11 @@ onDeactivated(() => { } .setting-label { + flex: 1; color: rgba(var(--v-theme-on-surface), 0.8); font-size: 0.9rem; + font-weight: 500; + line-height: 1.2; transition: color 0.2s ease; } @@ -462,7 +485,7 @@ onDeactivated(() => { &::before { position: absolute; - background-color: transparent; + background-color: var(--item-color, #4caf50); block-size: 100%; content: ''; inline-size: 4px; @@ -472,16 +495,15 @@ onDeactivated(() => { } &:hover { - border-color: rgba(var(--v-theme-on-surface), 0.15); - background-color: rgba(var(--v-theme-surface-variant), 0.6); + transform: translateY(-2px); } &.enabled { - border-color: rgba(var(--v-theme-primary), 0.5); - background-color: rgba(var(--v-theme-primary), 0.05); + border-color: rgba(var(--v-theme-primary), 0.3); + background-color: rgba(var(--v-theme-primary), 0.1); .setting-label { - color: rgb(var(--v-theme-primary)); + color: rgba(var(--v-theme-primary), 0.9); font-weight: 500; } } @@ -490,9 +512,16 @@ onDeactivated(() => { .setting-item-inner { display: flex; align-items: center; + gap: 8px; } .setting-check { - margin-inline-end: 8px; + flex-shrink: 0; +} + +@media (width <= 600px) { + .settings-grid { + grid-template-columns: repeat(2, 1fr); + } } diff --git a/src/pages/discover.vue b/src/pages/discover.vue index d9a109cd..bdbd268d 100644 --- a/src/pages/discover.vue +++ b/src/pages/discover.vue @@ -10,6 +10,7 @@ import api from '@/api' import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab' +import { getItemColor, initializeItemColors } from '@/utils/colorUtils' const display = useDisplay() @@ -44,6 +45,17 @@ const extraDiscoverSources = ref([]) // 排序对话框 const orderConfigDialog = ref(false) +// 为每个项目生成随机颜色 +const itemColors = ref<{ [key: string]: string }>({}) + +// 初始化颜色 +function initializeColors() { + initializeItemColors(discoverTabs.value, item => item.mediaid_prefix) + discoverTabs.value.forEach(item => { + itemColors.value[item.mediaid_prefix] = getItemColor(item.mediaid_prefix) + }) +} + // 初始化发现标签 function initDiscoverTabs() { const tabs = getDiscoverTabs() @@ -70,6 +82,10 @@ async function loadExtraDiscoverSources() { continue } discoverTabs.value.push(source) + // 为新增的数据源生成颜色 + if (!itemColors.value[source.mediaid_prefix]) { + itemColors.value[source.mediaid_prefix] = getItemColor(source.mediaid_prefix) + } } } catch (error) { console.log(error) @@ -145,6 +161,7 @@ registerHeaderTab({ onBeforeMount(async () => { initDiscoverTabs() + initializeColors() await loadOrderConfig() await loadExtraDiscoverSources() sortSubscribeOrder() @@ -225,9 +242,14 @@ onActivated(async () => { :component-data="{ 'class': 'settings-grid' }" > @@ -269,8 +291,11 @@ onActivated(async () => { } .setting-label { + flex: 1; color: rgba(var(--v-theme-on-surface), 0.8); font-size: 0.9rem; + font-weight: 500; + line-height: 1.2; transition: color 0.2s ease; } @@ -287,8 +312,7 @@ onActivated(async () => { &::before { position: absolute; - background-color: transparent; - background-color: rgb(var(--v-theme-primary)); + background-color: var(--item-color, #4caf50); block-size: 100%; content: ''; inline-size: 4px; @@ -298,16 +322,15 @@ onActivated(async () => { } &:hover { - border-color: rgba(var(--v-theme-on-surface), 0.15); - background-color: rgba(var(--v-theme-surface-variant), 0.6); + transform: translateY(-2px); } &.enabled { - border-color: rgba(var(--v-theme-primary), 0.5); - background-color: rgba(var(--v-theme-primary), 0.05); + border-color: rgba(var(--v-theme-primary), 0.3); + background-color: rgba(var(--v-theme-primary), 0.1); .setting-label { - color: rgb(var(--v-theme-primary)); + color: rgba(var(--v-theme-primary), 0.9); font-weight: 500; } } @@ -316,9 +339,22 @@ onActivated(async () => { .setting-item-inner { display: flex; align-items: center; + gap: 8px; } .setting-check { - margin-inline-end: 8px; + flex-shrink: 0; +} + +.drag-icon { + flex-shrink: 0; + color: rgba(var(--v-theme-on-surface), 0.5); + cursor: move; +} + +@media (width <= 600px) { + .settings-grid { + grid-template-columns: repeat(2, 1fr); + } } diff --git a/src/pages/recommend.vue b/src/pages/recommend.vue index 1cd35495..0916529e 100644 --- a/src/pages/recommend.vue +++ b/src/pages/recommend.vue @@ -5,6 +5,7 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue' import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab' +import { getItemColor, initializeItemColors } from '@/utils/colorUtils' const display = useDisplay() @@ -114,6 +115,17 @@ const enableConfig = ref<{ [key: string]: boolean }>({ ...Object.fromEntries(viewList.map(item => [item.title, true])), }) +// 为每个项目生成随机颜色 +const itemColors = ref<{ [key: string]: string }>({}) + +// 初始化颜色 +function initializeColors() { + initializeItemColors(viewList, item => item.title) + viewList.forEach(item => { + itemColors.value[item.title] = getItemColor(item.title) + }) +} + // 弹窗 const dialog = ref(false) @@ -127,8 +139,8 @@ async function loadExtraRecommendSources() { if (extraRecommendSources.value.length > 0) { extraRecommendSources.value.map(source => { if (!viewList.some(item => item.apipath === source.api_path)) { - const querySeparator = source.api_path.includes('?') ? '&' : '?'; - const linkUrl = `/browse/${source.api_path}${querySeparator}title=${encodeURIComponent(source.name)}`; + const querySeparator = source.api_path.includes('?') ? '&' : '?' + const linkUrl = `/browse/${source.api_path}${querySeparator}title=${encodeURIComponent(source.name)}` viewList.push({ apipath: source.api_path, linkurl: linkUrl, @@ -221,10 +233,17 @@ registerHeaderTab({ onBeforeMount(async () => { await loadConfig() + initializeColors() }) onMounted(async () => { await loadExtraRecommendSources() + // 为新增的数据源也生成颜色 + extraRecommendSources.value.forEach(source => { + if (!itemColors.value[source.name]) { + itemColors.value[source.name] = getItemColor(source.name) + } + }) }) onActivated(async () => { @@ -275,8 +294,8 @@ onActivated(async () => { class="setting-item" :class="{ 'enabled': enableConfig[item.title], - [item.type]: true, }" + :style="{ '--item-color': itemColors[item.title] }" @click="enableConfig[item.title] = !enableConfig[item.title]" >
@@ -394,7 +413,7 @@ onActivated(async () => { &::before { position: absolute; - background-color: transparent; + background-color: var(--item-color, #4caf50); block-size: 100%; content: ''; inline-size: 4px; @@ -403,19 +422,6 @@ onActivated(async () => { transition: background-color 0.3s ease; } - &.电影::before { - background-color: #4caf50; - } // Green - &.电视剧::before { - background-color: #2196f3; - } // Blue - &.动漫::before { - background-color: #ff9800; - } // Orange - &.排行榜::before { - background-color: #9c27b0; - } // Purple - &.enabled { border-color: rgba(var(--v-theme-primary), 0.3); background-color: rgba(var(--v-theme-primary), 0.1); @@ -452,7 +458,7 @@ onActivated(async () => { @media (width <= 600px) { .settings-grid { - grid-template-columns: 1fr; + grid-template-columns: repeat(2, 1fr); } } diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts new file mode 100644 index 00000000..e39f0b6c --- /dev/null +++ b/src/utils/colorUtils.ts @@ -0,0 +1,137 @@ +// 预定义的颜色数组,包含更多丰富的颜色选项 +const COLORS = [ + // 基础颜色 + '#4caf50', // 绿色 + '#2196f3', // 蓝色 + '#ff9800', // 橙色 + '#9c27b0', // 紫色 + '#f44336', // 红色 + '#00bcd4', // 青色 + '#8bc34a', // 浅绿色 + '#ff5722', // 深橙色 + '#3f51b5', // 靛蓝色 + '#009688', // 青绿色 + '#e91e63', // 粉红色 + '#673ab7', // 深紫色 + '#ffc107', // 琥珀色 + '#795548', // 棕色 + '#607d8b', // 蓝灰色 + + // 扩展颜色 + '#ff4081', // 深粉红色 + '#00e676', // 浅绿色 + '#ff6f00', // 深橙色 + '#4fc3f7', // 浅蓝色 + '#ba68c8', // 浅紫色 + '#81c784', // 浅绿色 + '#ffb74d', // 浅橙色 + '#64b5f6', // 浅蓝色 + '#f06292', // 浅粉红色 + '#4db6ac', // 浅青绿色 + '#aed581', // 浅绿色 + '#ffd54f', // 浅黄色 + '#7986cb', // 浅靛蓝色 + '#4dd0e1', // 浅青色 + '#ff8a65', // 浅红色 + '#9575cd', // 浅紫色 + '#4fc3f7', // 天蓝色 + '#ffcc02', // 金黄色 + '#7cb342', // 浅绿色 + '#42a5f5', // 蓝色 + '#ab47bc', // 紫色 + '#26a69a', // 青绿色 + '#66bb6a', // 绿色 + '#ff7043', // 深橙色 + '#29b6f6', // 浅蓝色 + '#7e57c2', // 紫色 + '#26c6da', // 青色 + '#9ccc65', // 浅绿色 + '#ffb300', // 琥珀色 + '#8d6e63', // 棕色 + '#78909c', // 蓝灰色 + '#ef5350', // 红色 + '#ec407a', // 粉红色 + '#ab47bc', // 紫色 + '#42a5f5', // 蓝色 + '#7cb342', // 绿色 + '#ffa726', // 橙色 + '#26c6da', // 青色 + '#d4e157', // 浅绿色 + '#ffca28', // 黄色 + '#9fa8da', // 浅靛蓝色 + '#80cbc4', // 浅青绿色 + '#c5e1a5', // 浅绿色 + '#ffe082', // 浅黄色 + '#b39ddb', // 浅紫色 + '#90caf9', // 浅蓝色 + '#a5d6a7', // 浅绿色 + '#ffcc80', // 浅橙色 + '#b2dfdb', // 浅青绿色 + '#f8bbd9', // 浅粉红色 + '#c8e6c9', // 浅绿色 + '#fff9c4', // 浅黄色 + '#d1c4e9', // 浅紫色 + '#bbdefb', // 浅蓝色 + '#c8e6c9', // 浅绿色 + '#ffecb3', // 浅琥珀色 + '#d7ccc8', // 浅棕色 + '#cfd8dc', // 浅蓝灰色 +] + +// 颜色缓存,确保同一项目总是获得相同颜色 +const colorCache = new Map() + +/** + * 生成随机颜色 + * @returns 随机颜色值 + */ +export function generateRandomColor(): string { + return COLORS[Math.floor(Math.random() * COLORS.length)] +} + +/** + * 为指定项目获取或生成颜色 + * @param itemKey 项目的唯一标识 + * @returns 颜色值 + */ +export function getItemColor(itemKey: string): string { + if (!colorCache.has(itemKey)) { + colorCache.set(itemKey, generateRandomColor()) + } + return colorCache.get(itemKey)! +} + +/** + * 初始化项目颜色 + * @param items 项目数组 + * @param keyExtractor 从项目中提取唯一键的函数 + */ +export function initializeItemColors(items: T[], keyExtractor: (item: T) => string): void { + items.forEach(item => { + const key = keyExtractor(item) + getItemColor(key) // 这会自动缓存颜色 + }) +} + +/** + * 清除颜色缓存 + */ +export function clearColorCache(): void { + colorCache.clear() +} + +/** + * 获取所有预定义颜色 + * @returns 颜色数组 + */ +export function getAllColors(): string[] { + return [...COLORS] +} + +/** + * 获取颜色总数 + * @returns 颜色数量 + */ +export function getColorCount(): number { + return COLORS.length +}