190 lines
8.0 KiB
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>
|