mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
feat(sync): add installed app cloud sync
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="duration-200 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-3xl max-h-[85vh] flex-col rounded-3xl border border-white/10 bg-white/95 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<div
|
||||
class="flex items-start justify-between border-b border-slate-200/70 p-6 dark:border-slate-800/70"
|
||||
>
|
||||
<div>
|
||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
从账号恢复
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
选择云端已同步的应用加入安装队列
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
aria-label="关闭"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||
>
|
||||
正在读取云端应用列表…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-4 py-6 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="items.length === 0"
|
||||
class="rounded-2xl border border-slate-200/70 px-4 py-10 text-center text-slate-500 dark:border-slate-800/70 dark:text-slate-400"
|
||||
>
|
||||
云端暂无已同步应用
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<label
|
||||
v-for="item in items"
|
||||
:key="cloudItemKey(item)"
|
||||
class="flex items-center gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm dark:border-slate-800/70 dark:bg-slate-900/70"
|
||||
:class="isInstalled(item) ? 'opacity-60' : 'cursor-pointer'"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-slate-300 text-brand focus:ring-brand"
|
||||
:aria-label="item.appName || item.pkgname"
|
||||
:checked="selectedKeys.has(cloudItemKey(item))"
|
||||
:disabled="isInstalled(item)"
|
||||
@change="toggleSelection(item)"
|
||||
/>
|
||||
<img
|
||||
v-if="item.iconUrl"
|
||||
:src="item.iconUrl"
|
||||
class="h-10 w-10 rounded-xl object-contain"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-400 dark:bg-slate-800"
|
||||
>
|
||||
<i class="fas fa-cube"></i>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-semibold text-slate-900 dark:text-white">
|
||||
{{ item.appName || item.pkgname }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ item.origin }} · {{ item.category }} · {{ item.pkgname }} ·
|
||||
{{ item.version }} · {{ item.packageArch }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
v-if="isInstalled(item)"
|
||||
class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300"
|
||||
>
|
||||
已安装
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 border-t border-slate-200/70 p-6 dark:border-slate-800/70"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
@click="emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-2xl bg-brand px-5 py-2 text-sm font-semibold text-white transition hover:bg-brand/90 disabled:opacity-40"
|
||||
:disabled="selectedItems.length === 0"
|
||||
@click="emit('install-selected', selectedItems)"
|
||||
>
|
||||
加入安装队列
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import type { SyncedAppListItem } from "@/global/typedefinition";
|
||||
import { cloudItemKey } from "@/modules/appListSync";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
loading: boolean;
|
||||
error: string;
|
||||
items: SyncedAppListItem[];
|
||||
installedKeys: Set<string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "install-selected", items: SyncedAppListItem[]): void;
|
||||
}>();
|
||||
|
||||
const selectedKeys = ref<Set<string>>(new Set());
|
||||
|
||||
const isInstalled = (item: SyncedAppListItem): boolean =>
|
||||
props.installedKeys.has(cloudItemKey(item));
|
||||
|
||||
const selectedItems = computed(() =>
|
||||
props.items.filter((item) => selectedKeys.value.has(cloudItemKey(item))),
|
||||
);
|
||||
|
||||
const toggleSelection = (item: SyncedAppListItem): void => {
|
||||
if (isInstalled(item)) return;
|
||||
const key = cloudItemKey(item);
|
||||
const next = new Set(selectedKeys.value);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
selectedKeys.value = next;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.show, props.items] as const,
|
||||
() => {
|
||||
selectedKeys.value = new Set();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
Reference in New Issue
Block a user