refactor: 基于 upstream/Erotica 重新整合本地功能(来源选择/合并详情/计数/窗口/首页)

将本地在 upstream !408(已安装应用合并+统计)之上的后续改动重新整合到最新 upstream/Erotica:
- 已安装应用:卸载/打开按 APM/Spark 来源选择;计数总数=APM+Spark 包数(双来源计为独立包)
- 查看详情:已安装页复用所有应用页合并详情视图(单来源优先已装类型,双来源自动模式)
- 窗口:恢复阈值改为 1600x900,过大回退默认尺寸并居中,不自动最大化
- 首页推荐:致谢卡片移至其他内容下方,加大标题/卡片/致谢间距
- 隐藏排行榜/荣耀榜侧栏入口(代码保留,v-if=false)
- 新增 scripts/test-build.sh 测试打包脚本(版本末段+1 加 -test)并更新 AGENTS.md

冲突文件(index.ts/App.vue/InstalledAppsModal.vue)均取本地完整实现(为上游版本的超集)。
This commit is contained in:
xiyidaiwa
2026-08-03 11:06:28 +08:00
parent 77b4244f1d
commit b89c001401
25 changed files with 1750 additions and 299 deletions
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="flex h-full min-h-0 flex-col gap-3">
<!-- 两个榜单并排每个榜单内容在容器内滚动 -->
<div class="grid min-h-0 flex-1 gap-6 lg:grid-cols-2">
<!-- 贡献荣耀榜 -->
<section class="flex min-h-0 flex-col">
<h2
class="mb-2 shrink-0 text-sm font-bold text-slate-800 dark:text-slate-200"
>
贡献荣耀榜
</h2>
<div
class="grid grid-rows-1 min-h-0 flex-1 gap-4"
:class="gridCols"
>
<div v-if="showSpark" class="flex min-h-0 flex-col">
<SourceLabel origin="spark" />
<div
class="scrollbar-muted min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1"
>
<RankingContributorRow
v-for="(item, i) in contributeSpark"
:key="item.name"
:rank="i + 1"
:name="item.name"
:count="item.count"
label="上榜应用"
/>
<EmptyState
v-if="contributeSpark.length === 0"
:loading="appsLoading"
/>
</div>
</div>
<div v-if="showApm" class="flex min-h-0 flex-col">
<SourceLabel origin="apm" />
<div
class="scrollbar-muted min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1"
>
<RankingContributorRow
v-for="(item, i) in contributeApm"
:key="item.name"
:rank="i + 1"
:name="item.name"
:count="item.count"
label="上榜应用"
/>
<EmptyState
v-if="contributeApm.length === 0"
:loading="appsLoading"
/>
</div>
</div>
</div>
</section>
<!-- 更新荣耀榜 -->
<section class="flex min-h-0 flex-col">
<h2
class="mb-2 shrink-0 text-sm font-bold text-slate-800 dark:text-slate-200"
>
更新荣耀榜
</h2>
<div
class="grid grid-rows-1 min-h-0 flex-1 gap-4"
:class="gridCols"
>
<div v-if="showSpark" class="flex min-h-0 flex-col">
<SourceLabel origin="spark" />
<div
class="scrollbar-muted min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1"
>
<RankingContributorRow
v-for="(item, i) in updateContributeSpark"
:key="item.name"
:rank="i + 1"
:name="item.name"
:count="item.count"
label="近期更新"
/>
<EmptyState
v-if="updateContributeSpark.length === 0"
:loading="appsLoading"
/>
</div>
</div>
<div v-if="showApm" class="flex min-h-0 flex-col">
<SourceLabel origin="apm" />
<div
class="scrollbar-muted min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1"
>
<RankingContributorRow
v-for="(item, i) in updateContributeApm"
:key="item.name"
:rank="i + 1"
:name="item.name"
:count="item.count"
label="近期更新"
/>
<EmptyState
v-if="updateContributeApm.length === 0"
:loading="appsLoading"
/>
</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { App } from "../global/typedefinition";
import { aggregateContributors, topUpdatedContributors } from "../modules/ranking";
import SourceLabel from "./SourceLabel.vue";
import RankingContributorRow from "./RankingContributorRow.vue";
import EmptyState from "./RankingEmptyState.vue";
const props = defineProps<{
apps: App[];
storeFilter?: "spark" | "apm" | "both";
}>();
const TOP = 10;
const showSpark = computed(() => props.storeFilter !== "apm");
const showApm = computed(() => props.storeFilter !== "spark");
const gridCols = computed(() =>
showSpark.value && showApm.value
? "grid-cols-1 sm:grid-cols-2"
: "grid-cols-1",
);
const appsLoading = computed(() => props.apps.length === 0);
const contributeSpark = computed(() =>
aggregateContributors(props.apps, "spark", TOP),
);
const contributeApm = computed(() =>
aggregateContributors(props.apps, "apm", TOP),
);
const updateContributeSpark = computed(() =>
topUpdatedContributors(props.apps, "spark"),
);
const updateContributeApm = computed(() =>
topUpdatedContributors(props.apps, "apm"),
);
</script>