mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-25 07:33:49 +08:00
feat: 新增动态侧边栏配置功能,优化主题色与侧边栏样式
新增SidebarEntry类型定义与侧边栏配置加载逻辑,支持从服务器拉取sidebar-config.json动态配置侧边栏入口 替换原分类侧边栏为可配置样式,新增CategoryBar分类选择组件,更新品牌色为苹果风格蓝色 重构侧边栏状态管理,拆分activeTab与选中分类逻辑,新增侧边栏入口计数统计 添加SIDEBAR_CONFIG.md文档说明配置格式与使用方法,更新测试用例与组件props
This commit is contained in:
@@ -21,16 +21,13 @@
|
||||
>
|
||||
{{ app.name || "" }}
|
||||
</div>
|
||||
<!-- 来源标识 -->
|
||||
<div class="flex shrink-0 gap-1">
|
||||
<!-- 合并标识:两个来源都有时显示 -->
|
||||
<span
|
||||
v-if="showMergedBadge"
|
||||
class="rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
>
|
||||
SPARK/APM
|
||||
</span>
|
||||
<!-- 单独标识 -->
|
||||
<template v-else>
|
||||
<span
|
||||
v-if="showSparkBadge"
|
||||
|
||||
@@ -51,13 +51,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeCategory !== 'home'"
|
||||
class="text-sm text-slate-500 dark:text-slate-400"
|
||||
id="currentCount"
|
||||
>
|
||||
<!-- 共 {{ appsCount }} 个应用 -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,7 +59,7 @@ import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
searchQuery: string;
|
||||
activeCategory: string;
|
||||
activeTab: string;
|
||||
appsCount: number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -30,59 +30,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StoreModeSwitcher />
|
||||
|
||||
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted px-2 py-1">
|
||||
<div class="flex-1 space-y-1 overflow-y-auto scrollbar-muted px-1 py-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="
|
||||
activeCategory === 'home'
|
||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||
: ''
|
||||
"
|
||||
@click="selectCategory('home')"
|
||||
class="sidebar-tab"
|
||||
:class="{ 'sidebar-tab-active': activeTab === 'home' }"
|
||||
@click="selectTab('home')"
|
||||
>
|
||||
<span>主页</span>
|
||||
<span class="sidebar-tab-icon"><i class="fas fa-star"></i></span>
|
||||
<span class="sidebar-tab-label">首页推荐</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="
|
||||
activeCategory === 'all'
|
||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||
: ''
|
||||
"
|
||||
@click="selectCategory('all')"
|
||||
class="sidebar-tab"
|
||||
:class="{ 'sidebar-tab-active': activeTab === 'all' }"
|
||||
@click="selectTab('all')"
|
||||
>
|
||||
<span>全部应用</span>
|
||||
<span class="sidebar-tab-icon"><i class="fas fa-th-large"></i></span>
|
||||
<span class="sidebar-tab-label">全部应用</span>
|
||||
<span
|
||||
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||
>{{ categoryCounts.all || 0 }}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="sidebarEntries.length > 0"
|
||||
class="my-3 border-t border-slate-100 dark:border-slate-800"
|
||||
></div>
|
||||
|
||||
<button
|
||||
v-for="(category, key) in categories"
|
||||
:key="key"
|
||||
v-for="entry in sidebarEntries"
|
||||
:key="entry.id"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="
|
||||
activeCategory === key
|
||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||
: ''
|
||||
"
|
||||
@click="selectCategory(key)"
|
||||
class="sidebar-tab"
|
||||
:class="{ 'sidebar-tab-active': activeTab === entry.id }"
|
||||
@click="selectTab(entry.id)"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>
|
||||
<div class="text-left">{{ category.zh }}</div>
|
||||
</span>
|
||||
<span class="sidebar-tab-icon">
|
||||
<i v-if="entry.icon" :class="entry.icon"></i>
|
||||
<i v-else class="fas fa-folder"></i>
|
||||
</span>
|
||||
<span class="sidebar-tab-label">{{ entry.name }}</span>
|
||||
<span
|
||||
v-if="entryCounts[entry.id]"
|
||||
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||
>{{ categoryCounts[key] || 0 }}</span
|
||||
>{{ entryCounts[entry.id] }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,20 +85,20 @@
|
||||
<button
|
||||
v-if="canManageApps"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
class="sidebar-tab"
|
||||
@click="$emit('list')"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
<span>应用管理</span>
|
||||
<span class="sidebar-tab-icon"><i class="fas fa-download"></i></span>
|
||||
<span class="sidebar-tab-label">应用管理</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canOpenUpdateCenter"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
class="sidebar-tab"
|
||||
@click="$emit('update')"
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
<span>软件更新</span>
|
||||
<span class="sidebar-tab-icon"><i class="fas fa-sync-alt"></i></span>
|
||||
<span class="sidebar-tab-label">软件更新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,21 +108,22 @@
|
||||
import { computed } from "vue";
|
||||
import ThemeToggle from "./ThemeToggle.vue";
|
||||
import amberLogo from "../assets/imgs/spark-store.svg";
|
||||
import type { SidebarEntry } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
categories: Record<string, any>;
|
||||
activeCategory: string;
|
||||
activeTab: string;
|
||||
categoryCounts: Record<string, number>;
|
||||
themeMode: "light" | "dark" | "auto";
|
||||
sparkAvailable: boolean;
|
||||
apmAvailable: boolean;
|
||||
storeFilter: "spark" | "apm" | "both";
|
||||
sidebarEntries: SidebarEntry[];
|
||||
entryCounts: Record<string, number>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "toggle-theme"): void;
|
||||
(e: "select-category", category: string): void;
|
||||
(e: "select-tab", tab: string): void;
|
||||
(e: "close"): void;
|
||||
(e: "list"): void;
|
||||
(e: "update"): void;
|
||||
@@ -147,7 +142,62 @@ const canManageApps = computed(() => {
|
||||
|
||||
const canOpenUpdateCenter = canManageApps;
|
||||
|
||||
const selectCategory = (category: string) => {
|
||||
emit("select-category", category);
|
||||
const selectTab = (tab: string) => {
|
||||
emit("select-tab", tab);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-tab {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-tab:hover {
|
||||
background: rgba(0, 113, 227, 0.06);
|
||||
color: #0071e3;
|
||||
}
|
||||
|
||||
.dark .sidebar-tab:hover {
|
||||
background: rgba(64, 156, 255, 0.1);
|
||||
color: #409cff;
|
||||
}
|
||||
|
||||
.sidebar-tab-active {
|
||||
background: rgba(0, 113, 227, 0.1);
|
||||
color: #0066cc;
|
||||
border-color: rgba(0, 113, 227, 0.2);
|
||||
}
|
||||
|
||||
.dark .sidebar-tab-active {
|
||||
background: rgba(64, 156, 255, 0.15);
|
||||
color: #409cff;
|
||||
border-color: rgba(64, 156, 255, 0.25);
|
||||
}
|
||||
|
||||
.sidebar-tab-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-tab-label {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="category-bar-wrapper">
|
||||
<div class="category-bar">
|
||||
<button
|
||||
type="button"
|
||||
class="category-pill"
|
||||
:class="{ 'category-pill-active': selectedCategory === 'all' }"
|
||||
@click="selectCategory('all')"
|
||||
>
|
||||
<span>全部</span>
|
||||
<span v-if="totalCount > 0" class="category-pill-count">{{ totalCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-for="(category, key) in categories"
|
||||
:key="key"
|
||||
type="button"
|
||||
class="category-pill"
|
||||
:class="{ 'category-pill-active': selectedCategory === key }"
|
||||
@click="selectCategory(key)"
|
||||
>
|
||||
<span>{{ category.zh }}</span>
|
||||
<span v-if="categoryCounts[key]" class="category-pill-count">{{ categoryCounts[key] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
categories: Record<string, any>;
|
||||
selectedCategory: string;
|
||||
categoryCounts: Record<string, number>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "select-category", category: string): void;
|
||||
}>();
|
||||
|
||||
const totalCount = computed(() => {
|
||||
let total = 0;
|
||||
Object.values(props.categoryCounts).forEach((v) => {
|
||||
if (typeof v === "number") total += v;
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
const selectCategory = (category: string) => {
|
||||
emit("select-category", category);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.category-bar-wrapper {
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dark .category-bar-wrapper {
|
||||
border-color: rgba(30, 41, 59, 0.7);
|
||||
}
|
||||
|
||||
.category-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.category-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
white-space: nowrap;
|
||||
padding: 0.375rem 0.875rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.category-pill:hover {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.dark .category-pill {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .category-pill:hover {
|
||||
background: #334155;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.category-pill-active {
|
||||
background: #0071e3;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.category-pill-active:hover {
|
||||
background: #0066cc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .category-pill-active {
|
||||
background: #409cff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .category-pill-active:hover {
|
||||
background: #0071e3;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.category-pill-count {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
@@ -72,7 +72,6 @@
|
||||
class="group block overflow-hidden rounded-xl transition-transform duration-300 hover:scale-[1.02]"
|
||||
:title="link.more as string"
|
||||
>
|
||||
<!-- 图片区域 - 850:400 比例 -->
|
||||
<div
|
||||
class="relative w-full aspect-[850/400] overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800"
|
||||
>
|
||||
@@ -88,7 +87,6 @@
|
||||
imageLoaded[link.url + link.name],
|
||||
}"
|
||||
/>
|
||||
<!-- 图片加载占位符 -->
|
||||
<div
|
||||
v-if="!imageLoaded[link.url + link.name]"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
@@ -98,7 +96,6 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文字信息区域 -->
|
||||
<div class="mt-3 px-1">
|
||||
<div
|
||||
class="text-base font-semibold text-slate-900 dark:text-white group-hover:text-brand dark:group-hover:text-brand transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user