feat: 添加应用详情页面和首页功能,支持应用搜索和过滤
This commit is contained in:
@@ -1,352 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import type { AppInfo, AppCategory } from '~/types'
|
|
||||||
|
|
||||||
const { t, locale, setLocale } = useI18n()
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const { platform } = usePlatformDetection()
|
|
||||||
|
|
||||||
// 获取应用列表
|
|
||||||
const { data: appsData } = await useFetch('/api/apps')
|
|
||||||
const apps = computed(() => appsData.value?.apps || [])
|
|
||||||
const categories = computed(() => appsData.value?.categories || [])
|
|
||||||
|
|
||||||
// 当前选中的分类
|
|
||||||
const selectedCategory = ref('all')
|
|
||||||
|
|
||||||
// 搜索关键词
|
|
||||||
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 selectedApp = ref<AppInfo | null>(null)
|
|
||||||
const showAppDetail = ref(false)
|
|
||||||
|
|
||||||
// 切换语言
|
|
||||||
function toggleLanguage() {
|
|
||||||
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开应用详情
|
|
||||||
function openAppDetail(app: AppInfo) {
|
|
||||||
selectedApp.value = app
|
|
||||||
showAppDetail.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载处理
|
|
||||||
async function handleDownload(app: AppInfo, type: 'ios' | 'android' | 'h5') {
|
|
||||||
const url = app.downloads[type]
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'h5') {
|
|
||||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
navigateTo(url, { external: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
await $fetch(`/api/track/${type}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: { appId: app.id },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDark = computed(() => colorMode.value === 'dark')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UApp>
|
<UApp>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<NuxtPage />
|
||||||
<!-- Header -->
|
|
||||||
<UContainer>
|
|
||||||
<header class="sticky top-0 z-50 backdrop-blur-md">
|
|
||||||
<div class="flex items-center justify-between py-4">
|
|
||||||
<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">
|
|
||||||
R
|
|
||||||
</div>
|
|
||||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{{ t('appName') }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<UButton
|
|
||||||
:label="locale === 'zh-CN' ? 'EN' : '中文'"
|
|
||||||
color="neutral"
|
|
||||||
variant="ghost"
|
|
||||||
@click="toggleLanguage"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
:icon="isDark ? 'i-heroicons-sun' : 'i-heroicons-moon'"
|
|
||||||
color="neutral"
|
|
||||||
variant="ghost"
|
|
||||||
@click="colorMode.preference = isDark ? 'light' : 'dark'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</UContainer>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<UContainer class="py-8">
|
|
||||||
<!-- Search and Filter -->
|
|
||||||
<div class="mb-8 space-y-4">
|
|
||||||
<!-- Search -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Categories -->
|
|
||||||
<div class="flex items-center gap-2 overflow-x-auto pb-2">
|
|
||||||
<UButton
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category.id"
|
|
||||||
: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>
|
|
||||||
|
|
||||||
<!-- Apps Grid -->
|
|
||||||
<div v-if="filteredApps.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<UCard
|
|
||||||
v-for="app in filteredApps"
|
|
||||||
:key="app.id"
|
|
||||||
class="cursor-pointer hover:shadow-xl transition-all hover:scale-[1.02]"
|
|
||||||
@click="openAppDetail(app)"
|
|
||||||
>
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<!-- App Icon -->
|
|
||||||
<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">
|
|
||||||
{{ app.name.charAt(0) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- App Info -->
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate">
|
|
||||||
{{ app.name }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1">
|
|
||||||
{{ app.shortDescription[locale as 'zh-CN' | 'en-US'] }}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Download Buttons -->
|
|
||||||
<div class="grid grid-cols-3 gap-2 mt-4">
|
|
||||||
<UButton
|
|
||||||
v-if="app.downloads.ios"
|
|
||||||
icon="i-heroicons-device-phone-mobile"
|
|
||||||
label="iOS"
|
|
||||||
size="sm"
|
|
||||||
block
|
|
||||||
@click.stop="handleDownload(app, 'ios')"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
v-if="app.downloads.android"
|
|
||||||
icon="i-heroicons-device-tablet"
|
|
||||||
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>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-else class="text-center py-20">
|
|
||||||
<UIcon name="i-heroicons-inbox" class="size-20 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
|
||||||
<p class="text-gray-600 dark:text-gray-400">
|
|
||||||
{{ locale === 'zh-CN' ? '没有找到应用' : 'No apps found' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</UContainer>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="mt-20 py-8 border-t border-gray-200 dark:border-gray-800">
|
|
||||||
<UContainer>
|
|
||||||
<div class="text-center text-sm text-gray-600 dark:text-gray-500">
|
|
||||||
<p>© 2025 Riwa. All rights reserved.</p>
|
|
||||||
</div>
|
|
||||||
</UContainer>
|
|
||||||
</footer>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
|||||||
217
packages/distribute/pages/apps/[id].vue
Normal file
217
packages/distribute/pages/apps/[id].vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AppInfo } from '~/types'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
|
// 获取应用详情
|
||||||
|
const { data: app, error } = await useFetch<AppInfo>(`/api/apps/${route.params.id}`)
|
||||||
|
|
||||||
|
// 如果应用不存在,跳转回首页
|
||||||
|
if (error.value) {
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载处理
|
||||||
|
async function handleDownload(type: 'ios' | 'android' | 'h5') {
|
||||||
|
if (!app.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = app.value.downloads[type]
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'h5') {
|
||||||
|
navigateTo(url, { external: true, open: { target: '_blank' } })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigateTo(url, { external: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fetch(`/api/track/${type}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { appId: app.value.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
useHead({
|
||||||
|
title: app.value ? `${app.value.name} - Riwa App Store` : 'Riwa App Store',
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: app.value?.shortDescription[locale.value as 'zh-CN' | 'en-US'] || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="app" class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<UContainer>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="sticky top-0 z-50">
|
||||||
|
<div class="flex items-center gap-4 py-4">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-left"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
@click="goBack"
|
||||||
|
/>
|
||||||
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{{ app.name }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="py-8">
|
||||||
|
<!-- App Header -->
|
||||||
|
<div class="flex items-start gap-6 mb-8">
|
||||||
|
<div class="size-24 rounded-3xl bg-linear-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-4xl flex-shrink-0 shadow-xl">
|
||||||
|
{{ app.name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ app.name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{{ app.shortDescription[locale as 'zh-CN' | 'en-US'] }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-500">
|
||||||
|
<span>{{ locale === 'zh-CN' ? '版本' : 'Version' }} {{ app.version }}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{{ app.releaseDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Buttons -->
|
||||||
|
<div class="grid md:grid-cols-3 gap-4 mb-8">
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.ios"
|
||||||
|
icon="i-heroicons-device-phone-mobile"
|
||||||
|
size="xl"
|
||||||
|
block
|
||||||
|
@click="handleDownload('ios')"
|
||||||
|
>
|
||||||
|
<div class="text-left w-full">
|
||||||
|
<div class="font-semibold text-base">iOS</div>
|
||||||
|
<div v-if="app.size?.ios" class="text-xs opacity-80">
|
||||||
|
{{ app.size.ios }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.android"
|
||||||
|
icon="i-heroicons-device-tablet"
|
||||||
|
size="xl"
|
||||||
|
block
|
||||||
|
@click="handleDownload('android')"
|
||||||
|
>
|
||||||
|
<div class="text-left w-full">
|
||||||
|
<div class="font-semibold text-base">Android</div>
|
||||||
|
<div v-if="app.size?.android" class="text-xs opacity-80">
|
||||||
|
{{ app.size.android }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.h5"
|
||||||
|
icon="i-heroicons-globe-alt"
|
||||||
|
size="xl"
|
||||||
|
block
|
||||||
|
@click="handleDownload('h5')"
|
||||||
|
>
|
||||||
|
<div class="text-left w-full">
|
||||||
|
<div class="font-semibold text-base">Web</div>
|
||||||
|
<div class="text-xs opacity-80">
|
||||||
|
PWA
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<UCard>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary-500">
|
||||||
|
{{ app.stats.total.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
{{ locale === 'zh-CN' ? '总下载' : 'Total Downloads' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary-500">
|
||||||
|
{{ app.stats.today.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
{{ locale === 'zh-CN' ? '今日下载' : 'Today' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary-500">
|
||||||
|
{{ app.stats.ios.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
iOS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
<UCard>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary-500">
|
||||||
|
{{ app.stats.android.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
Android
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<UCard class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
{{ locale === 'zh-CN' ? '应用介绍' : 'Description' }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{{ app.description[locale as 'zh-CN' | 'en-US'] }}
|
||||||
|
</p>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- What's New -->
|
||||||
|
<UCard>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
{{ locale === 'zh-CN' ? '更新内容' : "What's New" }}
|
||||||
|
</h3>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li
|
||||||
|
v-for="(note, index) in app.releaseNotes[locale as 'zh-CN' | 'en-US']"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-start gap-3 text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<span class="text-primary-500 mt-1 text-lg">•</span>
|
||||||
|
<span>{{ note }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
225
packages/distribute/pages/index.vue
Normal file
225
packages/distribute/pages/index.vue
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AppInfo } from '~/types'
|
||||||
|
|
||||||
|
const { t, locale, setLocale } = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
// 获取应用列表
|
||||||
|
const { data: appsData } = await useFetch('/api/apps')
|
||||||
|
const apps = computed(() => appsData.value?.apps || [])
|
||||||
|
const categories = computed(() => appsData.value?.categories || [])
|
||||||
|
|
||||||
|
// 当前选中的分类
|
||||||
|
const selectedCategory = ref('all')
|
||||||
|
|
||||||
|
// 搜索关键词
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换语言
|
||||||
|
function toggleLanguage() {
|
||||||
|
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开应用详情
|
||||||
|
function openAppDetail(app: AppInfo) {
|
||||||
|
navigateTo(`/apps/${app.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载处理
|
||||||
|
async function handleDownload(app: AppInfo, type: 'ios' | 'android' | 'h5') {
|
||||||
|
const url = app.downloads[type]
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'h5') {
|
||||||
|
navigateTo(url, { external: true, open: { target: '_blank' } })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigateTo(url, { external: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fetch(`/api/track/${type}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { appId: app.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDark = computed(() => colorMode.value === 'dark')
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
useHead({
|
||||||
|
title: locale.value === 'zh-CN' ? 'Riwa 应用商店' : 'Riwa App Store',
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: locale.value === 'zh-CN'
|
||||||
|
? '下载 Riwa 系列应用,包括 Riwa 主应用、Riwa 钱包和 Riwa 聊天等'
|
||||||
|
: 'Download Riwa apps including Riwa main app, Riwa Wallet, and Riwa Chat',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<!-- Header -->
|
||||||
|
<UContainer>
|
||||||
|
<header class="sticky top-0 z-50">
|
||||||
|
<div class="flex items-center justify-between py-4">
|
||||||
|
<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">
|
||||||
|
R
|
||||||
|
</div>
|
||||||
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{{ t('appName') }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UButton
|
||||||
|
:label="locale === 'zh-CN' ? 'EN' : '中文'"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
@click="toggleLanguage"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
:icon="isDark ? 'i-heroicons-sun' : 'i-heroicons-moon'"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
@click="colorMode.preference = isDark ? 'light' : 'dark'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</UContainer>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<UContainer class="py-8">
|
||||||
|
<!-- Search and Filter -->
|
||||||
|
<div class="mb-8 space-y-4">
|
||||||
|
<!-- Search -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="flex items-center gap-2 overflow-x-auto pb-2">
|
||||||
|
<UButton
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Apps Grid -->
|
||||||
|
<div v-if="filteredApps.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="app in filteredApps"
|
||||||
|
:key="app.id"
|
||||||
|
class="cursor-pointer hover:shadow-xl transition-all hover:scale-[1.02]"
|
||||||
|
@click="openAppDetail(app)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<!-- App Icon -->
|
||||||
|
<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">
|
||||||
|
{{ app.name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App Info -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate">
|
||||||
|
{{ app.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1">
|
||||||
|
{{ app.shortDescription[locale as 'zh-CN' | 'en-US'] }}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Download Buttons -->
|
||||||
|
<div class="grid grid-cols-3 gap-2 mt-4">
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.ios"
|
||||||
|
icon="i-heroicons-device-phone-mobile"
|
||||||
|
label="iOS"
|
||||||
|
size="sm"
|
||||||
|
block
|
||||||
|
@click.stop="handleDownload(app, 'ios')"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="app.downloads.android"
|
||||||
|
icon="i-heroicons-device-tablet"
|
||||||
|
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>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="text-center py-20">
|
||||||
|
<UIcon name="i-heroicons-inbox" class="size-20 mx-auto text-gray-400 dark:text-gray-600 mb-4" />
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
{{ locale === 'zh-CN' ? '没有找到应用' : 'No apps found' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</UContainer>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="mt-20 py-8 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<UContainer>
|
||||||
|
<div class="text-center text-sm text-gray-600 dark:text-gray-500">
|
||||||
|
<p>© 2025 Riwa. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</UContainer>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user