feat: 添加应用列表和分类功能,支持搜索和过滤
This commit is contained in:
@@ -1,31 +1,64 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DownloadStats } from '~/types'
|
import type { AppInfo, AppCategory } from '~/types'
|
||||||
|
|
||||||
const { t, locale, setLocale } = useI18n()
|
const { t, locale, setLocale } = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const { platform, isIOS, isAndroid } = usePlatformDetection()
|
const { platform } = usePlatformDetection()
|
||||||
|
|
||||||
// 版本数据
|
// 获取应用列表
|
||||||
const { data: versionData } = await useFetch('/api/version')
|
const { data: appsData } = await useFetch('/api/apps')
|
||||||
const version = computed(() => versionData.value || {
|
const apps = computed(() => appsData.value?.apps || [])
|
||||||
version: '1.0.0',
|
const categories = computed(() => appsData.value?.categories || [])
|
||||||
buildNumber: '100',
|
|
||||||
releaseDate: '2025-12-30',
|
// 当前选中的分类
|
||||||
releaseNotes: { 'zh-CN': [], 'en-US': [] },
|
const selectedCategory = ref('all')
|
||||||
downloads: { ios: '', android: '', h5: '' },
|
|
||||||
|
// 搜索关键词
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
|
// 过滤后的应用列表
|
||||||
|
const filteredApps = computed(() => {
|
||||||
|
let result = apps.value
|
||||||
|
|
||||||
|
// 按分类过滤
|
||||||
|
if (selectedCategory.value !== 'all') {
|
||||||
|
result = result.filter(app => app.category === selectedCategory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按搜索关键词过滤
|
||||||
|
if (searchKeyword.value.trim()) {
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
result = result.filter(app =>
|
||||||
|
app.name.toLowerCase().includes(keyword)
|
||||||
|
|| app.shortDescription[locale.value as 'zh-CN' | 'en-US'].toLowerCase().includes(keyword),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
// 下载统计
|
// 选中的应用(用于显示详情)
|
||||||
const { data: stats, refresh: refreshStats } = await useFetch<DownloadStats>('/api/stats')
|
const selectedApp = ref<AppInfo | null>(null)
|
||||||
|
const showAppDetail = ref(false)
|
||||||
|
|
||||||
// 切换语言
|
// 切换语言
|
||||||
function toggleLanguage() {
|
function toggleLanguage() {
|
||||||
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开应用详情
|
||||||
|
function openAppDetail(app: AppInfo) {
|
||||||
|
selectedApp.value = app
|
||||||
|
showAppDetail.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 下载处理
|
// 下载处理
|
||||||
async function handleDownload(type: 'ios' | 'android' | 'h5') {
|
async function handleDownload(app: AppInfo, type: 'ios' | 'android' | 'h5') {
|
||||||
const url = version.value.downloads[type]
|
const url = app.downloads[type]
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'h5') {
|
if (type === 'h5') {
|
||||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
navigateTo(url, { external: true, open: { target: '_blank' } })
|
||||||
@@ -34,8 +67,10 @@ async function handleDownload(type: 'ios' | 'android' | 'h5') {
|
|||||||
navigateTo(url, { external: true })
|
navigateTo(url, { external: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
await $fetch(`/api/track/${type}`, { method: 'POST' })
|
await $fetch(`/api/track/${type}`, {
|
||||||
await refreshStats()
|
method: 'POST',
|
||||||
|
body: { appId: app.id },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = computed(() => colorMode.value === 'dark')
|
const isDark = computed(() => colorMode.value === 'dark')
|
||||||
@@ -43,11 +78,11 @@ const isDark = computed(() => colorMode.value === 'dark')
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UApp>
|
<UApp>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<UContainer>
|
<UContainer>
|
||||||
<header class="sticky top-0 z-50 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md">
|
<header class="sticky top-0 z-50 backdrop-blur-md">
|
||||||
<div class="flex items-center justify-between py-4">
|
<div class="flex items-center justify-between py-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="size-10 rounded-xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-xl">
|
<div class="size-10 rounded-xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-xl">
|
||||||
R
|
R
|
||||||
@@ -76,165 +111,99 @@ const isDark = computed(() => colorMode.value === 'dark')
|
|||||||
</UContainer>
|
</UContainer>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<UContainer class="py-12">
|
<UContainer class="py-8">
|
||||||
<!-- Hero Section -->
|
<!-- Search and Filter -->
|
||||||
<div class="text-center mb-16">
|
<div class="mb-8 space-y-4">
|
||||||
<div class="size-24 mx-auto mb-6 rounded-3xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center shadow-xl">
|
<!-- Search -->
|
||||||
<span class="text-white font-bold text-4xl">R</span>
|
<div class="relative max-w-2xl mx-auto">
|
||||||
|
<UInput
|
||||||
|
v-model="searchKeyword"
|
||||||
|
icon="i-heroicons-magnifying-glass"
|
||||||
|
size="xl"
|
||||||
|
:placeholder="locale === 'zh-CN' ? '搜索应用...' : 'Search apps...'"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-4xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{{ t('downloadTitle') }}
|
<!-- Categories -->
|
||||||
</h2>
|
<div class="flex items-center gap-2 overflow-x-auto pb-2">
|
||||||
<div class="flex items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
<UButton
|
||||||
<span>{{ t('version') }} {{ version.version }}</span>
|
v-for="category in categories"
|
||||||
<span>•</span>
|
:key="category.id"
|
||||||
<span>{{ t('updateDate') }} {{ version.releaseDate }}</span>
|
:label="category.name[locale as 'zh-CN' | 'en-US']"
|
||||||
|
:color="selectedCategory === category.id ? 'primary' : 'neutral'"
|
||||||
|
:variant="selectedCategory === category.id ? 'solid' : 'ghost'"
|
||||||
|
@click="selectedCategory = category.id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download Buttons -->
|
<!-- Apps Grid -->
|
||||||
<div class="grid md:grid-cols-3 gap-4 mb-16">
|
<div v-if="filteredApps.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<UCard
|
<UCard
|
||||||
:class="isIOS && 'ring-2 ring-primary-500'"
|
v-for="app in filteredApps"
|
||||||
class="cursor-pointer hover:shadow-xl transition-all hover:scale-105"
|
:key="app.id"
|
||||||
@click="handleDownload('ios')"
|
class="cursor-pointer hover:shadow-xl transition-all hover:scale-[1.02]"
|
||||||
|
@click="openAppDetail(app)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-start gap-4">
|
||||||
<UIcon name="i-heroicons-device-phone-mobile" class="size-8" />
|
<!-- App Icon -->
|
||||||
<div>
|
<div class="size-16 rounded-2xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-2xl flex-shrink-0">
|
||||||
<h3 class="font-semibold text-lg">
|
{{ app.name.charAt(0) }}
|
||||||
iOS
|
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
iPhone & iPad
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<p class="text-sm font-medium text-primary-500">
|
|
||||||
{{ t('downloadIOS') }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard
|
<!-- App Info -->
|
||||||
:class="isAndroid && 'ring-2 ring-primary-500'"
|
<div class="flex-1 min-w-0">
|
||||||
class="cursor-pointer hover:shadow-xl transition-all hover:scale-105"
|
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate">
|
||||||
@click="handleDownload('android')"
|
{{ app.name }}
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<UIcon name="i-heroicons-device-tablet" class="size-8" />
|
|
||||||
<div>
|
|
||||||
<h3 class="font-semibold text-lg">
|
|
||||||
Android
|
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1">
|
||||||
APK Package
|
{{ app.shortDescription[locale as 'zh-CN' | 'en-US'] }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex items-center gap-3 mt-3 text-xs text-gray-500 dark:text-gray-500">
|
||||||
|
<span>v{{ app.version }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{{ app.stats.total.toLocaleString() }} {{ locale === 'zh-CN' ? '次下载' : 'downloads' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium text-primary-500">
|
|
||||||
{{ t('downloadAndroid') }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard
|
<!-- Download Buttons -->
|
||||||
class="cursor-pointer hover:shadow-xl transition-all hover:scale-105"
|
<div class="grid grid-cols-3 gap-2 mt-4">
|
||||||
@click="handleDownload('h5')"
|
<UButton
|
||||||
>
|
v-if="app.downloads.ios"
|
||||||
<div class="flex items-center gap-3 mb-2">
|
icon="i-heroicons-device-phone-mobile"
|
||||||
<UIcon name="i-heroicons-globe-alt" class="size-8" />
|
label="iOS"
|
||||||
<div>
|
size="sm"
|
||||||
<h3 class="font-semibold text-lg">
|
block
|
||||||
Web
|
@click.stop="handleDownload(app, 'ios')"
|
||||||
</h3>
|
/>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<UButton
|
||||||
H5 Version
|
v-if="app.downloads.android"
|
||||||
</p>
|
icon="i-heroicons-device-tablet"
|
||||||
</div>
|
label="Android"
|
||||||
|
size="sm"
|
||||||
|
block
|
||||||
|
@click.stop="handleDownload(app, 'android')"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.h5"
|
||||||
|
icon="i-heroicons-globe-alt"
|
||||||
|
label="Web"
|
||||||
|
size="sm"
|
||||||
|
block
|
||||||
|
@click.stop="handleDownload(app, 'h5')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium text-primary-500">
|
|
||||||
{{ t('openH5') }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download Stats -->
|
<!-- Empty State -->
|
||||||
<div v-if="stats" class="grid md:grid-cols-4 gap-4 mb-16">
|
<div v-else class="text-center py-20">
|
||||||
<UCard>
|
<UIcon name="i-heroicons-inbox" class="size-20 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
{{ t('totalDownloads') }}
|
{{ locale === 'zh-CN' ? '没有找到应用' : 'No apps found' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{{ stats.total.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
<UCard>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
{{ t('todayDownloads') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-3xl font-bold text-primary-500">
|
|
||||||
{{ stats.today.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
<UCard>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
{{ t('iosDownloads') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{{ stats.ios.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
<UCard>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
{{ t('androidDownloads') }}
|
|
||||||
</p>
|
|
||||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{{ stats.android.toLocaleString() }}
|
|
||||||
</p>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- What's New -->
|
|
||||||
<UCard class="mb-16">
|
|
||||||
<h3 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">
|
|
||||||
{{ t('whatsNew') }}
|
|
||||||
</h3>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li
|
|
||||||
v-for="(note, index) in version.releaseNotes[locale]"
|
|
||||||
:key="index"
|
|
||||||
class="flex items-start gap-3 text-gray-700 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
<span class="text-primary-500 mt-1">•</span>
|
|
||||||
<span>{{ note }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<!-- Install Guide -->
|
|
||||||
<div class="grid md:grid-cols-2 gap-6">
|
|
||||||
<UCard>
|
|
||||||
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{{ t('iosGuideTitle') }}
|
|
||||||
</h3>
|
|
||||||
<ol class="space-y-3 text-gray-700 dark:text-gray-300">
|
|
||||||
<li>{{ t('iosGuideStep1') }}</li>
|
|
||||||
<li>{{ t('iosGuideStep2') }}</li>
|
|
||||||
<li>{{ t('iosGuideStep3') }}</li>
|
|
||||||
<li>{{ t('iosGuideStep4') }}</li>
|
|
||||||
</ol>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard>
|
|
||||||
<h3 class="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{{ t('androidGuideTitle') }}
|
|
||||||
</h3>
|
|
||||||
<ol class="space-y-3 text-gray-700 dark:text-gray-300">
|
|
||||||
<li>{{ t('androidGuideStep1') }}</li>
|
|
||||||
<li>{{ t('androidGuideStep2') }}</li>
|
|
||||||
<li>{{ t('androidGuideStep3') }}</li>
|
|
||||||
<li>{{ t('androidGuideStep4') }}</li>
|
|
||||||
</ol>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
|
|
||||||
@@ -246,6 +215,138 @@ const isDark = computed(() => colorMode.value === 'dark')
|
|||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- App Detail Modal -->
|
||||||
|
<UModal v-model="showAppDetail" :ui="{ width: 'max-w-3xl' }">
|
||||||
|
<UCard v-if="selectedApp">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="size-20 rounded-2xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-3xl">
|
||||||
|
{{ selectedApp.name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{{ selectedApp.name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{{ selectedApp.shortDescription[locale as 'zh-CN' | 'en-US'] }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-500">
|
||||||
|
<span>{{ locale === 'zh-CN' ? '版本' : 'Version' }} {{ selectedApp.version }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{{ selectedApp.releaseDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ locale === 'zh-CN' ? '应用介绍' : 'Description' }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
|
{{ selectedApp.description[locale as 'zh-CN' | 'en-US'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div class="text-center p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-primary-500">
|
||||||
|
{{ selectedApp.stats.total.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{{ locale === 'zh-CN' ? '总下载' : 'Total' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-primary-500">
|
||||||
|
{{ selectedApp.stats.today.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{{ locale === 'zh-CN' ? '今日' : 'Today' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-primary-500">
|
||||||
|
{{ selectedApp.stats.ios.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
iOS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div class="text-2xl font-bold text-primary-500">
|
||||||
|
{{ selectedApp.stats.android.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Android
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- What's New -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
{{ locale === 'zh-CN' ? '更新内容' : "What's New" }}
|
||||||
|
</h3>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li
|
||||||
|
v-for="(note, index) in selectedApp.releaseNotes[locale as 'zh-CN' | 'en-US']"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-start gap-2 text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<span class="text-primary-500 mt-1">•</span>
|
||||||
|
<span>{{ note }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Buttons -->
|
||||||
|
<div class="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<UButton
|
||||||
|
v-if="selectedApp.downloads.ios"
|
||||||
|
icon="i-heroicons-device-phone-mobile"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
@click="handleDownload(selectedApp, 'ios')"
|
||||||
|
>
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="font-semibold">iOS</div>
|
||||||
|
<div v-if="selectedApp.size?.ios" class="text-xs opacity-80">
|
||||||
|
{{ selectedApp.size.ios }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="selectedApp.downloads.android"
|
||||||
|
icon="i-heroicons-device-tablet"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
@click="handleDownload(selectedApp, 'android')"
|
||||||
|
>
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="font-semibold">Android</div>
|
||||||
|
<div v-if="selectedApp.size?.android" class="text-xs opacity-80">
|
||||||
|
{{ selectedApp.size.android }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="selectedApp.downloads.h5"
|
||||||
|
icon="i-heroicons-globe-alt"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
@click="handleDownload(selectedApp, 'h5')"
|
||||||
|
>
|
||||||
|
<div class="font-semibold">Web</div>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
</UApp>
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
183
packages/distribute/data/apps.ts
Normal file
183
packages/distribute/data/apps.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import type { AppInfo, AppCategory } from '~/types'
|
||||||
|
|
||||||
|
// 应用分类
|
||||||
|
export const categories: AppCategory[] = [
|
||||||
|
{ id: 'all', name: { 'zh-CN': '全部', 'en-US': 'All' } },
|
||||||
|
{ id: 'finance', name: { 'zh-CN': '金融', 'en-US': 'Finance' } },
|
||||||
|
{ id: 'social', name: { 'zh-CN': '社交', 'en-US': 'Social' } },
|
||||||
|
{ id: 'tools', name: { 'zh-CN': '工具', 'en-US': 'Tools' } },
|
||||||
|
{ id: 'entertainment', name: { 'zh-CN': '娱乐', 'en-US': 'Entertainment' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 应用列表
|
||||||
|
export const apps: AppInfo[] = [
|
||||||
|
{
|
||||||
|
id: 'riwa-app',
|
||||||
|
name: 'Riwa',
|
||||||
|
icon: '/icons/riwa.svg',
|
||||||
|
shortDescription: {
|
||||||
|
'zh-CN': '数字资产交易平台',
|
||||||
|
'en-US': 'Digital Asset Trading Platform',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
'zh-CN': 'Riwa 是一个安全、快速的数字资产交易平台,提供专业的交易工具和实时行情数据。',
|
||||||
|
'en-US': 'Riwa is a secure and fast digital asset trading platform with professional trading tools and real-time market data.',
|
||||||
|
},
|
||||||
|
category: 'finance',
|
||||||
|
version: '1.0.0',
|
||||||
|
buildNumber: '100',
|
||||||
|
releaseDate: '2025-12-30',
|
||||||
|
releaseNotes: {
|
||||||
|
'zh-CN': [
|
||||||
|
'🎉 首次发布',
|
||||||
|
'✨ 全新的用户界面设计',
|
||||||
|
'🔐 增强的安全特性',
|
||||||
|
'⚡ 性能优化,响应速度提升 30%',
|
||||||
|
'🌍 支持多语言切换',
|
||||||
|
'🌙 深色模式支持',
|
||||||
|
],
|
||||||
|
'en-US': [
|
||||||
|
'🎉 Initial Release',
|
||||||
|
'✨ Brand new user interface',
|
||||||
|
'🔐 Enhanced security features',
|
||||||
|
'⚡ Performance optimization, 30% faster response',
|
||||||
|
'🌍 Multi-language support',
|
||||||
|
'🌙 Dark mode support',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
downloads: {
|
||||||
|
ios: 'https://example.com/riwa-ios-1.0.0.ipa',
|
||||||
|
android: 'https://example.com/riwa-android-1.0.0.apk',
|
||||||
|
h5: 'https://app.riwa.com',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
ios: '45.2 MB',
|
||||||
|
android: '38.6 MB',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
total: 12580,
|
||||||
|
today: 156,
|
||||||
|
ios: 7234,
|
||||||
|
android: 5346,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'riwa-wallet',
|
||||||
|
name: 'Riwa Wallet',
|
||||||
|
icon: '/icons/wallet.svg',
|
||||||
|
shortDescription: {
|
||||||
|
'zh-CN': '安全的数字资产钱包',
|
||||||
|
'en-US': 'Secure Digital Asset Wallet',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
'zh-CN': 'Riwa Wallet 是一个去中心化的数字资产钱包,支持多链资产管理,私钥由您掌控。',
|
||||||
|
'en-US': 'Riwa Wallet is a decentralized digital asset wallet supporting multi-chain asset management with your keys under your control.',
|
||||||
|
},
|
||||||
|
category: 'finance',
|
||||||
|
version: '2.1.5',
|
||||||
|
buildNumber: '215',
|
||||||
|
releaseDate: '2025-12-28',
|
||||||
|
releaseNotes: {
|
||||||
|
'zh-CN': [
|
||||||
|
'🔐 增强了安全性',
|
||||||
|
'✨ 支持更多区块链网络',
|
||||||
|
'🐛 修复了若干已知问题',
|
||||||
|
'⚡ 交易速度优化',
|
||||||
|
],
|
||||||
|
'en-US': [
|
||||||
|
'🔐 Enhanced security',
|
||||||
|
'✨ Support for more blockchain networks',
|
||||||
|
'🐛 Fixed several known issues',
|
||||||
|
'⚡ Transaction speed optimization',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
downloads: {
|
||||||
|
ios: 'https://example.com/wallet-ios-2.1.5.ipa',
|
||||||
|
android: 'https://example.com/wallet-android-2.1.5.apk',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
ios: '32.8 MB',
|
||||||
|
android: '28.3 MB',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
total: 8920,
|
||||||
|
today: 89,
|
||||||
|
ios: 5230,
|
||||||
|
android: 3690,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'riwa-chat',
|
||||||
|
name: 'Riwa Chat',
|
||||||
|
icon: '/icons/chat.svg',
|
||||||
|
shortDescription: {
|
||||||
|
'zh-CN': '加密即时通讯工具',
|
||||||
|
'en-US': 'Encrypted Instant Messaging',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
'zh-CN': 'Riwa Chat 提供端到端加密的即时通讯服务,保护您的隐私安全。',
|
||||||
|
'en-US': 'Riwa Chat provides end-to-end encrypted instant messaging to protect your privacy.',
|
||||||
|
},
|
||||||
|
category: 'social',
|
||||||
|
version: '3.0.2',
|
||||||
|
buildNumber: '302',
|
||||||
|
releaseDate: '2025-12-25',
|
||||||
|
releaseNotes: {
|
||||||
|
'zh-CN': [
|
||||||
|
'💬 优化了消息同步机制',
|
||||||
|
'📎 支持发送更多类型的文件',
|
||||||
|
'🎨 界面美化',
|
||||||
|
'🔊 新增语音消息功能',
|
||||||
|
],
|
||||||
|
'en-US': [
|
||||||
|
'💬 Optimized message sync',
|
||||||
|
'📎 Support for more file types',
|
||||||
|
'🎨 UI improvements',
|
||||||
|
'🔊 Added voice message feature',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
downloads: {
|
||||||
|
ios: 'https://example.com/chat-ios-3.0.2.ipa',
|
||||||
|
android: 'https://example.com/chat-android-3.0.2.apk',
|
||||||
|
h5: 'https://chat.riwa.com',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
ios: '52.1 MB',
|
||||||
|
android: '46.8 MB',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
total: 15430,
|
||||||
|
today: 203,
|
||||||
|
ios: 8920,
|
||||||
|
android: 6510,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取所有应用
|
||||||
|
export function getAllApps(): AppInfo[] {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID获取应用
|
||||||
|
export function getAppById(id: string): AppInfo | undefined {
|
||||||
|
return apps.find(app => app.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据分类获取应用
|
||||||
|
export function getAppsByCategory(categoryId: string): AppInfo[] {
|
||||||
|
if (categoryId === 'all') {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
return apps.filter(app => app.category === categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索应用
|
||||||
|
export function searchApps(keyword: string, locale: 'zh-CN' | 'en-US' = 'zh-CN'): AppInfo[] {
|
||||||
|
const lowerKeyword = keyword.toLowerCase()
|
||||||
|
return apps.filter(app =>
|
||||||
|
app.name.toLowerCase().includes(lowerKeyword)
|
||||||
|
|| app.shortDescription[locale].toLowerCase().includes(lowerKeyword)
|
||||||
|
|| app.description[locale].toLowerCase().includes(lowerKeyword),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
{
|
{
|
||||||
"appName": "Riwa App",
|
"appName": "Riwa App Store",
|
||||||
"downloadTitle": "Download Riwa",
|
"searchApps": "Search apps...",
|
||||||
|
"allApps": "All Apps",
|
||||||
|
"downloads": "downloads",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"updateDate": "Update Date",
|
"releaseDate": "Release Date",
|
||||||
"downloadIOS": "Download iOS",
|
|
||||||
"downloadAndroid": "Download Android",
|
|
||||||
"openH5": "Open Web Version",
|
|
||||||
"whatsNew": "What's New",
|
"whatsNew": "What's New",
|
||||||
|
"appDescription": "Description",
|
||||||
"downloadStats": "Download Statistics",
|
"downloadStats": "Download Statistics",
|
||||||
"totalDownloads": "Total Downloads",
|
"totalDownloads": "Total Downloads",
|
||||||
"todayDownloads": "Today",
|
"todayDownloads": "Today",
|
||||||
"iosDownloads": "iOS Downloads",
|
"iosDownloads": "iOS Downloads",
|
||||||
"androidDownloads": "Android Downloads",
|
"androidDownloads": "Android Downloads",
|
||||||
"installGuide": "Installation Guide",
|
"noAppsFound": "No apps found",
|
||||||
"iosGuideTitle": "iOS Installation",
|
"downloadApp": "Download App"
|
||||||
"iosGuideStep1": "1. Click download button and wait",
|
|
||||||
"iosGuideStep2": "2. Go to Settings - General - VPN & Device Management",
|
|
||||||
"iosGuideStep3": "3. Trust the enterprise app certificate",
|
|
||||||
"iosGuideStep4": "4. Return to home screen and open Riwa App",
|
|
||||||
"androidGuideTitle": "Android Installation",
|
|
||||||
"androidGuideStep1": "1. Click download button and wait",
|
|
||||||
"androidGuideStep2": "2. Allow installation from unknown sources",
|
|
||||||
"androidGuideStep3": "3. Tap the APK file to install",
|
|
||||||
"androidGuideStep4": "4. Open the app after installation"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
{
|
{
|
||||||
"appName": "Riwa App",
|
"appName": "Riwa 应用商店",
|
||||||
"downloadTitle": "下载 Riwa",
|
"searchApps": "搜索应用...",
|
||||||
|
"allApps": "全部应用",
|
||||||
|
"downloads": "次下载",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"updateDate": "更新日期",
|
"releaseDate": "发布日期",
|
||||||
"downloadIOS": "下载 iOS 版",
|
|
||||||
"downloadAndroid": "下载 Android 版",
|
|
||||||
"openH5": "打开网页版",
|
|
||||||
"whatsNew": "更新内容",
|
"whatsNew": "更新内容",
|
||||||
|
"appDescription": "应用介绍",
|
||||||
"downloadStats": "下载统计",
|
"downloadStats": "下载统计",
|
||||||
"totalDownloads": "总下载量",
|
"totalDownloads": "总下载量",
|
||||||
"todayDownloads": "今日下载",
|
"todayDownloads": "今日下载",
|
||||||
"iosDownloads": "iOS 下载",
|
"iosDownloads": "iOS 下载",
|
||||||
"androidDownloads": "Android 下载",
|
"androidDownloads": "Android 下载",
|
||||||
"installGuide": "安装指引",
|
"noAppsFound": "没有找到应用",
|
||||||
"iosGuideTitle": "iOS 安装说明",
|
"downloadApp": "下载应用"
|
||||||
"iosGuideStep1": "1. 点击下载按钮,等待下载完成",
|
|
||||||
"iosGuideStep2": "2. 进入「设置」-「通用」-「VPN与设备管理」",
|
|
||||||
"iosGuideStep3": "3. 信任企业级应用证书",
|
|
||||||
"iosGuideStep4": "4. 返回主屏幕打开 Riwa App",
|
|
||||||
"androidGuideTitle": "Android 安装说明",
|
|
||||||
"androidGuideStep1": "1. 点击下载按钮,等待下载完成",
|
|
||||||
"androidGuideStep2": "2. 允许安装未知来源应用",
|
|
||||||
"androidGuideStep3": "3. 点击安装包进行安装",
|
|
||||||
"androidGuideStep4": "4. 安装完成后打开应用"
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/distribute/server/api/apps.get.ts
Normal file
8
packages/distribute/server/api/apps.get.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { getAllApps, categories } from '~/data/apps'
|
||||||
|
|
||||||
|
export default defineEventHandler(() => {
|
||||||
|
return {
|
||||||
|
apps: getAllApps(),
|
||||||
|
categories,
|
||||||
|
}
|
||||||
|
})
|
||||||
23
packages/distribute/server/api/apps/[id].get.ts
Normal file
23
packages/distribute/server/api/apps/[id].get.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { getAppById } from '~/data/apps'
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'App ID is required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = getAppById(id)
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'App not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
})
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const platform = getRouterParam(event, 'platform') as 'ios' | 'android' | 'h5'
|
const platform = getRouterParam(event, 'platform') as 'ios' | 'android' | 'h5'
|
||||||
|
const body = await readBody(event)
|
||||||
|
const appId = body?.appId
|
||||||
|
|
||||||
if (!['ios', 'android', 'h5'].includes(platform)) {
|
if (!['ios', 'android', 'h5'].includes(platform)) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -10,5 +12,5 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
await trackDownload(platform)
|
await trackDownload(platform)
|
||||||
|
|
||||||
return { success: true }
|
return { success: true, appId, platform }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,36 @@
|
|||||||
|
export interface AppInfo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
shortDescription: {
|
||||||
|
'zh-CN': string
|
||||||
|
'en-US': string
|
||||||
|
}
|
||||||
|
description: {
|
||||||
|
'zh-CN': string
|
||||||
|
'en-US': string
|
||||||
|
}
|
||||||
|
category: string
|
||||||
|
version: string
|
||||||
|
buildNumber: string
|
||||||
|
releaseDate: string
|
||||||
|
releaseNotes: {
|
||||||
|
'zh-CN': string[]
|
||||||
|
'en-US': string[]
|
||||||
|
}
|
||||||
|
downloads: {
|
||||||
|
ios?: string
|
||||||
|
android?: string
|
||||||
|
h5?: string
|
||||||
|
}
|
||||||
|
screenshots?: string[]
|
||||||
|
size?: {
|
||||||
|
ios?: string
|
||||||
|
android?: string
|
||||||
|
}
|
||||||
|
stats: DownloadStats
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppVersion {
|
export interface AppVersion {
|
||||||
version: string
|
version: string
|
||||||
buildNumber: string
|
buildNumber: string
|
||||||
@@ -21,3 +54,12 @@ export interface DownloadStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Platform = 'ios' | 'android' | 'desktop' | 'unknown'
|
export type Platform = 'ios' | 'android' | 'desktop' | 'unknown'
|
||||||
|
|
||||||
|
export interface AppCategory {
|
||||||
|
id: string
|
||||||
|
name: {
|
||||||
|
'zh-CN': string
|
||||||
|
'en-US': string
|
||||||
|
}
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user