新增应用数据管理功能,优化应用列表和详情页面的加载逻辑
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."
|
"en-US": "YixinDa is China's leading instant messaging app, providing secure and convenient chat and social services."
|
||||||
},
|
},
|
||||||
"category": "tools",
|
"category": "tools",
|
||||||
"version": "1.0.0",
|
"version": "1.1.8",
|
||||||
"buildNumber": "1000",
|
"buildNumber": "1000",
|
||||||
"releaseDate": "2026-03-06",
|
"releaseDate": "2026-03-08",
|
||||||
"releaseNotes": {
|
"releaseNotes": {
|
||||||
"zh-CN": [
|
"zh-CN": [
|
||||||
"初始版本发布",
|
"初始版本发布",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"downloads": {
|
"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"
|
"h5": "https://www.yxdim.com"
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
@@ -1,7 +1,73 @@
|
|||||||
import type { AppItem, Locale } from '../types/app'
|
import { readonly, ref } from 'vue'
|
||||||
import appsJson from './apps.json'
|
|
||||||
|
|
||||||
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>> = {
|
export const categoryLabel: Record<string, Record<Locale, string>> = {
|
||||||
all: {
|
all: {
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { RouterLink, useRoute } from "vue-router";
|
import { RouterLink, useRoute } from "vue-router";
|
||||||
|
|
||||||
import { apps, categoryLabel } from "../data/apps";
|
import { categoryLabel, useAppsStore } from "../data/apps";
|
||||||
import { useLocaleStore } from "../composables/useLocaleStore";
|
import { useLocaleStore } from "../composables/useLocaleStore";
|
||||||
import { formatDate, formatNumber } from "../utils/format";
|
import { formatDate, formatNumber } from "../utils/format";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { locale, t } = useLocaleStore();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="/">{{
|
<RouterLink class="back-link" to="/">{{
|
||||||
t("返回应用列表", "Back to apps")
|
t("返回应用列表", "Back to apps")
|
||||||
}}</RouterLink>
|
}}</RouterLink>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
|
|
||||||
import { apps, categoryLabel } from "../data/apps";
|
import { categoryLabel, useAppsStore } from "../data/apps";
|
||||||
import { useLocaleStore } from "../composables/useLocaleStore";
|
import { useLocaleStore } from "../composables/useLocaleStore";
|
||||||
import { formatDate, formatNumber } from "../utils/format";
|
import { formatDate, formatNumber } from "../utils/format";
|
||||||
|
|
||||||
const { locale, t } = useLocaleStore();
|
const { locale, t } = useLocaleStore();
|
||||||
|
const { apps, isLoading, error, fetchApps } = useAppsStore();
|
||||||
|
|
||||||
const selectedCategory = ref("all");
|
const selectedCategory = ref("all");
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
|
|
||||||
const categories = computed(() => [
|
const categories = computed(() => [
|
||||||
"all",
|
"all",
|
||||||
...new Set(apps.map((item) => item.category)),
|
...new Set(apps.value.map((item) => item.category)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
const searchWord = keyword.value.trim().toLowerCase();
|
const searchWord = keyword.value.trim().toLowerCase();
|
||||||
|
|
||||||
return apps.filter((item) => {
|
return apps.value.filter((item) => {
|
||||||
const categoryMatched =
|
const categoryMatched =
|
||||||
selectedCategory.value === "all" ||
|
selectedCategory.value === "all" ||
|
||||||
item.category === selectedCategory.value;
|
item.category === selectedCategory.value;
|
||||||
@@ -32,6 +33,10 @@ const filteredApps = computed(() => {
|
|||||||
return categoryMatched && searchMatched;
|
return categoryMatched && searchMatched;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void fetchApps();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -78,8 +83,16 @@ const filteredApps = computed(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="app-grid">
|
<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
|
<RouterLink
|
||||||
v-for="item in filteredApps"
|
v-for="item in !isLoading && !error ? filteredApps : []"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="app-card"
|
class="app-card"
|
||||||
:to="`/app/${item.id}`"
|
:to="`/app/${item.id}`"
|
||||||
@@ -105,7 +118,10 @@ const filteredApps = computed(() => {
|
|||||||
</dl>
|
</dl>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<article v-if="filteredApps.length === 0" class="empty">
|
<article
|
||||||
|
v-if="!isLoading && !error && filteredApps.length === 0"
|
||||||
|
class="empty"
|
||||||
|
>
|
||||||
{{ t("未找到匹配应用", "No matching apps found") }}
|
{{ t("未找到匹配应用", "No matching apps found") }}
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user