Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -50,19 +50,19 @@ pnpm generate
|
||||
|
||||
```typescript
|
||||
export const currentVersion: AppVersion = {
|
||||
version: '1.0.0',
|
||||
buildNumber: '100',
|
||||
releaseDate: '2025-12-30',
|
||||
version: "1.0.0",
|
||||
buildNumber: "100",
|
||||
releaseDate: "2025-12-30",
|
||||
releaseNotes: {
|
||||
'zh-CN': ['更新内容...'],
|
||||
'en-US': ['What\'s new...'],
|
||||
"zh-CN": ["更新内容..."],
|
||||
"en-US": ["What's new..."],
|
||||
},
|
||||
downloads: {
|
||||
ios: 'https://example.com/app.ipa',
|
||||
android: 'https://example.com/app.apk',
|
||||
h5: 'https://app.example.com',
|
||||
ios: "https://example.com/app.ipa",
|
||||
android: "https://example.com/app.apk",
|
||||
h5: "https://app.example.com",
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 接入真实 API
|
||||
@@ -81,9 +81,9 @@ Nuxt UI 使用 TailwindCSS 4,可在 `nuxt.config.ts` 中配置:
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
colorMode: {
|
||||
preference: 'system', // 'light' | 'dark' | 'system'
|
||||
preference: "system", // 'light' | 'dark' | 'system'
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
primary: "blue",
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,4 +6,3 @@
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
|
||||
/* 发光脉冲效果 */
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
50% {
|
||||
@@ -96,7 +97,8 @@
|
||||
|
||||
/* 悬浮动画 */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
@@ -208,7 +210,8 @@
|
||||
|
||||
/* 脉冲缩放动画 */
|
||||
@keyframes pulse-scale {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
@@ -250,7 +253,8 @@
|
||||
|
||||
/* 微妙的悬浮动画 */
|
||||
@keyframes float-subtle {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) translateZ(0);
|
||||
}
|
||||
50% {
|
||||
@@ -284,7 +288,8 @@
|
||||
|
||||
/* 发光脉冲动画 */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
@@ -298,7 +303,8 @@
|
||||
|
||||
/* 微妙的脉冲动画 */
|
||||
@keyframes pulse-subtle {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
@@ -362,7 +368,8 @@
|
||||
|
||||
/* 震动效果 */
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
@@ -401,7 +408,8 @@
|
||||
|
||||
/* 呼吸灯效果 */
|
||||
@keyframes breathe {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
const { canInstall, isInstalled, install } = usePWAInstall()
|
||||
const installing = ref(false)
|
||||
const dismissed = ref(false)
|
||||
const { t } = useI18n();
|
||||
const { canInstall, isInstalled, install } = usePWAInstall();
|
||||
const installing = ref(false);
|
||||
const dismissed = ref(false);
|
||||
|
||||
// 从 localStorage 读取是否已关闭,但只在未安装状态下有效
|
||||
onMounted(() => {
|
||||
// 如果应用未安装,检查用户是否之前关闭过横幅
|
||||
if (!isInstalled.value) {
|
||||
dismissed.value = localStorage.getItem('pwa-banner-dismissed') === 'true'
|
||||
dismissed.value = localStorage.getItem("pwa-banner-dismissed") === "true";
|
||||
}
|
||||
else {
|
||||
// 如果应用已安装,清除关闭记录(为了卸载后能再次提示)
|
||||
localStorage.removeItem('pwa-banner-dismissed')
|
||||
localStorage.removeItem("pwa-banner-dismissed");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 监听安装状态变化
|
||||
watch(isInstalled, (newValue) => {
|
||||
if (newValue) {
|
||||
// 应用安装后,清除关闭记录
|
||||
localStorage.removeItem('pwa-banner-dismissed')
|
||||
dismissed.value = false
|
||||
localStorage.removeItem("pwa-banner-dismissed");
|
||||
dismissed.value = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 监听 canInstall 变化(卸载后会重新触发 beforeinstallprompt)
|
||||
watch(canInstall, (newValue) => {
|
||||
if (newValue && !isInstalled.value) {
|
||||
// 如果可以安装且未安装,清除之前的关闭记录
|
||||
// 这样卸载后再次访问就会重新显示横幅
|
||||
localStorage.removeItem('pwa-banner-dismissed')
|
||||
dismissed.value = false
|
||||
localStorage.removeItem("pwa-banner-dismissed");
|
||||
dismissed.value = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
async function handleInstall() {
|
||||
installing.value = true
|
||||
installing.value = true;
|
||||
try {
|
||||
const success = await install()
|
||||
const success = await install();
|
||||
if (success) {
|
||||
console.log('PWA 安装成功')
|
||||
console.log("PWA 安装成功");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
installing.value = false
|
||||
installing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function dismissBanner() {
|
||||
dismissed.value = true
|
||||
localStorage.setItem('pwa-banner-dismissed', 'true')
|
||||
dismissed.value = true;
|
||||
localStorage.setItem("pwa-banner-dismissed", "true");
|
||||
}
|
||||
|
||||
const showBanner = computed(() => canInstall.value && !isInstalled.value && !dismissed.value)
|
||||
const showBanner = computed(() => canInstall.value && !isInstalled.value && !dismissed.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,7 +73,7 @@ const showBanner = computed(() => canInstall.value && !isInstalled.value && !dis
|
||||
<div class="absolute inset-0 bg-linear-to-br from-primary-50/80 via-blue-50/50 to-purple-50/80 dark:from-primary-950/50 dark:via-blue-950/30 dark:to-purple-950/50" />
|
||||
<div class="absolute top-0 right-0 w-64 h-64 bg-primary-500/10 rounded-full blur-3xl" />
|
||||
<div class="absolute bottom-0 left-0 w-64 h-64 bg-purple-500/10 rounded-full blur-3xl" />
|
||||
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="relative z-10 flex items-center gap-4">
|
||||
<!-- 图标 -->
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
const { canInstall, isInstalled, install } = usePWAInstall()
|
||||
const installing = ref(false)
|
||||
const { t } = useI18n();
|
||||
const { canInstall, isInstalled, install } = usePWAInstall();
|
||||
const installing = ref(false);
|
||||
|
||||
async function handleInstall() {
|
||||
installing.value = true
|
||||
installing.value = true;
|
||||
try {
|
||||
const success = await install()
|
||||
const success = await install();
|
||||
if (success) {
|
||||
// 可以显示成功提示
|
||||
console.log('PWA 安装成功')
|
||||
console.log("PWA 安装成功");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
installing.value = false
|
||||
installing.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
export function usePWAInstall() {
|
||||
const deferredPrompt = ref<any>(null)
|
||||
const canInstall = ref(false)
|
||||
const isInstalled = ref(false)
|
||||
const deferredPrompt = ref<any>(null);
|
||||
const canInstall = ref(false);
|
||||
const isInstalled = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否已安装
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
isInstalled.value = true
|
||||
return
|
||||
if (window.matchMedia("(display-mode: standalone)").matches) {
|
||||
isInstalled.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听安装提示事件
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault()
|
||||
deferredPrompt.value = e
|
||||
canInstall.value = true
|
||||
})
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt.value = e;
|
||||
canInstall.value = true;
|
||||
});
|
||||
|
||||
// 监听安装成功事件
|
||||
window.addEventListener('appinstalled', () => {
|
||||
deferredPrompt.value = null
|
||||
canInstall.value = false
|
||||
isInstalled.value = true
|
||||
})
|
||||
})
|
||||
window.addEventListener("appinstalled", () => {
|
||||
deferredPrompt.value = null;
|
||||
canInstall.value = false;
|
||||
isInstalled.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
async function install() {
|
||||
if (!deferredPrompt.value) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deferredPrompt.value.prompt()
|
||||
const { outcome } = await deferredPrompt.value.userChoice
|
||||
await deferredPrompt.value.prompt();
|
||||
const { outcome } = await deferredPrompt.value.userChoice;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
deferredPrompt.value = null
|
||||
canInstall.value = false
|
||||
return true
|
||||
if (outcome === "accepted") {
|
||||
deferredPrompt.value = null;
|
||||
canInstall.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
return false
|
||||
console.error("安装失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@ export function usePWAInstall() {
|
||||
canInstall,
|
||||
isInstalled,
|
||||
install,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import type { Platform } from '~/types'
|
||||
import type { Platform } from "~/types";
|
||||
|
||||
export function usePlatformDetection() {
|
||||
const platform = useState<Platform>('platform', () => 'unknown')
|
||||
const platform = useState<Platform>("platform", () => "unknown");
|
||||
|
||||
function detectPlatform(): Platform {
|
||||
if (import.meta.server)
|
||||
return 'unknown'
|
||||
return "unknown";
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (/iphone|ipad|ipod/.test(ua))
|
||||
return 'ios'
|
||||
return "ios";
|
||||
else if (/android/.test(ua))
|
||||
return 'android'
|
||||
return "android";
|
||||
else if (/windows|macintosh|linux/.test(ua))
|
||||
return 'desktop'
|
||||
return "desktop";
|
||||
|
||||
return 'unknown'
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
platform.value = detectPlatform()
|
||||
})
|
||||
platform.value = detectPlatform();
|
||||
});
|
||||
|
||||
return {
|
||||
platform: readonly(platform),
|
||||
isIOS: computed(() => platform.value === 'ios'),
|
||||
isAndroid: computed(() => platform.value === 'android'),
|
||||
isDesktop: computed(() => platform.value === 'desktop'),
|
||||
}
|
||||
isIOS: computed(() => platform.value === "ios"),
|
||||
isAndroid: computed(() => platform.value === "android"),
|
||||
isDesktop: computed(() => platform.value === "desktop"),
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +1,34 @@
|
||||
import type { AppVersion, DownloadStats } from '~/types'
|
||||
import type { AppVersion, DownloadStats } from "~/types";
|
||||
|
||||
// 当前版本信息
|
||||
export const currentVersion: AppVersion = {
|
||||
version: '1.0.0',
|
||||
buildNumber: '100',
|
||||
releaseDate: '2025-12-30',
|
||||
version: "1.0.0",
|
||||
buildNumber: "100",
|
||||
releaseDate: "2025-12-30",
|
||||
releaseNotes: {
|
||||
'zh-CN': [
|
||||
'🎉 首次发布',
|
||||
'✨ 全新的用户界面设计',
|
||||
'🔐 增强的安全特性',
|
||||
'⚡ 性能优化,响应速度提升 30%',
|
||||
'🌍 支持多语言切换',
|
||||
'🌙 深色模式支持',
|
||||
"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',
|
||||
"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: 'http://localhost:5173',
|
||||
ios: "https://example.com/riwa-ios-1.0.0.ipa",
|
||||
android: "https://example.com/riwa-android-1.0.0.apk",
|
||||
h5: "http://localhost:5173",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟下载统计数据
|
||||
export const mockDownloadStats: DownloadStats = {
|
||||
@@ -36,4 +36,4 @@ export const mockDownloadStats: DownloadStats = {
|
||||
today: 156,
|
||||
ios: 7234,
|
||||
android: 5346,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ services:
|
||||
# nginx conf
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
|
||||
|
||||
# version: '3.8'
|
||||
|
||||
# services:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
import antfu from "@antfu/eslint-config";
|
||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||
|
||||
export default withNuxt(
|
||||
antfu({
|
||||
formatters: true,
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxt/eslint',
|
||||
'@vite-pwa/nuxt',
|
||||
"@nuxt/ui",
|
||||
"@nuxtjs/i18n",
|
||||
"@nuxt/eslint",
|
||||
"@vite-pwa/nuxt",
|
||||
],
|
||||
|
||||
devtools: { enabled: true },
|
||||
|
||||
pwa: {
|
||||
registerType: 'autoUpdate',
|
||||
registerType: "autoUpdate",
|
||||
manifest: {
|
||||
name: 'Riwa应用商店',
|
||||
short_name: 'Riwa应用商店',
|
||||
description: 'Riwa App 下载 - iOS, Android, H5',
|
||||
theme_color: '#3b82f6',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
scope: '/',
|
||||
start_url: '/',
|
||||
name: "Riwa应用商店",
|
||||
short_name: "Riwa应用商店",
|
||||
description: "Riwa App 下载 - iOS, Android, H5",
|
||||
theme_color: "#3b82f6",
|
||||
background_color: "#ffffff",
|
||||
display: "standalone",
|
||||
scope: "/",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any',
|
||||
src: "/favicon.svg",
|
||||
sizes: "512x512",
|
||||
type: "image/svg+xml",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: '/favicon.svg',
|
||||
sizes: '192x192',
|
||||
type: 'image/svg+xml',
|
||||
src: "/favicon.svg",
|
||||
sizes: "192x192",
|
||||
type: "image/svg+xml",
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
navigateFallback: '/',
|
||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
||||
navigateFallback: "/",
|
||||
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: 'google-fonts-cache',
|
||||
cacheName: "google-fonts-cache",
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||
@@ -55,9 +55,9 @@ export default defineNuxtConfig({
|
||||
},
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: 'gstatic-fonts-cache',
|
||||
cacheName: "gstatic-fonts-cache",
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||
@@ -75,81 +75,81 @@ export default defineNuxtConfig({
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
type: 'module',
|
||||
type: "module",
|
||||
},
|
||||
injectManifest: {
|
||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
||||
globPatterns: ["**/*.{js,css,html,png,svg,ico}"],
|
||||
},
|
||||
},
|
||||
|
||||
css: [
|
||||
'~/assets/css/main.css',
|
||||
'~/assets/css/animations.css',
|
||||
"~/assets/css/main.css",
|
||||
"~/assets/css/animations.css",
|
||||
],
|
||||
|
||||
colorMode: {
|
||||
preference: 'light',
|
||||
preference: "light",
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'zh-CN',
|
||||
defaultLocale: "zh-CN",
|
||||
locales: [
|
||||
{
|
||||
code: 'zh-CN',
|
||||
name: '简体中文',
|
||||
file: 'zh-CN.json',
|
||||
code: "zh-CN",
|
||||
name: "简体中文",
|
||||
file: "zh-CN.json",
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
name: 'English',
|
||||
file: 'en-US.json',
|
||||
code: "en-US",
|
||||
name: "English",
|
||||
file: "en-US.json",
|
||||
},
|
||||
],
|
||||
strategy: 'no_prefix',
|
||||
strategy: "no_prefix",
|
||||
detectBrowserLanguage: {
|
||||
useCookie: true,
|
||||
cookieKey: 'i18n_locale',
|
||||
redirectOn: 'root',
|
||||
cookieKey: "i18n_locale",
|
||||
redirectOn: "root",
|
||||
},
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, initial-scale=1',
|
||||
title: 'Riwa App 下载',
|
||||
charset: "utf-8",
|
||||
viewport: "width=device-width, initial-scale=1",
|
||||
title: "Riwa App 下载",
|
||||
meta: [
|
||||
{ name: 'description', content: 'Riwa App 下载 - iOS, Android, H5' },
|
||||
{ name: "description", content: "Riwa App 下载 - iOS, Android, H5" },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
||||
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
devServer:{
|
||||
host: '0.0.0.0'
|
||||
devServer: {
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ['/'],
|
||||
routes: ["/"],
|
||||
crawlLinks: true,
|
||||
},
|
||||
},
|
||||
|
||||
hooks: {
|
||||
async 'nitro:config'(nitroConfig) {
|
||||
"nitro:config": async function (nitroConfig) {
|
||||
// 预渲染所有应用详情页
|
||||
const { apps } = await import('./data/apps')
|
||||
const routes = apps.map(app => `/apps/${app.id}`)
|
||||
nitroConfig.prerender = nitroConfig.prerender || {}
|
||||
const { apps } = await import("./data/apps");
|
||||
const routes = apps.map(app => `/apps/${app.id}`);
|
||||
nitroConfig.prerender = nitroConfig.prerender || {};
|
||||
nitroConfig.prerender.routes = [
|
||||
...(nitroConfig.prerender.routes || []),
|
||||
...routes,
|
||||
]
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2025-12-30',
|
||||
})
|
||||
compatibilityDate: "2025-12-30",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@riwa/distribute",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type { AppInfo } from '~/types'
|
||||
import { apps } from '~/data/apps'
|
||||
import type { AppInfo } from "~/types";
|
||||
import { apps } from "~/data/apps";
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
// 直接从数据文件获取应用详情
|
||||
const app = computed(() => apps.find(a => a.id === route.params.id))
|
||||
const app = computed(() => apps.find(a => a.id === route.params.id));
|
||||
|
||||
// 如果应用不存在,跳转回首页
|
||||
if (!app.value) {
|
||||
navigateTo('/')
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
// 下载处理
|
||||
async function handleDownload(type: 'ios' | 'android' | 'h5') {
|
||||
async function handleDownload(type: "ios" | "android" | "h5") {
|
||||
if (!app.value) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const url = app.value.downloads[type]
|
||||
const url = app.value.downloads[type];
|
||||
|
||||
if (!url) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'h5') {
|
||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
||||
if (type === "h5") {
|
||||
navigateTo(url, { external: true, open: { target: "_blank" } });
|
||||
}
|
||||
else {
|
||||
navigateTo(url, { external: true })
|
||||
navigateTo(url, { external: true });
|
||||
}
|
||||
|
||||
await $fetch(`/api/track/${type}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: { appId: app.value.id },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 返回首页
|
||||
function goBack() {
|
||||
router.back()
|
||||
router.back();
|
||||
}
|
||||
|
||||
// SEO
|
||||
useHead({
|
||||
title: app.value ? `${app.value.name} - Riwa App Store` : 'Riwa App Store',
|
||||
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'] || '',
|
||||
name: "description",
|
||||
content: app.value?.shortDescription[locale.value as "zh-CN" | "en-US"] || "",
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="app" class="min-h-screen relative overflow-hidden bg-gradient-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
||||
<!-- 科技感网格背景 -->
|
||||
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
||||
<div class="absolute inset-0" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;"></div>
|
||||
<div class="absolute inset-0" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 动态发光球体背景 -->
|
||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-pulse" />
|
||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-pulse" style="animation-delay: 1s;" />
|
||||
</div>
|
||||
|
||||
<UContainer class="relative z-10">
|
||||
@@ -91,10 +91,10 @@ useHead({
|
||||
<!-- App Header -->
|
||||
<div class="flex items-start gap-6 mb-8 group">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-2 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-3xl opacity-50 blur-xl group-hover:opacity-100 transition-all duration-500 animate-pulse"></div>
|
||||
<div class="absolute -inset-2 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-3xl opacity-50 blur-xl group-hover:opacity-100 transition-all duration-500 animate-pulse" />
|
||||
<div class="size-24 rounded-3xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-4xl shrink-0 shadow-2xl shadow-blue-500/50 relative overflow-hidden group-hover:scale-110 transition-all duration-500 p-3">
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/30 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></div>
|
||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-2xl" />
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-white/0 via-white/30 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700" />
|
||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-2xl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
@@ -115,7 +115,7 @@ useHead({
|
||||
<!-- Download Buttons -->
|
||||
<div class="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<div v-if="app.downloads.ios" class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UButton
|
||||
icon="i-heroicons-device-phone-mobile"
|
||||
size="xl"
|
||||
@@ -123,16 +123,18 @@ useHead({
|
||||
class="relative transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/50 hover:-translate-y-2"
|
||||
@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 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>
|
||||
</div>
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-if="app.downloads.android" class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UButton
|
||||
icon="i-heroicons-device-tablet"
|
||||
size="xl"
|
||||
@@ -140,16 +142,18 @@ useHead({
|
||||
class="relative transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/50 hover:-translate-y-2"
|
||||
@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 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>
|
||||
</div>
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-if="app.downloads.h5" class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UButton
|
||||
icon="i-heroicons-globe-alt"
|
||||
size="xl"
|
||||
@@ -158,7 +162,9 @@ useHead({
|
||||
@click="handleDownload('h5')"
|
||||
>
|
||||
<div class="text-left w-full">
|
||||
<div class="font-semibold text-base">Web</div>
|
||||
<div class="font-semibold text-base">
|
||||
Web
|
||||
</div>
|
||||
<div class="text-xs opacity-80">
|
||||
PWA
|
||||
</div>
|
||||
@@ -170,12 +176,12 @@ useHead({
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold bg-gradient-to-r from-primary-500 to-purple-500 bg-clip-text text-transparent">
|
||||
{{ app.stats.total.toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
{{ locale === 'zh-CN' ? '总下载' : 'Total Downloads' }}
|
||||
</div>
|
||||
@@ -183,7 +189,7 @@ useHead({
|
||||
</UCard>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
@@ -196,7 +202,7 @@ useHead({
|
||||
</UCard>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-blue-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-cyan-500 bg-clip-text text-transparent">
|
||||
@@ -209,7 +215,7 @@ useHead({
|
||||
</UCard>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-xl opacity-0 group-hover:opacity-100 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-indigo-500/20 transition-all duration-500 hover:-translate-y-1">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent">
|
||||
@@ -225,7 +231,7 @@ useHead({
|
||||
|
||||
<!-- Description -->
|
||||
<div class="relative group mb-8">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-primary-500 via-purple-500 to-blue-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-primary-500/10 transition-all duration-500">
|
||||
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent mb-4">
|
||||
{{ locale === 'zh-CN' ? '应用介绍' : 'Description' }}
|
||||
@@ -238,11 +244,11 @@ useHead({
|
||||
|
||||
<!-- What's New -->
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 via-pink-500 to-rose-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500"></div>
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-purple-500 via-pink-500 to-rose-500 rounded-xl opacity-0 group-hover:opacity-30 blur transition-all duration-500" />
|
||||
<UCard class="relative backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 hover:shadow-xl hover:shadow-purple-500/10 transition-all duration-500">
|
||||
<h3 class="text-xl font-semibold bg-gradient-to-r from-gray-900 via-purple-600 to-pink-600 dark:from-white dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-4">
|
||||
{{ locale === 'zh-CN' ? '更新内容' : "What's New" }}
|
||||
</h3>
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li
|
||||
v-for="(note, index) in app.releaseNotes[locale as 'zh-CN' | 'en-US']"
|
||||
|
||||
@@ -1,104 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import type { AppInfo } from '~/types'
|
||||
import type { AppInfo } from "~/types";
|
||||
|
||||
import { apps as appsData, categories as categoriesData } from '~/data/apps'
|
||||
import { apps as appsData, categories as categoriesData } from "~/data/apps";
|
||||
|
||||
const { t, locale, setLocale } = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const colorMode = useColorMode();
|
||||
|
||||
// 直接使用数据文件
|
||||
const apps = computed(() => appsData)
|
||||
const categories = computed(() => categoriesData)
|
||||
const apps = computed(() => appsData);
|
||||
const categories = computed(() => categoriesData);
|
||||
|
||||
// 当前选中的分类
|
||||
const selectedCategory = ref('all')
|
||||
const selectedCategory = ref("all");
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
const searchKeyword = ref("");
|
||||
|
||||
// 过滤后的应用列表
|
||||
const filteredApps = computed(() => {
|
||||
let result = apps.value
|
||||
let result = apps.value;
|
||||
|
||||
// 按分类过滤
|
||||
if (selectedCategory.value !== 'all') {
|
||||
result = result.filter(app => app.category === selectedCategory.value)
|
||||
if (selectedCategory.value !== "all") {
|
||||
result = result.filter(app => app.category === selectedCategory.value);
|
||||
}
|
||||
|
||||
// 按搜索关键词过滤
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
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),
|
||||
)
|
||||
|| app.shortDescription[locale.value as "zh-CN" | "en-US"].toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
|
||||
// 切换语言
|
||||
function toggleLanguage() {
|
||||
setLocale(locale.value === 'zh-CN' ? 'en-US' : 'zh-CN')
|
||||
setLocale(locale.value === "zh-CN" ? "en-US" : "zh-CN");
|
||||
}
|
||||
|
||||
// 打开应用详情
|
||||
function openAppDetail(app: AppInfo) {
|
||||
navigateTo(`/apps/${app.id}`)
|
||||
navigateTo(`/apps/${app.id}`);
|
||||
}
|
||||
|
||||
// 下载处理
|
||||
async function handleDownload(app: AppInfo, type: 'ios' | 'android' | 'h5', event?: MouseEvent | TouchEvent) {
|
||||
const url = app.downloads[type]
|
||||
async function handleDownload(app: AppInfo, type: "ios" | "android" | "h5", event?: MouseEvent | TouchEvent) {
|
||||
const url = app.downloads[type];
|
||||
|
||||
if (!url) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'h5') {
|
||||
navigateTo(url, { external: true, open: { target: '_blank' } })
|
||||
if (type === "h5") {
|
||||
navigateTo(url, { external: true, open: { target: "_blank" } });
|
||||
}
|
||||
else {
|
||||
navigateTo(url, { external: true })
|
||||
navigateTo(url, { external: true });
|
||||
}
|
||||
|
||||
await $fetch(`/api/track/${type}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: { appId: app.id },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const isDark = computed(() => colorMode.value === 'dark')
|
||||
const isDark = computed(() => colorMode.value === "dark");
|
||||
|
||||
// SEO
|
||||
useHead({
|
||||
title: locale.value === 'zh-CN' ? 'Riwa 应用商店' : 'Riwa App Store',
|
||||
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',
|
||||
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 relative overflow-hidden bg-linear-to-br from-gray-50 via-blue-50/30 to-purple-50/30 dark:from-gray-950 dark:via-blue-950/20 dark:to-purple-950/20">
|
||||
<!-- 科技感网格背景 -->
|
||||
<div class="fixed inset-0 opacity-30 dark:opacity-20 pointer-events-none">
|
||||
<div class="absolute inset-0 animate-pulse-subtle" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;"></div>
|
||||
<div class="absolute inset-0 animate-pulse-subtle" style="background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 50px 50px;" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 动态发光球体背景 -->
|
||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-breathe"></div>
|
||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-breathe" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-breathe" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute top-1/4 -left-48 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl animate-breathe" />
|
||||
<div class="absolute bottom-1/4 -right-48 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl animate-breathe" style="animation-delay: 1s;" />
|
||||
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl animate-breathe" style="animation-delay: 2s;" />
|
||||
<!-- 额外的动态光球 -->
|
||||
<div class="absolute top-1/3 right-1/4 w-64 h-64 bg-cyan-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 0.5s;"></div>
|
||||
<div class="absolute bottom-1/3 left-1/3 w-72 h-72 bg-indigo-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 1.5s;"></div>
|
||||
<div class="absolute top-1/3 right-1/4 w-64 h-64 bg-cyan-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 0.5s;" />
|
||||
<div class="absolute bottom-1/3 left-1/3 w-72 h-72 bg-indigo-500/15 rounded-full blur-2xl animate-float" style="animation-delay: 1.5s;" />
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
@@ -107,8 +107,8 @@ useHead({
|
||||
<div class="flex items-center justify-between py-4">
|
||||
<div class="flex items-center gap-3 group">
|
||||
<div class="size-10 rounded-xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-xl relative overflow-hidden shadow-lg shadow-blue-500/50">
|
||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/20 to-white/0 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
|
||||
<div class="absolute -inset-1 bg-blue-400/30 rounded-xl blur-md opacity-0 group-hover:opacity-100 animate-pulse-ring"></div>
|
||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/20 to-white/0 -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
|
||||
<div class="absolute -inset-1 bg-blue-400/30 rounded-xl blur-md opacity-0 group-hover:opacity-100 animate-pulse-ring" />
|
||||
<span class="relative z-10">R</span>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold bg-linear-to-r from-gray-900 via-primary-600 to-purple-600 dark:from-white dark:via-primary-400 dark:to-purple-400 bg-clip-text text-transparent animate-gradient">
|
||||
@@ -180,30 +180,30 @@ useHead({
|
||||
class="cursor-pointer backdrop-blur-sm bg-white/90 dark:bg-gray-900/90 border border-gray-200/50 dark:border-gray-800/50 duration-500 hover:shadow-2xl hover:shadow-primary-500/20 hover:-translate-y-2 hover:rotate-y-2 active:scale-95 active:shadow-lg active:shadow-primary-500/40 relative overflow-hidden"
|
||||
@click="openAppDetail(app)"
|
||||
>
|
||||
<!-- 内部发光效果 -->
|
||||
<div class="absolute inset-0 bg-linear-to-br from-primary-500/5 to-purple-500/5"></div>
|
||||
|
||||
<div class="flex items-start gap-4 relative z-10">
|
||||
<!-- 内部发光效果 -->
|
||||
<div class="absolute inset-0 bg-linear-to-br from-primary-500/5 to-purple-500/5" />
|
||||
|
||||
<div class="flex items-start gap-4 relative z-10">
|
||||
<!-- App Icon -->
|
||||
<div class="size-16 rounded-2xl bg-linear-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-2xl shrink-0 shadow-lg shadow-blue-500/50 relative overflow-hidden group-hover:shadow-2xl group-hover:shadow-blue-500/60 transition-all duration-500 group-hover:scale-110 group-hover:rotate-3 group-active:scale-105 group-active:rotate-1 p-2">
|
||||
<!-- 动态发光效果 -->
|
||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/30 to-white/0 -translate-x-full group-hover:translate-x-full group-active:translate-x-[50%] transition-transform duration-700"></div>
|
||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-lg transition-all duration-300 group-active:scale-95 group-hover:-rotate-3" />
|
||||
<div class="absolute inset-0 bg-linear-to-tr from-white/0 via-white/30 to-white/0 -translate-x-full group-hover:translate-x-full group-active:translate-x-[50%] transition-transform duration-700" />
|
||||
<img :src="app.icon" :alt="app.name" class="size-full object-contain relative z-10 rounded-lg transition-all duration-300 group-active:scale-95 group-hover:-rotate-3">
|
||||
</div>
|
||||
|
||||
<!-- App Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-lg text-gray-900 dark:text-white truncate group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300">
|
||||
{{ app.name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1 transition-colors duration-300 group-hover:text-gray-700 dark:group-hover:text-gray-300">
|
||||
{{ 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 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300">
|
||||
<span class="animate-pulse-subtle">v{{ app.version }}</span>
|
||||
<span>•</span>
|
||||
<span class="font-medium">{{ app.stats.total.toLocaleString() }} {{ locale === 'zh-CN' ? '次下载' : 'downloads' }}</span>
|
||||
</div>
|
||||
{{ app.name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1 transition-colors duration-300 group-hover:text-gray-700 dark:group-hover:text-gray-300">
|
||||
{{ 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 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors duration-300">
|
||||
<span class="animate-pulse-subtle">v{{ app.version }}</span>
|
||||
<span>•</span>
|
||||
<span class="font-medium">{{ app.stats.total.toLocaleString() }} {{ locale === 'zh-CN' ? '次下载' : 'downloads' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -217,8 +217,7 @@ useHead({
|
||||
block
|
||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||
@click.stop="(e) => handleDownload(app, 'ios', e)"
|
||||
>
|
||||
</UButton>
|
||||
/>
|
||||
<UButton
|
||||
v-if="app.downloads.android"
|
||||
icon="i-heroicons-device-tablet"
|
||||
@@ -227,8 +226,7 @@ useHead({
|
||||
block
|
||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||
@click.stop="(e) => handleDownload(app, 'android', e)"
|
||||
>
|
||||
</UButton>
|
||||
/>
|
||||
<UButton
|
||||
v-if="app.downloads.h5"
|
||||
icon="i-heroicons-globe-alt"
|
||||
@@ -237,8 +235,7 @@ useHead({
|
||||
block
|
||||
class="transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/50 hover:-translate-y-1 active:scale-95 active:shadow-xl active:shadow-blue-500/60 relative overflow-hidden group/btn"
|
||||
@click.stop="(e) => handleDownload(app, 'h5', e)"
|
||||
>
|
||||
</UButton>
|
||||
/>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
@@ -262,4 +259,4 @@ useHead({
|
||||
</UContainer>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getAllApps, categories } from '~/data/apps'
|
||||
import { categories, getAllApps } from "~/data/apps";
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
return {
|
||||
apps: getAllApps(),
|
||||
categories,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { getAppById } from '~/data/apps'
|
||||
import { getAppById } from "~/data/apps";
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const id = getRouterParam(event, 'id')
|
||||
const id = getRouterParam(event, "id");
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'App ID is required',
|
||||
})
|
||||
message: "App ID is required",
|
||||
});
|
||||
}
|
||||
|
||||
const app = getAppById(id)
|
||||
const app = getAppById(id);
|
||||
|
||||
if (!app) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'App not found',
|
||||
})
|
||||
message: "App not found",
|
||||
});
|
||||
}
|
||||
|
||||
return app
|
||||
})
|
||||
return app;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchDownloadStats()
|
||||
})
|
||||
return await fetchDownloadStats();
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const platform = getRouterParam(event, 'platform') as 'ios' | 'android' | 'h5'
|
||||
const body = await readBody(event)
|
||||
const appId = body?.appId
|
||||
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({
|
||||
statusCode: 400,
|
||||
message: 'Invalid platform',
|
||||
})
|
||||
message: "Invalid platform",
|
||||
});
|
||||
}
|
||||
|
||||
await trackDownload(platform)
|
||||
await trackDownload(platform);
|
||||
|
||||
return { success: true, appId, platform }
|
||||
})
|
||||
return { success: true, appId, platform };
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentVersion } from '~/data/versions'
|
||||
import { currentVersion } from "~/data/versions";
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
return currentVersion
|
||||
})
|
||||
return currentVersion;
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { DownloadStats } from '~/types'
|
||||
import { mockDownloadStats } from '~/data/versions'
|
||||
import type { DownloadStats } from "~/types";
|
||||
import { mockDownloadStats } from "~/data/versions";
|
||||
|
||||
// 获取下载统计(可替换为真实 API)
|
||||
export async function fetchDownloadStats(): Promise<DownloadStats> {
|
||||
// 模拟 API 延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return mockDownloadStats
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return mockDownloadStats;
|
||||
}
|
||||
|
||||
// 记录下载事件(可替换为真实 API)
|
||||
export async function trackDownload(platform: 'ios' | 'android' | 'h5'): Promise<void> {
|
||||
export async function trackDownload(platform: "ios" | "android" | "h5"): Promise<void> {
|
||||
// 模拟 API 延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
console.log(`Download tracked: ${platform}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
console.log(`Download tracked: ${platform}`);
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
export interface AppInfo {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
shortDescription: {
|
||||
'zh-CN': string
|
||||
'en-US': string
|
||||
}
|
||||
"zh-CN": string;
|
||||
"en-US": string;
|
||||
};
|
||||
description: {
|
||||
'zh-CN': string
|
||||
'en-US': string
|
||||
}
|
||||
category: string
|
||||
version: string
|
||||
buildNumber: string
|
||||
releaseDate: string
|
||||
"zh-CN": string;
|
||||
"en-US": string;
|
||||
};
|
||||
category: string;
|
||||
version: string;
|
||||
buildNumber: string;
|
||||
releaseDate: string;
|
||||
releaseNotes: {
|
||||
'zh-CN': string[]
|
||||
'en-US': string[]
|
||||
}
|
||||
"zh-CN": string[];
|
||||
"en-US": string[];
|
||||
};
|
||||
downloads: {
|
||||
ios?: string
|
||||
android?: string
|
||||
h5?: string
|
||||
}
|
||||
screenshots?: string[]
|
||||
ios?: string;
|
||||
android?: string;
|
||||
h5?: string;
|
||||
};
|
||||
screenshots?: string[];
|
||||
size?: {
|
||||
ios?: string
|
||||
android?: string
|
||||
}
|
||||
stats: DownloadStats
|
||||
ios?: string;
|
||||
android?: string;
|
||||
};
|
||||
stats: DownloadStats;
|
||||
}
|
||||
|
||||
export interface AppVersion {
|
||||
version: string
|
||||
buildNumber: string
|
||||
releaseDate: string
|
||||
version: string;
|
||||
buildNumber: string;
|
||||
releaseDate: string;
|
||||
releaseNotes: {
|
||||
'zh-CN': string[]
|
||||
'en-US': string[]
|
||||
}
|
||||
"zh-CN": string[];
|
||||
"en-US": string[];
|
||||
};
|
||||
downloads: {
|
||||
ios: string
|
||||
android: string
|
||||
h5: string
|
||||
}
|
||||
ios: string;
|
||||
android: string;
|
||||
h5: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DownloadStats {
|
||||
total: number
|
||||
today: number
|
||||
ios: number
|
||||
android: number
|
||||
total: number;
|
||||
today: number;
|
||||
ios: number;
|
||||
android: number;
|
||||
}
|
||||
|
||||
export type Platform = 'ios' | 'android' | 'desktop' | 'unknown'
|
||||
export type Platform = "ios" | "android" | "desktop" | "unknown";
|
||||
|
||||
export interface AppCategory {
|
||||
id: string
|
||||
id: string;
|
||||
name: {
|
||||
'zh-CN': string
|
||||
'en-US': string
|
||||
}
|
||||
icon?: string
|
||||
"zh-CN": string;
|
||||
"en-US": string;
|
||||
};
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user