add store

This commit is contained in:
Elysia
2026-01-17 20:07:27 +08:00
parent 2250f89266
commit a5b3d1278c
2169 changed files with 387526 additions and 86 deletions

View File

@@ -0,0 +1,57 @@
<template>
<div class="card" @click="openDetail">
<div class="icon">
<img
:data-src="iconPath"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23f0f3f8'/%3E%3C/svg%3E"
alt="icon"
class="lazy"
>
</div>
<div class="meta">
<div class="title">{{ app.Name || '' }}</div>
<div class="muted">{{ app.Pkgname || '' }} · {{ app.Version || '' }}</div>
<div class="description">
{{ description }}
</div>
</div>
</div>
</template>
<script setup>
import { computed, defineProps, defineEmits, onMounted } from 'vue';
const props = defineProps({
app: {
type: Object,
required: true
}
});
const emit = defineEmits(['open-detail']);
const iconPath = computed(() => {
return `./${props.app._category}/${props.app.Pkgname}/icon.png`;
});
const description = computed(() => {
const more = props.app.More || '';
return more.substring(0, 80) + (more.length > 80 ? '...' : '');
});
const openDetail = () => {
emit('open-detail', props.app);
};
onMounted(() => {
// 懒加载图片
const lazyImage = document.querySelector('.lazy');
if (window.observer && lazyImage) {
window.observer.observe(lazyImage);
}
});
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="modal-backdrop" :style="{ display: show ? 'flex' : 'none' }" role="dialog" aria-hidden="false">
<div class="modal">
<div class="modal-header">
<div class="modal-icon-title" v-if="app">
<div class="modal-icon">
<img
:src="iconPath"
alt="icon"
/>
</div>
<div class="modal-title-section">
<div class="modal-title">{{ app.Name || '' }}</div>
<div class="modal-subtitle">{{ app.Pkgname || '' }} · {{ app.Version || '' }}</div>
</div>
</div>
<div class="modal-actions">
<button class="install-btn" @click="handleInstall">
<i class="fas fa-download"></i> 安装
</button>
<button class="close-modal" @click="closeModal" aria-label="关闭">×</button>
</div>
</div>
<div class="apm-note">
首次安装APM后需要重启系统以在启动器中看到应用入口可前往
<a href="https://gitee.com/amber-ce/amber-pm/releases" target="_blank">APM Releases</a> 获取 APM
</div>
<div class="screens">
<img
v-for="(screen, index) in screenshots"
:key="index"
:src="screen"
alt="screenshot"
@click="openPreview(index)"
@error="e => e.target.style.display = 'none'"
>
</div>
<div class="info">
<div class="info-item" v-if="app?.Author">
<div class="info-label">作者</div>
<div class="info-value">{{ app.Author }}</div>
</div>
<div class="info-item" v-if="app?.Contributor">
<div class="info-label">贡献者</div>
<div class="info-value">{{ app.Contributor }}</div>
</div>
<div class="info-item" v-if="app?.Size">
<div class="info-label">大小</div>
<div class="info-value">{{ app.Size }}</div>
</div>
<div class="info-item" v-if="app?.Update">
<div class="info-label">更新时间</div>
<div class="info-value">{{ app.Update }}</div>
</div>
<div class="info-item" v-if="app?.Website">
<div class="info-label">网站</div>
<div class="info-value">
<a :href="app.Website" target="_blank">{{ app.Website }}</a>
</div>
</div>
<div class="info-item" v-if="app?.Version">
<div class="info-label">版本</div>
<div class="info-value">{{ app.Version }}</div>
</div>
<div class="info-item" v-if="app?.Tags">
<div class="info-label">标签</div>
<div class="info-value">{{ app.Tags }}</div>
</div>
</div>
<div class="more-details" v-if="app?.More && app.More.trim() !== ''">
<div class="more-details-title">应用详情</div>
<div class="more-details-content" v-html="app.More.replace(/\n/g, '<br>')"></div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, defineProps, defineEmits } from 'vue';
import { APM_STORE_BASE_URL } from '../global/StoreConfig';
const props = defineProps({
show: {
type: Boolean,
required: true
},
app: {
type: Object,
default: null
},
screenshots: {
type: Array,
required: true
}
});
const emit = defineEmits(['close', 'install', 'open-preview']);
const iconPath = computed(() => {
return props.app ? `${APM_STORE_BASE_URL}/${props.app._category}/${props.app.Pkgname}/icon.png` : '';
});
const closeModal = () => {
emit('close');
};
const handleInstall = () => {
emit('install');
};
const openPreview = (index) => {
emit('open-preview', index);
};
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="grid" v-if="!loading">
<AppCard
v-for="(app, index) in apps"
:key="index"
:app="app"
@open-detail="$emit('open-detail', app)"
/>
</div>
<div v-else class="loading">
<div class="grid">
<div v-for="n in 8" :key="n" class="card loading">
<div class="icon"></div>
<div class="meta">
<div class="title">加载中...</div>
<div class="muted">正在获取应用数据</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import AppCard from './AppCard.vue';
defineProps({
apps: {
type: Array,
required: true
},
loading: {
type: Boolean,
required: true
}
});
defineEmits(['open-detail']);
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="topbar">
<TopActions @update="$emit('update')" @list="$emit('list')" />
<div class="search">
<input
id="searchBox"
placeholder="搜索应用名 / 包名 / 标签(回车或自动)"
@input="debounceSearch"
v-model="localSearchQuery"
/>
</div>
<div class="stats" id="currentCount">
{{ appsCount }} 个应用 · 在任何主流Linux发行上安装应用
</div>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';
import TopActions from './TopActions.vue';
const props = defineProps({
searchQuery: {
type: String,
required: true
},
appsCount: {
type: Number,
required: true
}
});
const emit = defineEmits(['update-search', 'update', 'list']);
const localSearchQuery = ref(props.searchQuery || '');
const timeoutId = ref(null);
const debounceSearch = (e) => {
clearTimeout(timeoutId.value);
timeoutId.value = setTimeout(() => {
emit('update-search', e);
}, 220);
};
watch(() => props.searchQuery, (newVal) => {
localSearchQuery.value = newVal || '';
});
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="brand">APM x86客户端商店</div>
<ThemeToggle :is-dark="isDarkTheme" @toggle="toggleTheme" />
<div class="categories">
<div class="category" :class="{ 'active': activeCategory === 'all' }" @click="selectCategory('all')">
<div>全部应用</div>
<div class="count">{{ categoryCounts.all || 0 }}</div>
</div>
<div v-for="(category, key) in categories" :key="key" class="category" :class="{ 'active': activeCategory === key }"
@click="selectCategory(key)">
<div>{{ category.zh }} <span class="muted">({{ category.en }})</span></div>
<div class="count">{{ categoryCounts[key] || 0 }}</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import ThemeToggle from './ThemeToggle.vue';
const props = defineProps({
categories: {
type: Object,
required: true
},
activeCategory: {
type: String,
required: true
},
categoryCounts: {
type: Object,
required: true
},
isDarkTheme: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['toggle-theme', 'select-category']);
const toggleTheme = () => {
emit('toggle-theme');
};
const selectCategory = (category) => {
emit('select-category', category);
};
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -1,38 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="screen-preview-backdrop" :style="{ display: show ? 'flex' : 'none' }">
<div class="screen-preview">
<div class="screen-preview-controls">
<div class="screen-preview-nav">
<button class="screen-preview-btn" @click="prevScreen" :disabled="currentScreenIndex === 0" aria-label="上一张">
<i class="fas fa-chevron-left"></i>
</button>
<button class="screen-preview-btn" @click="nextScreen" :disabled="currentScreenIndex === screenshots.length - 1" aria-label="下一张">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<button class="screen-preview-btn close-preview" @click="closePreview" aria-label="关闭">
<i class="fas fa-times"></i>
</button>
</div>
<img :src="currentScreenshot" alt="应用截图预览">
<div class="screen-preview-counter">{{ previewCounterText }}</div>
</div>
</div>
</template>
<script setup>
import { computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
show: {
type: Boolean,
required: true
},
screenshots: {
type: Array,
required: true
},
currentScreenIndex: {
type: Number,
required: true
}
});
const emit = defineEmits(['close', 'prev', 'next']);
const currentScreenshot = computed(() => {
return props.screenshots[props.currentScreenIndex] || '';
});
const previewCounterText = computed(() => {
return `${props.currentScreenIndex + 1} / ${props.screenshots.length}`;
});
const closePreview = () => {
emit('close');
};
const prevScreen = () => {
emit('prev');
};
const nextScreen = () => {
emit('next');
};
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="theme-toggle-container">
<span class="theme-label">主题切换</span>
<label class="theme-toggle">
<input type="checkbox" :checked="isDark" @change="toggle">
<span class="theme-slider">
<i class="fas fa-sun"></i>
<i class="fas fa-moon"></i>
</span>
</label>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
isDark: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['toggle']);
const toggle = () => {
emit('toggle');
};
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="top-actions">
<button class="apm-btn" @click="handleUpdate" title="启动 apm-update-tool">
<i class="fas fa-sync-alt"></i> 软件更新
</button>
<button class="apm-btn" @click="handleList" title="启动 apm-installer --list">
<i class="fas fa-download"></i> 应用管理
</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['update', 'list']);
const handleUpdate = () => {
emit('update');
};
const handleList = () => {
emit('list');
};
</script>
<style scoped>
/* 该组件样式已在全局样式中定义 */
</style>