macos-web/src/apps/appstore/AppStore.vue

190 lines
8.0 KiB
Vue

<template>
<div class="store-body">
<div class="store-top">
<div class="store-tabs">
<button
v-for="t in tabs" :key="t[0]"
class="store-tab" :class="{ on: tab === t[0] }"
@click="go(t[0])"
>{{ t[1] }}</button>
</div>
<input
class="text-input store-search" type="search" placeholder="搜索 App"
v-model="query" @input="onSearchInput" @keydown.stop
>
</div>
<div class="store-content">
<!-- 发现 -->
<template v-if="tab === 'discover'">
<h2 class="store-h">编辑推荐</h2>
<div class="store-hero">
<div>
<div class="store-hero-tag">今日 App</div>
<div class="store-hero-name">熊掌记</div>
<div class="store-hero-desc">记录灵感,优雅如斯。</div>
<div class="store-card hero-get" style="background:transparent;border:none;padding:10px 0 0">
<button
class="btn store-get" :class="{ primary: !installed('bear') }"
:disabled="installing === 'bear'"
@click="installOrOpen('bear')"
>{{ installing === 'bear' ? '安装中…' : installed('bear') ? '打开' : '获取' }}</button>
</div>
</div>
<img src="/assets/icons/bear.png" alt="" class="store-icon" style="width:120px;height:120px">
</div>
<h2 class="store-h">热门 App</h2>
<div class="store-grid">
<div v-for="item in hotApps" :key="item.id" class="store-card">
<img :src="item.icon" alt="" class="store-icon">
<div class="store-info">
<div class="store-name">{{ item.name }}</div>
<div class="store-desc">{{ item.desc }}</div>
<div class="store-cat">{{ item.cat }}</div>
</div>
<button
class="btn store-get" :class="{ primary: !installed(item.id) }"
:disabled="installing === item.id"
@click="installOrOpen(item.id)"
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
</div>
</div>
</template>
<!-- 分类 -->
<template v-if="tab === 'cats'">
<template v-for="cat in categories" :key="cat">
<h2 class="store-h">{{ cat }}</h2>
<div class="store-grid">
<div v-for="item in catApps(cat)" :key="item.id" class="store-card">
<img :src="item.icon" alt="" class="store-icon">
<div class="store-info">
<div class="store-name">{{ item.name }}</div>
<div class="store-desc">{{ item.desc }}</div>
<div class="store-cat">{{ item.cat }}</div>
</div>
<button
class="btn store-get" :class="{ primary: !installed(item.id) }"
:disabled="installing === item.id"
@click="installOrOpen(item.id)"
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
</div>
</div>
</template>
</template>
<!-- 搜索 -->
<template v-if="tab === 'arcade'">
<h2 class="store-h">搜索 App</h2>
<div v-if="!searchResults.length" class="empty-state" style="height:120px">没有找到相关 App</div>
<div class="store-grid">
<div v-for="item in searchResults" :key="item.id" class="store-card">
<img :src="item.icon" alt="" class="store-icon">
<div class="store-info">
<div class="store-name">{{ item.name }}</div>
<div class="store-desc">{{ item.desc }}</div>
<div class="store-cat">{{ item.cat }}</div>
</div>
<button
class="btn store-get" :class="{ primary: !installed(item.id) }"
:disabled="installing === item.id"
@click="installOrOpen(item.id)"
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
</div>
</div>
</template>
<!-- 更新 -->
<template v-if="tab === 'updates'">
<template v-if="!installedApps.length">
<div class="empty-state" style="height:200px">
<div class="es-icon"></div>
<div>全部 App 均为最新版本</div>
</div>
</template>
<template v-else>
<h2 class="store-h">已安装</h2>
<div class="store-grid">
<div v-for="item in installedApps" :key="item.id" class="store-card">
<img :src="item.icon" alt="" class="store-icon">
<div class="store-info">
<div class="store-name">{{ item.name }}</div>
<div class="store-desc">{{ item.desc }}</div>
<div class="store-cat">{{ item.cat }}</div>
</div>
<button class="btn store-get" @click="installOrOpen(item.id)">打开</button>
</div>
</div>
<p class="set-note">所有已安装 App 均为最新版本</p>
</template>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { store } from '../../composables/useStore'
import { Apps } from '../../composables/useApps'
import { Notify } from '../../composables/useNotify'
const STORE_CATALOG = [
{ id: 'bear', name: '熊掌记', cat: '效率', desc: '优雅的 Markdown 笔记', icon: '/assets/icons/bear.png' },
{ id: 'typora', name: 'Typora', cat: '效率', desc: '所见即所得 Markdown 编辑器', icon: '/assets/icons/typora.png' },
{ id: 'vscode', name: 'VS Code Web', cat: '开发', desc: '轻量代码编辑器', icon: '/assets/icons/vscode.png' },
{ id: 'keynote', name: 'Keynote 讲演', cat: '效率', desc: '制作精美演示文稿', icon: '/assets/icons/keynote.png' },
{ id: 'podcasts', name: '播客', cat: '娱乐', desc: '收听你喜爱的节目', icon: '/assets/icons/podcasts.png' },
{ id: 'news', name: 'Apple News', cat: '阅读', desc: '精选科技资讯', icon: '/assets/icons/news.png' },
{ id: 'github', name: 'GitHub Desktop', cat: '开发', desc: '浏览与管理代码仓库', icon: '/assets/icons/github.png' },
{ id: 'tv', name: 'Apple TV', cat: '娱乐', desc: '观看影片与节目', icon: '/assets/icons/tv.png' },
]
const tabs: [string, string][] = [['discover', '发现'], ['cats', '分类'], ['arcade', '搜索'], ['updates', '更新']]
const tab = ref('discover')
const query = ref('')
const queryDebounced = ref('')
const installing = ref<string | null>(null)
let debounceTimer: any = null
function installedList(): string[] { return store.get('appstore-installed', []) }
function installed(id: string): boolean { return installedList().includes(id) }
function setInstalled(id: string) {
const l = installedList()
if (!l.includes(id)) { l.push(id); store.set('appstore-installed', l) }
}
function onSearchInput() {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
queryDebounced.value = query.value.trim()
if (queryDebounced.value) tab.value = 'arcade'
}, 200)
}
const hotApps = computed(() => STORE_CATALOG.slice(1))
const categories = computed(() => [...new Set(STORE_CATALOG.map(i => i.cat))])
const catApps = (cat: string) => STORE_CATALOG.filter(i => i.cat === cat)
const searchResults = computed(() => {
const q = queryDebounced.value.toLowerCase()
if (!q) return []
return STORE_CATALOG.filter(i => (i.name + i.desc + i.cat).toLowerCase().includes(q))
})
const installedApps = computed(() => STORE_CATALOG.filter(i => installed(i.id)))
function go(t: string) { tab.value = t }
function installOrOpen(id: string) {
const item = STORE_CATALOG.find(i => i.id === id)
if (!item) return
if (installed(id)) { Apps.open(id); return }
installing.value = id
setTimeout(() => {
setInstalled(id)
installing.value = null
Notify.send({ appId: 'appstore', title: 'App Store', body: `"${item.name}"已安装,可在启动台与应用程序文件夹中找到。` })
}, 1200 + Math.random() * 1200)
}
defineExpose({ go, tab })
</script>