新增应用数据管理功能,优化应用列表和详情页面的加载逻辑
This commit is contained in:
@@ -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": {
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user