mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-11-11 23:11:03 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bf7bb8046 | ||
|
|
e22f71d255 |
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
spark-store (5.3.2) UNRELEASED; urgency=medium
|
spark-store (5.3.3) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
| 字段 | 来源 | 结论 |
|
| 字段 | 来源 | 结论 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `app.downloadCount` | 现有 `apmRanking` / `sparkRanking`(`App.vue` 1164–1250,异步逐应用拉 `download-times.txt`) | ✅ 直接复用,无需新增请求 |
|
| `app.downloadCount` | 现有 `apmRanking` / `sparkRanking`(由 `applist.json` 的 `DownloadTimes` 直接映射,无额外请求) | ✅ 直接复用,无需新增请求 |
|
||||||
| `app.update` | `App.vue:886` 由 `appJson.Update` 映射,格式 `"2026-01-26 17:34:15"`,同格式可直接 `localeCompare` 倒序 | ✅ 列表已含,可直接排序 |
|
| `app.update` | `App.vue:886` 由 `appJson.Update` 映射,格式 `"2026-01-26 17:34:15"`,同格式可直接 `localeCompare` 倒序 | ✅ 列表已含,可直接排序 |
|
||||||
| `app.contributor` | `App.vue:884` 由 `appJson.Contributor` 映射 | ✅ 列表已含,无需详情请求 |
|
| `app.contributor` | `App.vue:884` 由 `appJson.Contributor` 映射 | ✅ 列表已含,无需详情请求 |
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "5.3.2",
|
"version": "5.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "5.3.2",
|
"version": "5.3.3",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "5.3.2",
|
"version": "5.3.3",
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
"description": "Client for Spark App Store",
|
"description": "Client for Spark App Store",
|
||||||
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { normalizeAppJson } from "@/composables/useCatalog";
|
||||||
|
import { loadRanking } from "@/composables/useRanking";
|
||||||
|
import {
|
||||||
|
apps,
|
||||||
|
apmRanking,
|
||||||
|
rankingLoading,
|
||||||
|
sparkRanking,
|
||||||
|
} from "@/composables/useAppState";
|
||||||
|
import { parseDownloadTimes } from "@/modules/ranking";
|
||||||
|
import type { App, AppJson } from "@/global/typedefinition";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载量来源回归测试:
|
||||||
|
* 下载量取自 app.json / applist.json 的 DownloadTimes 字段,
|
||||||
|
* 不得再逐个应用请求 download-times.txt。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const baseJson = (overrides: Partial<AppJson>): AppJson => ({
|
||||||
|
Name: "App",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Filename: "app_1.0.0_amd64.deb",
|
||||||
|
Torrent_address: "",
|
||||||
|
Pkgname: "app",
|
||||||
|
Author: "",
|
||||||
|
Contributor: "",
|
||||||
|
Website: "",
|
||||||
|
Update: "2026-01-01 00:00:00",
|
||||||
|
Size: "1M",
|
||||||
|
More: "",
|
||||||
|
Tags: "",
|
||||||
|
img_urls: "[]",
|
||||||
|
icons: "",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseDownloadTimes", () => {
|
||||||
|
it("接受数字与数字字符串", () => {
|
||||||
|
expect(parseDownloadTimes(52210)).toBe(52210);
|
||||||
|
expect(parseDownloadTimes("4806")).toBe(4806);
|
||||||
|
expect(parseDownloadTimes(" 42 ")).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("截断小数并拒绝非法值", () => {
|
||||||
|
expect(parseDownloadTimes(12.7)).toBe(12);
|
||||||
|
expect(parseDownloadTimes(undefined)).toBeUndefined();
|
||||||
|
expect(parseDownloadTimes(null)).toBeUndefined();
|
||||||
|
expect(parseDownloadTimes("")).toBeUndefined();
|
||||||
|
expect(parseDownloadTimes("abc")).toBeUndefined();
|
||||||
|
expect(parseDownloadTimes(-1)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeAppJson", () => {
|
||||||
|
it("把 applist.json 的 DownloadTimes 映射为 downloadCount", () => {
|
||||||
|
const app = normalizeAppJson(
|
||||||
|
baseJson({ DownloadTimes: 9001 }),
|
||||||
|
"office",
|
||||||
|
"spark",
|
||||||
|
);
|
||||||
|
expect(app.downloadCount).toBe(9001);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("字段缺失时 downloadCount 为 undefined(不代表 0 次下载)", () => {
|
||||||
|
const app = normalizeAppJson(baseJson({}), "office", "apm");
|
||||||
|
expect(app.downloadCount).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadRanking", () => {
|
||||||
|
const makeApp = (
|
||||||
|
pkgname: string,
|
||||||
|
origin: "spark" | "apm",
|
||||||
|
downloadCount?: number,
|
||||||
|
): App => ({
|
||||||
|
name: pkgname,
|
||||||
|
pkgname,
|
||||||
|
version: "1.0.0",
|
||||||
|
filename: `${pkgname}.deb`,
|
||||||
|
torrent_address: "",
|
||||||
|
author: "",
|
||||||
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
more: "",
|
||||||
|
tags: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: "",
|
||||||
|
category: "tools",
|
||||||
|
origin,
|
||||||
|
currentStatus: "not-installed",
|
||||||
|
downloadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apps.value = [];
|
||||||
|
apmRanking.value = [];
|
||||||
|
sparkRanking.value = [];
|
||||||
|
rankingLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("按 downloadCount 排序分来源发布,且不发网络请求", () => {
|
||||||
|
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
||||||
|
apps.value = [
|
||||||
|
makeApp("apm-low", "apm", 10),
|
||||||
|
makeApp("spark-low", "spark", 5),
|
||||||
|
makeApp("apm-high", "apm", 900),
|
||||||
|
makeApp("spark-high", "spark", 500),
|
||||||
|
makeApp("no-count", "apm"),
|
||||||
|
];
|
||||||
|
|
||||||
|
loadRanking();
|
||||||
|
|
||||||
|
expect(apmRanking.value.map((a) => a.pkgname)).toEqual([
|
||||||
|
"apm-high",
|
||||||
|
"apm-low",
|
||||||
|
"no-count",
|
||||||
|
]);
|
||||||
|
expect(sparkRanking.value.map((a) => a.pkgname)).toEqual([
|
||||||
|
"spark-high",
|
||||||
|
"spark-low",
|
||||||
|
]);
|
||||||
|
expect(rankingLoading.value).toBe(false);
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("应用列表为空时直接返回,不发布空榜", () => {
|
||||||
|
loadRanking();
|
||||||
|
expect(apmRanking.value).toEqual([]);
|
||||||
|
expect(sparkRanking.value).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -580,6 +580,7 @@ import {
|
|||||||
tagPriorityStrategyRef,
|
tagPriorityStrategyRef,
|
||||||
type TagPriorityStrategy,
|
type TagPriorityStrategy,
|
||||||
} from "../global/tagPriority";
|
} from "../global/tagPriority";
|
||||||
|
import { parseDownloadTimes } from "../modules/ranking";
|
||||||
// 评论功能暂时关闭
|
// 评论功能暂时关闭
|
||||||
// import { buildReviewAppKey, buildReviewTags } from "../modules/appIdentity";
|
// import { buildReviewAppKey, buildReviewTags } from "../modules/appIdentity";
|
||||||
import type { App, AppReview, ReviewTags } from "../global/typedefinition";
|
import type { App, AppReview, ReviewTags } from "../global/typedefinition";
|
||||||
@@ -846,28 +847,38 @@ const iconPath = computed(() => {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
const downloadCount = ref<string>("");
|
const downloadCount = ref<string>("");
|
||||||
|
// 请求序号:快速切换应用/来源时丢弃过期响应,避免旧应用的下载量覆盖新应用
|
||||||
|
let downloadCountRequest = 0;
|
||||||
|
|
||||||
// 监听 app 变化,获取新app的下载量
|
// 监听 app 变化,解析新 app 的下载量。
|
||||||
|
// 优先使用目录数据(applist.json)已携带的 downloadCount;
|
||||||
|
// 仅当缺失(例如已安装页的兜底 App)才回退读取 app.json 的 DownloadTimes,
|
||||||
|
// 两者都不再请求 download-times.txt。
|
||||||
watch(
|
watch(
|
||||||
() => displayApp.value,
|
() => displayApp.value,
|
||||||
async (newApp) => {
|
async (newApp) => {
|
||||||
if (newApp) {
|
const seq = ++downloadCountRequest;
|
||||||
downloadCount.value = "";
|
downloadCount.value = "";
|
||||||
try {
|
if (!newApp) return;
|
||||||
const arch = window.apm_store.arch || "amd64";
|
if (typeof newApp.downloadCount === "number") {
|
||||||
const finalArch =
|
downloadCount.value = String(newApp.downloadCount);
|
||||||
newApp.origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
return;
|
||||||
const url = `${APM_STORE_BASE_URL}/${finalArch}/${newApp.category}/${newApp.pkgname}/download-times.txt`;
|
}
|
||||||
const resp = await axios.get(url, { responseType: "text" });
|
try {
|
||||||
if (resp.status === 200) {
|
const arch = window.apm_store.arch || "amd64";
|
||||||
downloadCount.value = String(resp.data).trim();
|
const finalArch =
|
||||||
} else {
|
newApp.origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
downloadCount.value = "N/A";
|
const url = `${APM_STORE_BASE_URL}/${finalArch}/${encodeURIComponent(
|
||||||
throw new Error(`Unexpected response status: ${resp.status}`);
|
newApp.category,
|
||||||
}
|
)}/${encodeURIComponent(newApp.pkgname)}/app.json`;
|
||||||
} catch (e) {
|
const resp = await axios.get(url);
|
||||||
console.error("Failed to fetch download count", e);
|
if (seq !== downloadCountRequest) return;
|
||||||
}
|
const count = parseDownloadTimes(
|
||||||
|
(resp.data as { DownloadTimes?: unknown } | null)?.DownloadTimes,
|
||||||
|
);
|
||||||
|
if (count !== undefined) downloadCount.value = String(count);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to read download count from app.json", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
buildReviewTags,
|
buildReviewTags,
|
||||||
} from "../modules/appIdentity";
|
} from "../modules/appIdentity";
|
||||||
import { loadFavoriteMetadataForDetail } from "./useFavorites";
|
import { loadFavoriteMetadataForDetail } from "./useFavorites";
|
||||||
|
import { parseDownloadTimes } from "../modules/ranking";
|
||||||
import { axiosInstance } from "./useHttp";
|
import { axiosInstance } from "./useHttp";
|
||||||
import type { ReviewTags } from "../global/typedefinition";
|
import type { ReviewTags } from "../global/typedefinition";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
@@ -107,6 +108,8 @@ const fetchAppFromStore = async (
|
|||||||
category,
|
category,
|
||||||
origin,
|
origin,
|
||||||
currentStatus: "not-installed",
|
currentStatus: "not-installed",
|
||||||
|
// app.json 同样携带下载次数,避免详情页再单独请求 download-times.txt
|
||||||
|
downloadCount: parseDownloadTimes(appJson.DownloadTimes),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 组件卸载/详情关闭触发 abort 时静默返回,避免刷 AbortError 日志
|
// 组件卸载/详情关闭触发 abort 时静默返回,避免刷 AbortError 日志
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "./useAppState";
|
} from "./useAppState";
|
||||||
import { axiosInstance, fetchWithRetry, rootAbortController } from "./useHttp";
|
import { axiosInstance, fetchWithRetry, rootAbortController } from "./useHttp";
|
||||||
import { loadPriorityConfig, APM_STORE_BASE_URL } from "../global/storeConfig";
|
import { loadPriorityConfig, APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
|
import { parseDownloadTimes } from "../modules/ranking";
|
||||||
|
|
||||||
export const loadCategories = async () => {
|
export const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -179,6 +180,8 @@ export const normalizeAppJson = (
|
|||||||
category: category,
|
category: category,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
currentStatus: "not-installed" as const,
|
currentStatus: "not-installed" as const,
|
||||||
|
// 下载量由 applist.json 直接携带,无需再逐个请求 download-times.txt
|
||||||
|
downloadCount: parseDownloadTimes(appJson.DownloadTimes),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loadTabCategories = async () => {
|
export const loadTabCategories = async () => {
|
||||||
|
|||||||
+14
-100
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* useRanking —— 首页区域(links + 推荐列表入口)与下载排行榜数据。
|
* useRanking —— 首页区域(links + 推荐列表入口)与下载排行榜数据。
|
||||||
*
|
*
|
||||||
* 从原 App.vue 原样搬移(loadHome / loadRanking / fetchDownloadCount / 缓存逻辑 /
|
* 下载量直接复用目录接口(applist.json)携带的 `DownloadTimes` 字段,
|
||||||
* publishRanking),逻辑零改动。
|
* 由 normalizeAppJson 映射为 `App.downloadCount`,不再逐个应用请求
|
||||||
|
* download-times.txt(数千个应用意味着数千次额外请求)。
|
||||||
*
|
*
|
||||||
* 共享状态(apps / homeLinks / apmRanking / sparkRanking / rankingLoading /
|
* 共享状态(apps / homeLinks / apmRanking / sparkRanking / rankingLoading /
|
||||||
* homeLoading / homeError / storeFilter)来自 useAppState 单例。
|
* homeLoading / homeError / storeFilter)来自 useAppState 单例。
|
||||||
@@ -18,120 +19,33 @@ import {
|
|||||||
rankingLoading,
|
rankingLoading,
|
||||||
storeFilter,
|
storeFilter,
|
||||||
} from "./useAppState";
|
} from "./useAppState";
|
||||||
import { rootAbortController } from "./useHttp";
|
|
||||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
|
|
||||||
const DOWNLOAD_COUNT_CACHE_KEY = "spark-store:download-counts:v1";
|
|
||||||
const DOWNLOAD_COUNT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 小时
|
|
||||||
|
|
||||||
interface CachedCount {
|
|
||||||
count: number;
|
|
||||||
ts: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadCountCache = new Map<string, CachedCount>();
|
|
||||||
|
|
||||||
const cacheKey = (app: App) => `${app.origin}:${app.category}:${app.pkgname}`;
|
|
||||||
|
|
||||||
const loadCacheFromStorage = () => {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(DOWNLOAD_COUNT_CACHE_KEY);
|
|
||||||
if (!raw) return;
|
|
||||||
const data = JSON.parse(raw) as Record<string, CachedCount>;
|
|
||||||
const now = Date.now();
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
|
||||||
if (now - v.ts < DOWNLOAD_COUNT_CACHE_TTL_MS) {
|
|
||||||
downloadCountCache.set(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore corrupted cache
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveCacheToStorage = () => {
|
|
||||||
try {
|
|
||||||
const obj: Record<string, CachedCount> = {};
|
|
||||||
downloadCountCache.forEach((v, k) => {
|
|
||||||
obj[k] = v;
|
|
||||||
});
|
|
||||||
localStorage.setItem(DOWNLOAD_COUNT_CACHE_KEY, JSON.stringify(obj));
|
|
||||||
} catch {
|
|
||||||
// ignore quota errors
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDownloadCount = async (app: App): Promise<number> => {
|
|
||||||
const key = cacheKey(app);
|
|
||||||
const cached = downloadCountCache.get(key);
|
|
||||||
if (cached) return cached.count;
|
|
||||||
const arch = window.apm_store.arch || "amd64";
|
|
||||||
const finalArch = app.origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
|
||||||
try {
|
|
||||||
const resp = await fetch(
|
|
||||||
`${APM_STORE_BASE_URL}/${finalArch}/${app.category}/${app.pkgname}/download-times.txt`,
|
|
||||||
{ signal: rootAbortController.signal },
|
|
||||||
);
|
|
||||||
if (!resp.ok) return 0;
|
|
||||||
const text = (await resp.text()).trim();
|
|
||||||
const n = parseInt(text, 10);
|
|
||||||
const count = Number.isFinite(n) ? n : 0;
|
|
||||||
downloadCountCache.set(key, { count, ts: Date.now() });
|
|
||||||
return count;
|
|
||||||
} catch {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const publishRanking = (results: App[]) => {
|
const publishRanking = (results: App[]) => {
|
||||||
|
// 浅拷贝榜单条目,避免榜单与目录 apps 共享同一对象被下游误改
|
||||||
apmRanking.value = results
|
apmRanking.value = results
|
||||||
.filter((a) => a.origin === "apm")
|
.filter((a) => a.origin === "apm")
|
||||||
.sort((x, y) => (y.downloadCount || 0) - (x.downloadCount || 0))
|
.sort((x, y) => (y.downloadCount || 0) - (x.downloadCount || 0))
|
||||||
.slice(0, 10);
|
.slice(0, 10)
|
||||||
|
.map((a) => ({ ...a }));
|
||||||
sparkRanking.value = results
|
sparkRanking.value = results
|
||||||
.filter((a) => a.origin === "spark")
|
.filter((a) => a.origin === "spark")
|
||||||
.sort((x, y) => (y.downloadCount || 0) - (x.downloadCount || 0))
|
.sort((x, y) => (y.downloadCount || 0) - (x.downloadCount || 0))
|
||||||
.slice(0, 10);
|
.slice(0, 10)
|
||||||
|
.map((a) => ({ ...a }));
|
||||||
};
|
};
|
||||||
|
|
||||||
let rankingGeneration = 0;
|
/**
|
||||||
|
* 下载排行:下载量已在 applist.json 加载阶段随应用写入,此处仅做排序与发布,
|
||||||
const loadRanking = async () => {
|
* 不发起任何网络请求。必须在 loadApps 完成(apps 已填充)之后调用。
|
||||||
|
*/
|
||||||
|
const loadRanking = () => {
|
||||||
if (apps.value.length === 0) return;
|
if (apps.value.length === 0) return;
|
||||||
const gen = ++rankingGeneration;
|
|
||||||
rankingLoading.value = true;
|
rankingLoading.value = true;
|
||||||
const all = apps.value.slice();
|
publishRanking(apps.value);
|
||||||
const CONCURRENCY = 15;
|
|
||||||
const results: App[] = [];
|
|
||||||
for (let i = 0; i < all.length; i += CONCURRENCY) {
|
|
||||||
if (gen !== rankingGeneration) {
|
|
||||||
saveCacheToStorage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const batch = all.slice(i, i + CONCURRENCY);
|
|
||||||
const settled = await Promise.all(
|
|
||||||
batch.map(async (app) => ({
|
|
||||||
...app,
|
|
||||||
downloadCount: await fetchDownloadCount(app),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
results.push(...settled);
|
|
||||||
// 边拉边发:每批完成后立即发布增量排名
|
|
||||||
if (gen === rankingGeneration) publishRanking(results);
|
|
||||||
}
|
|
||||||
if (gen !== rankingGeneration) {
|
|
||||||
saveCacheToStorage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rankingLoading.value = false;
|
rankingLoading.value = false;
|
||||||
saveCacheToStorage();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 启动时即加载本地缓存(无需等待 apps),二次启动首屏即可见缓存排行
|
|
||||||
loadCacheFromStorage();
|
|
||||||
|
|
||||||
// 排行榜在 loadApps 全量完成后(onMounted)触发一次,确保 spark/apm 应用均已就绪
|
|
||||||
|
|
||||||
const loadHome = async () => {
|
const loadHome = async () => {
|
||||||
homeLoading.value = true;
|
homeLoading.value = true;
|
||||||
homeError.value = "";
|
homeError.value = "";
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export interface DownloadItem {
|
|||||||
"More": "VSCode是一款非常牛逼的编辑器",
|
"More": "VSCode是一款非常牛逼的编辑器",
|
||||||
"Tags": "community;ubuntu;deepin;uos;debian",
|
"Tags": "community;ubuntu;deepin;uos;debian",
|
||||||
"img_urls": "[\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_1.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_2.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_3.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_4.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_5.png\"]",
|
"img_urls": "[\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_1.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_2.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_3.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_4.png\",\"https://cdn.d.store.deepinos.org.cn/store/development/code/screen_5.png\"]",
|
||||||
"icons": "https://cdn.d.store.deepinos.org.cn/store/development/code/icon.png"
|
"icons": "https://cdn.d.store.deepinos.org.cn/store/development/code/icon.png",
|
||||||
|
"DownloadTimes": 12345
|
||||||
*/
|
*/
|
||||||
export interface AppJson {
|
export interface AppJson {
|
||||||
// 原始数据
|
// 原始数据
|
||||||
@@ -87,6 +88,8 @@ export interface AppJson {
|
|||||||
Tags: string;
|
Tags: string;
|
||||||
img_urls: string; // 注意:部分 json 里可能是字符串形式的数组
|
img_urls: string; // 注意:部分 json 里可能是字符串形式的数组
|
||||||
icons: string;
|
icons: string;
|
||||||
|
/** 下载次数,由服务器注入到 app.json / applist.json;可能缺失或为字符串数字 */
|
||||||
|
DownloadTimes?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
@@ -117,7 +120,7 @@ export interface App {
|
|||||||
viewingOrigin?: "spark" | "apm"; // Currently viewed origin inside the app modal
|
viewingOrigin?: "spark" | "apm"; // Currently viewed origin inside the app modal
|
||||||
forceViewingOrigin?: boolean; // true 表示 viewingOrigin 为父组件显式指定(如从已安装页按特定来源打开),优先级高于用户标签策略;未设置或 false 时由标签策略决定默认展示
|
forceViewingOrigin?: boolean; // true 表示 viewingOrigin 为父组件显式指定(如从已安装页按特定来源打开),优先级高于用户标签策略;未设置或 false 时由标签策略决定默认展示
|
||||||
origins?: Array<"spark" | "apm">; // 实际安装来源集合(同时以 APM 与 Spark 安装时含两项)
|
origins?: Array<"spark" | "apm">; // 实际安装来源集合(同时以 APM 与 Spark 安装时含两项)
|
||||||
downloadCount?: number; // 下载量(用于下载排行,由 download-times.txt 解析)
|
downloadCount?: number; // 下载量(用于下载排行,取自 applist.json / app.json 的 DownloadTimes 字段)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateAppItem {
|
export interface UpdateAppItem {
|
||||||
|
|||||||
@@ -8,6 +8,21 @@ import type { App } from "../global/typedefinition";
|
|||||||
/** 榜单统一条数(需求确认:10 条) */
|
/** 榜单统一条数(需求确认:10 条) */
|
||||||
export const TOP_N = 10;
|
export const TOP_N = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析服务器在 app.json / applist.json 中注入的 `DownloadTimes` 字段。
|
||||||
|
* 该字段正常为数字,但历史数据/第三方镜像也可能是字符串甚至缺失,
|
||||||
|
* 统一收敛为「非负整数」;无法解析时返回 undefined,交由调用方决定是否回退。
|
||||||
|
*/
|
||||||
|
export function parseDownloadTimes(raw: unknown): number | undefined {
|
||||||
|
const n =
|
||||||
|
typeof raw === "number"
|
||||||
|
? raw
|
||||||
|
: typeof raw === "string" && raw.trim() !== ""
|
||||||
|
? Number(raw.trim())
|
||||||
|
: NaN;
|
||||||
|
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** 更新荣耀榜取「最近更新」应用的窗口大小(用于聚合这部分贡献者) */
|
/** 更新荣耀榜取「最近更新」应用的窗口大小(用于聚合这部分贡献者) */
|
||||||
export const RECENT_UPDATE_WINDOW = 30;
|
export const RECENT_UPDATE_WINDOW = 30;
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,19 @@
|
|||||||
# 下载量统计接口规范
|
# 下载量统计接口规范
|
||||||
|
|
||||||
## 1. 获取下载次数
|
## 0. 客户端读取方式(现行)
|
||||||
|
|
||||||
|
客户端**不再**逐个应用请求 `download-times.txt`(数千个应用意味着数千次请求),
|
||||||
|
改为直接读取目录/详情 JSON 中服务器注入的 `DownloadTimes` 字段:
|
||||||
|
|
||||||
|
- 列表与下载排行:`{SOURCE_URL}/{ARCH}/{CATEGORY}/applist.json` 每项的 `DownloadTimes`
|
||||||
|
- 详情页兜底:`{SOURCE_URL}/{ARCH}/{CATEGORY}/{Package Name}/app.json` 的 `DownloadTimes`
|
||||||
|
|
||||||
|
`DownloadTimes` 为正整数(下载次数),字段缺失时客户端视为「未知」,不展示下载量。
|
||||||
|
下文 `download-times.txt` 接口仍然保留可用,但客户端已不再使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 获取下载次数(旧接口,客户端已不再请求)
|
||||||
|
|
||||||
获取指定应用的下载次数
|
获取指定应用的下载次数
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user