mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
add store
This commit is contained in:
57
src/components/AppCard.vue
Normal file
57
src/components/AppCard.vue
Normal 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>
|
||||
122
src/components/AppDetailModal.vue
Normal file
122
src/components/AppDetailModal.vue
Normal 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>
|
||||
43
src/components/AppGrid.vue
Normal file
43
src/components/AppGrid.vue
Normal 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>
|
||||
52
src/components/AppHeader.vue
Normal file
52
src/components/AppHeader.vue
Normal 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>
|
||||
53
src/components/AppSidebar.vue
Normal file
53
src/components/AppSidebar.vue
Normal 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>
|
||||
@@ -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>
|
||||
66
src/components/ScreenPreview.vue
Normal file
66
src/components/ScreenPreview.vue
Normal 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>
|
||||
33
src/components/ThemeToggle.vue
Normal file
33
src/components/ThemeToggle.vue
Normal 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>
|
||||
28
src/components/TopActions.vue
Normal file
28
src/components/TopActions.vue
Normal 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>
|
||||
Reference in New Issue
Block a user