新增应用数据管理功能,优化应用列表和详情页面的加载逻辑

This commit is contained in:
2026-03-08 01:42:44 +07:00
parent a726c3516e
commit e104c5ce43
4 changed files with 115 additions and 16 deletions

View File

@@ -12,9 +12,9 @@
"en-US": "YixinDa is China's leading instant messaging app, providing secure and convenient chat and social services."
},
"category": "tools",
"version": "1.0.0",
"version": "1.1.8",
"buildNumber": "1000",
"releaseDate": "2026-03-06",
"releaseDate": "2026-03-08",
"releaseNotes": {
"zh-CN": [
"初始版本发布",
@@ -30,7 +30,7 @@
]
},
"downloads": {
"android": "https://s3.yxdim.com/__UNI__44929DB__20260306175925.apk",
"android": "https://download.yxdim.com/yxd_1.1.8.apk",
"h5": "https://www.yxdim.com"
},
"size": {

View File

@@ -1,7 +1,73 @@
import type { AppItem, Locale } from '../types/app'
import appsJson from './apps.json'
import { readonly, ref } from 'vue'
export const apps: AppItem[] = appsJson as AppItem[]
import type { AppItem, Locale } from '../types/app'
const APPS_URL = 'https://s3.yxdim.com/apps.json'
const appsState = ref<AppItem[]>([])
const loadingState = ref(false)
const errorState = ref('')
let hasLoaded = false
let pendingRequest: Promise<void> | null = null
function getErrorMessage(error: unknown) {
if (error instanceof Error) {
return error.message
}
return 'Unknown error'
}
export async function fetchApps(force = false) {
if (pendingRequest) {
return pendingRequest
}
if (hasLoaded && !force) {
return
}
loadingState.value = true
errorState.value = ''
pendingRequest = (async () => {
const response = await fetch(APPS_URL, {
method: 'GET',
})
if (!response.ok) {
throw new Error(`Failed to fetch apps: ${response.status}`)
}
const data: unknown = await response.json()
if (!Array.isArray(data)) {
throw new Error('Invalid apps data format')
}
appsState.value = data as AppItem[]
hasLoaded = true
})()
.catch((error: unknown) => {
errorState.value = getErrorMessage(error)
})
.finally(() => {
loadingState.value = false
pendingRequest = null
})
return pendingRequest
}
export function useAppsStore() {
return {
apps: readonly(appsState),
isLoading: readonly(loadingState),
error: readonly(errorState),
fetchApps,
}
}
export const categoryLabel: Record<string, Record<Locale, string>> = {
all: {

View File

@@ -1,19 +1,36 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, onMounted } from "vue";
import { RouterLink, useRoute } from "vue-router";
import { apps, categoryLabel } from "../data/apps";
import { categoryLabel, useAppsStore } from "../data/apps";
import { useLocaleStore } from "../composables/useLocaleStore";
import { formatDate, formatNumber } from "../utils/format";
const route = useRoute();
const { locale, t } = useLocaleStore();
const { apps, isLoading, error, fetchApps } = useAppsStore();
const app = computed(() => apps.find((item) => item.id === route.params.id));
const app = computed(() =>
apps.value.find((item) => item.id === String(route.params.id)),
);
onMounted(() => {
void fetchApps();
});
</script>
<template>
<section v-if="app" class="detail-page">
<section v-if="isLoading" class="not-found">
<h1>{{ t("正在加载应用数据...", "Loading apps...") }}</h1>
</section>
<section v-else-if="error" class="not-found">
<h1>{{ t("加载失败", "Failed to load") }}</h1>
<p>{{ error }}</p>
<RouterLink to="/">{{ t("返回首页", "Back Home") }}</RouterLink>
</section>
<section v-else-if="app" class="detail-page">
<RouterLink class="back-link" to="/">{{
t("返回应用列表", "Back to apps")
}}</RouterLink>

View File

@@ -1,25 +1,26 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { RouterLink } from "vue-router";
import { apps, categoryLabel } from "../data/apps";
import { categoryLabel, useAppsStore } from "../data/apps";
import { useLocaleStore } from "../composables/useLocaleStore";
import { formatDate, formatNumber } from "../utils/format";
const { locale, t } = useLocaleStore();
const { apps, isLoading, error, fetchApps } = useAppsStore();
const selectedCategory = ref("all");
const keyword = ref("");
const categories = computed(() => [
"all",
...new Set(apps.map((item) => item.category)),
...new Set(apps.value.map((item) => item.category)),
]);
const filteredApps = computed(() => {
const searchWord = keyword.value.trim().toLowerCase();
return apps.filter((item) => {
return apps.value.filter((item) => {
const categoryMatched =
selectedCategory.value === "all" ||
item.category === selectedCategory.value;
@@ -32,6 +33,10 @@ const filteredApps = computed(() => {
return categoryMatched && searchMatched;
});
});
onMounted(() => {
void fetchApps();
});
</script>
<template>
@@ -78,8 +83,16 @@ const filteredApps = computed(() => {
</section>
<section class="app-grid">
<article v-if="isLoading" class="empty">
{{ t("正在加载应用数据...", "Loading apps...") }}
</article>
<article v-else-if="error" class="empty">
{{ t("加载失败", "Failed to load") }}: {{ error }}
</article>
<RouterLink
v-for="item in filteredApps"
v-for="item in !isLoading && !error ? filteredApps : []"
:key="item.id"
class="app-card"
:to="`/app/${item.id}`"
@@ -105,7 +118,10 @@ const filteredApps = computed(() => {
</dl>
</RouterLink>
<article v-if="filteredApps.length === 0" class="empty">
<article
v-if="!isLoading && !error && filteredApps.length === 0"
class="empty"
>
{{ t("未找到匹配应用", "No matching apps found") }}
</article>
</section>