mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
refactor: improve code formatting and consistency across components
- Updated button and span elements in ThemeToggle.vue and TopActions.vue for better readability. - Enhanced UninstallConfirmModal.vue and UpdateAppsModal.vue with consistent indentation and spacing. - Refactored downloadStatus.ts and storeConfig.ts for improved code clarity. - Standardized string quotes and spacing in typedefinition.ts and processInstall.ts. - Ensured consistent use of arrow functions and improved variable declarations throughout the codebase.
This commit is contained in:
@@ -1,47 +1,67 @@
|
||||
<template>
|
||||
<div @click="openDetail"
|
||||
class="group flex h-full cursor-pointer gap-4 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm transition hover:-translate-y-1 hover:border-brand/50 hover:shadow-lg dark:border-slate-800/60 dark:bg-slate-900/60">
|
||||
<div
|
||||
@click="openDetail"
|
||||
class="group flex h-full cursor-pointer gap-4 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm transition hover:-translate-y-1 hover:border-brand/50 hover:shadow-lg dark:border-slate-800/60 dark:bg-slate-900/60"
|
||||
>
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700">
|
||||
<img ref="iconImg" :src="loadedIcon" alt="icon"
|
||||
:class="['h-full w-full object-cover transition-opacity duration-300', isLoaded ? 'opacity-100' : 'opacity-0']" />
|
||||
class="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700"
|
||||
>
|
||||
<img
|
||||
ref="iconImg"
|
||||
:src="loadedIcon"
|
||||
alt="icon"
|
||||
:class="[
|
||||
'h-full w-full object-cover transition-opacity duration-300',
|
||||
isLoaded ? 'opacity-100' : 'opacity-0',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-1 overflow-hidden">
|
||||
<div class="truncate text-base font-semibold text-slate-900 dark:text-white">{{ app.name || '' }}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">{{ app.pkgname || '' }} · {{ app.version || '' }}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">{{ description }}</div>
|
||||
<div
|
||||
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ app.name || "" }}
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { APM_STORE_BASE_URL } from '../global/storeConfig';
|
||||
import type { App } from '../global/typedefinition';
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||
import type { App } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
app: App
|
||||
const props = defineProps<{
|
||||
app: App;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-detail', app: App): void
|
||||
(e: "open-detail", app: App): void;
|
||||
}>();
|
||||
|
||||
const iconImg = ref<HTMLImageElement | null>(null);
|
||||
const isLoaded = ref(false);
|
||||
const loadedIcon = ref('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E');
|
||||
const loadedIcon = ref(
|
||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E',
|
||||
);
|
||||
|
||||
const iconPath = computed(() => {
|
||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${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 more = props.app.more || "";
|
||||
return more.substring(0, 80) + (more.length > 80 ? "..." : "");
|
||||
});
|
||||
|
||||
const openDetail = () => {
|
||||
emit('open-detail', props.app);
|
||||
emit("open-detail", props.app);
|
||||
};
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
@@ -57,24 +77,23 @@ onMounted(() => {
|
||||
img.onload = () => {
|
||||
loadedIcon.value = iconPath.value;
|
||||
isLoaded.value = true;
|
||||
if (observer)
|
||||
observer.unobserve(entry.target);
|
||||
if (observer) observer.unobserve(entry.target);
|
||||
};
|
||||
img.onerror = () => {
|
||||
// 加载失败时使用默认图标
|
||||
loadedIcon.value = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23e0e0e0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3ENo Icon%3C/text%3E%3C/svg%3E';
|
||||
loadedIcon.value =
|
||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23e0e0e0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3ENo Icon%3C/text%3E%3C/svg%3E';
|
||||
isLoaded.value = true;
|
||||
if (observer)
|
||||
observer.unobserve(entry.target);
|
||||
if (observer) observer.unobserve(entry.target);
|
||||
};
|
||||
img.src = iconPath.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '50px', // 提前50px开始加载
|
||||
threshold: 0.01
|
||||
}
|
||||
rootMargin: "50px", // 提前50px开始加载
|
||||
threshold: 0.01,
|
||||
},
|
||||
);
|
||||
|
||||
// 观察图标元素
|
||||
@@ -85,7 +104,8 @@ onMounted(() => {
|
||||
|
||||
// 当 app 变更时重置懒加载状态并重新观察
|
||||
watch(iconPath, () => {
|
||||
loadedIcon.value = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E';
|
||||
loadedIcon.value =
|
||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E';
|
||||
isLoaded.value = false;
|
||||
if (observer && iconImg.value) {
|
||||
observer.unobserve(iconImg.value);
|
||||
|
||||
@@ -1,107 +1,197 @@
|
||||
<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" v-bind="attrs"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/70 p-4"
|
||||
@click.self="closeModal">
|
||||
<div class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
||||
<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"
|
||||
v-bind="attrs"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/70 p-4"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<div
|
||||
class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
<div
|
||||
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700">
|
||||
<img v-if="app" :src="iconPath" alt="icon" class="h-full w-full object-cover" />
|
||||
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700"
|
||||
>
|
||||
<img
|
||||
v-if="app"
|
||||
:src="iconPath"
|
||||
alt="icon"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white">{{ app?.name || '' }}</p>
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{{ app?.name || "" }}
|
||||
</p>
|
||||
<!-- Close button for mobile layout could be considered here if needed, but for now sticking to desktop layout logic mainly -->
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">{{ app?.pkgname || '' }} · {{ app?.version || '' }}</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ app?.pkgname || "" }} · {{ app?.version || "" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 lg:ml-auto">
|
||||
<button v-if="!isinstalled" type="button"
|
||||
<button
|
||||
v-if="!isinstalled"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40 transition hover:-translate-y-0.5"
|
||||
:class="installFeedback ? 'from-emerald-500 to-emerald-600' : 'from-brand to-brand-dark'"
|
||||
:class="
|
||||
installFeedback
|
||||
? 'from-emerald-500 to-emerald-600'
|
||||
: 'from-brand to-brand-dark'
|
||||
"
|
||||
@click="handleInstall"
|
||||
:disabled="installFeedback || isCompleted">
|
||||
<i class="fas" :class="installFeedback ? 'fa-check' : 'fa-download'"></i>
|
||||
:disabled="installFeedback || isCompleted"
|
||||
>
|
||||
<i
|
||||
class="fas"
|
||||
:class="installFeedback ? 'fa-check' : 'fa-download'"
|
||||
></i>
|
||||
<span>{{ installBtnText }}</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5"
|
||||
@click="emit('open-app', app?.pkgname || '')">
|
||||
@click="emit('open-app', app?.pkgname || '')"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<span>打开</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-rose-500 to-rose-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 disabled:opacity-40 transition hover:-translate-y-0.5"
|
||||
@click="handleRemove">
|
||||
@click="handleRemove"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
<span>卸载</span>
|
||||
</button>
|
||||
</template>
|
||||
<button type="button"
|
||||
<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"
|
||||
@click="closeModal" aria-label="关闭">
|
||||
@click="closeModal"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3 text-sm text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300">
|
||||
class="mt-4 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3 text-sm text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
|
||||
>
|
||||
首次安装 APM 后需要重启系统以在启动器中看到应用入口。可前往
|
||||
<a href="https://gitee.com/amber-ce/amber-pm/releases" target="_blank"
|
||||
class="font-semibold text-brand hover:underline">APM Releases</a>
|
||||
<a
|
||||
href="https://gitee.com/amber-ce/amber-pm/releases"
|
||||
target="_blank"
|
||||
class="font-semibold text-brand hover:underline"
|
||||
>APM Releases</a
|
||||
>
|
||||
获取 APM。
|
||||
</div>
|
||||
|
||||
<div v-if="screenshots.length" class="mt-6 grid gap-3 sm:grid-cols-2">
|
||||
<img v-for="(screen, index) in screenshots" :key="index" :src="screen" alt="screenshot"
|
||||
<img
|
||||
v-for="(screen, index) in screenshots"
|
||||
:key="index"
|
||||
:src="screen"
|
||||
alt="screenshot"
|
||||
class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60"
|
||||
@click="openPreview(index)" @error="hideImage" />
|
||||
@click="openPreview(index)"
|
||||
@error="hideImage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
<div v-if="app?.author" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.author"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">作者</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.author }}</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.author }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="app?.contributor" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.contributor"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">贡献者</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.contributor }}</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.contributor }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="app?.size" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.size"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">大小</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.size }}</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.size }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="app?.update" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">更新时间</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.update }}</p>
|
||||
<div
|
||||
v-if="app?.update"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">
|
||||
更新时间
|
||||
</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.update }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="app?.website" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.website"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">网站</p>
|
||||
<a :href="app.website" target="_blank"
|
||||
class="text-sm font-medium text-brand hover:underline">{{ app.website }}</a>
|
||||
<a
|
||||
:href="app.website"
|
||||
target="_blank"
|
||||
class="text-sm font-medium text-brand hover:underline"
|
||||
>{{ app.website }}</a
|
||||
>
|
||||
</div>
|
||||
<div v-if="app?.version" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.version"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">版本</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.version }}</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.version }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="app?.tags" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="app?.tags"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-slate-400">标签</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.tags }}</p>
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||
{{ app.tags }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="app?.more && app.more.trim() !== ''" class="mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">应用详情</h3>
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
应用详情
|
||||
</h3>
|
||||
<div
|
||||
class="max-h-60 space-y-2 overflow-y-auto rounded-2xl border border-slate-200/60 bg-slate-50/80 p-4 text-sm leading-relaxed text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
|
||||
v-html="app.more.replace(/\n/g, '<br>')"></div>
|
||||
v-html="app.more.replace(/\n/g, '<br>')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,10 +199,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed,useAttrs } from 'vue';
|
||||
import { useDownloadItemStatus,useInstallFeedback } from '../global/downloadStatus';
|
||||
import { APM_STORE_BASE_URL } from '../global/storeConfig';
|
||||
import type { App } from '../global/typedefinition';
|
||||
import { computed, useAttrs } from "vue";
|
||||
import {
|
||||
useDownloadItemStatus,
|
||||
useInstallFeedback,
|
||||
} from "../global/downloadStatus";
|
||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||
import type { App } from "../global/typedefinition";
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
@@ -124,46 +217,45 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'install'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'open-preview', index: number): void;
|
||||
(e: 'open-app', pkgname: string ): void;
|
||||
(e: "close"): void;
|
||||
(e: "install"): void;
|
||||
(e: "remove"): void;
|
||||
(e: "open-preview", index: number): void;
|
||||
(e: "open-app", pkgname: string): void;
|
||||
}>();
|
||||
|
||||
|
||||
const appPkgname = computed(() => props.app?.pkgname);
|
||||
const { installFeedback } = useInstallFeedback(appPkgname);
|
||||
const { isCompleted } = useDownloadItemStatus(appPkgname);
|
||||
const installBtnText = computed(() => {
|
||||
if (isCompleted.value) {
|
||||
// TODO: 似乎有一个时间差,安装好了之后并不是立马就可以从已安装列表看见
|
||||
// TODO: 似乎有一个时间差,安装好了之后并不是立马就可以从已安装列表看见
|
||||
return "已安装";
|
||||
}
|
||||
return installFeedback.value ? "已加入队列" : "安装";
|
||||
});
|
||||
const iconPath = computed(() => {
|
||||
if (!props.app) return '';
|
||||
if (!props.app) return "";
|
||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleInstall = () => {
|
||||
emit('install');
|
||||
emit("install");
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
emit('remove');
|
||||
}
|
||||
emit("remove");
|
||||
};
|
||||
|
||||
const openPreview = (index: number) => {
|
||||
emit('open-preview', index);
|
||||
emit("open-preview", index);
|
||||
};
|
||||
|
||||
const hideImage = (e: Event) => {
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
(e.target as HTMLElement).style.display = "none";
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
<template>
|
||||
<div v-if="!loading" class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<AppCard v-for="(app, index) in apps" :key="index" :app="app" @open-detail="$emit('open-detail', app)" />
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||
>
|
||||
<AppCard
|
||||
v-for="(app, index) in apps"
|
||||
:key="index"
|
||||
:app="app"
|
||||
@open-detail="$emit('open-detail', app)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<div v-for="n in 8" :key="n"
|
||||
class="flex gap-4 rounded-2xl border border-slate-200/60 bg-white/80 p-4 shadow-sm dark:border-slate-800/60 dark:bg-slate-900/50">
|
||||
<div class="h-16 w-16 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-800"></div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||
>
|
||||
<div
|
||||
v-for="n in 8"
|
||||
:key="n"
|
||||
class="flex gap-4 rounded-2xl border border-slate-200/60 bg-white/80 p-4 shadow-sm dark:border-slate-800/60 dark:bg-slate-900/50"
|
||||
>
|
||||
<div
|
||||
class="h-16 w-16 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-800"
|
||||
></div>
|
||||
<div class="flex flex-1 flex-col justify-center gap-2">
|
||||
<div class="h-4 w-2/3 animate-pulse rounded-full bg-slate-200 dark:bg-slate-800"></div>
|
||||
<div class="h-3 w-1/2 animate-pulse rounded-full bg-slate-200/80 dark:bg-slate-800/80"></div>
|
||||
<div
|
||||
class="h-4 w-2/3 animate-pulse rounded-full bg-slate-200 dark:bg-slate-800"
|
||||
></div>
|
||||
<div
|
||||
class="h-3 w-1/2 animate-pulse rounded-full bg-slate-200/80 dark:bg-slate-800/80"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppCard from './AppCard.vue';
|
||||
import type { App } from '../global/typedefinition';
|
||||
import AppCard from "./AppCard.vue";
|
||||
import type { App } from "../global/typedefinition";
|
||||
|
||||
defineProps<{
|
||||
apps: App[];
|
||||
@@ -24,6 +44,6 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open-detail', app: App): void;
|
||||
(e: "open-detail", app: App): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -5,10 +5,16 @@
|
||||
<div class="w-full flex-1">
|
||||
<label for="searchBox" class="sr-only">搜索应用</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input id="searchBox" v-model="localSearchQuery"
|
||||
<i
|
||||
class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
></i>
|
||||
<input
|
||||
id="searchBox"
|
||||
v-model="localSearchQuery"
|
||||
class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-4 text-sm text-slate-700 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/50 focus:ring-4 focus:ring-brand/10 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-200"
|
||||
placeholder="搜索应用名 / 包名 / 标签" @input="debounceSearch" />
|
||||
placeholder="搜索应用名 / 包名 / 标签"
|
||||
@input="debounceSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,8 +25,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import TopActions from './TopActions.vue';
|
||||
import { ref, watch } from "vue";
|
||||
import TopActions from "./TopActions.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
searchQuery: string;
|
||||
@@ -28,22 +34,25 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update-search', query: string): void;
|
||||
(e: 'update'): void;
|
||||
(e: 'list'): void;
|
||||
(e: "update-search", query: string): void;
|
||||
(e: "update"): void;
|
||||
(e: "list"): void;
|
||||
}>();
|
||||
|
||||
const localSearchQuery = ref(props.searchQuery || '');
|
||||
const localSearchQuery = ref(props.searchQuery || "");
|
||||
const timeoutId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const debounceSearch = () => {
|
||||
if (timeoutId.value) clearTimeout(timeoutId.value);
|
||||
timeoutId.value = setTimeout(() => {
|
||||
emit('update-search', localSearchQuery.value);
|
||||
emit("update-search", localSearchQuery.value);
|
||||
}, 220);
|
||||
};
|
||||
|
||||
watch(() => props.searchQuery, (newVal) => {
|
||||
localSearchQuery.value = newVal || '';
|
||||
});
|
||||
watch(
|
||||
() => props.searchQuery,
|
||||
(newVal) => {
|
||||
localSearchQuery.value = newVal || "";
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,41 +1,71 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="amberLogo" alt="Amber PM" class="h-11 w-11 rounded-2xl bg-white/70 p-2 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-800" />
|
||||
<img
|
||||
:src="amberLogo"
|
||||
alt="Amber PM"
|
||||
class="h-11 w-11 rounded-2xl bg-white/70 p-2 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-800"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400">APM Store</span>
|
||||
<span class="text-lg font-semibold text-slate-900 dark:text-white">APM 客户端商店</span>
|
||||
<span
|
||||
class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400"
|
||||
>APM Store</span
|
||||
>
|
||||
<span class="text-lg font-semibold text-slate-900 dark:text-white"
|
||||
>APM 客户端商店</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ThemeToggle :is-dark="isDarkTheme" @toggle="toggleTheme" />
|
||||
|
||||
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
|
||||
<button type="button" class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="activeCategory === 'all' ? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15' : ''"
|
||||
@click="selectCategory('all')">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="
|
||||
activeCategory === 'all'
|
||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||
: ''
|
||||
"
|
||||
@click="selectCategory('all')"
|
||||
>
|
||||
<span>全部应用</span>
|
||||
<span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">{{ categoryCounts.all || 0 }}</span>
|
||||
<span
|
||||
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||
>{{ categoryCounts.all || 0 }}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button v-for="(category, key) in categories" :key="key" type="button"
|
||||
<button
|
||||
v-for="(category, key) in categories"
|
||||
:key="key"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
:class="activeCategory === key ? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15' : ''"
|
||||
@click="selectCategory(key)">
|
||||
:class="
|
||||
activeCategory === key
|
||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||
: ''
|
||||
"
|
||||
@click="selectCategory(key)"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>
|
||||
<div class="text-left">{{ category.zh }}</div>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">{{ categoryCounts[key] || 0 }}</span>
|
||||
<span
|
||||
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||
>{{ categoryCounts[key] || 0 }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ThemeToggle from './ThemeToggle.vue';
|
||||
import amberLogo from '../assets/imgs/amber-pm-logo.png';
|
||||
import ThemeToggle from "./ThemeToggle.vue";
|
||||
import amberLogo from "../assets/imgs/amber-pm-logo.png";
|
||||
|
||||
defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -46,15 +76,15 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-theme'): void;
|
||||
(e: 'select-category', category: string): void;
|
||||
(e: "toggle-theme"): void;
|
||||
(e: "select-category", category: string): void;
|
||||
}>();
|
||||
|
||||
const toggleTheme = () => {
|
||||
emit('toggle-theme');
|
||||
emit("toggle-theme");
|
||||
};
|
||||
|
||||
const selectCategory = (category: string) => {
|
||||
emit('select-category', category);
|
||||
emit("select-category", category);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,72 +1,123 @@
|
||||
<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"
|
||||
<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="handleOverlayClick">
|
||||
<div class="scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
@click.stop>
|
||||
@click="handleOverlayClick"
|
||||
>
|
||||
<div
|
||||
class="scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<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>
|
||||
<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"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/60 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
||||
@click="close">
|
||||
@click="close"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="download" class="mt-6 space-y-6">
|
||||
<div class="flex items-center gap-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div class="h-16 w-16 overflow-hidden rounded-2xl bg-slate-100 dark:bg-slate-800">
|
||||
<img :src="download.icon" :alt="download.name" class="h-full w-full object-cover" />
|
||||
<div
|
||||
class="flex items-center gap-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<div
|
||||
class="h-16 w-16 overflow-hidden rounded-2xl bg-slate-100 dark:bg-slate-800"
|
||||
>
|
||||
<img
|
||||
:src="download.icon"
|
||||
:alt="download.name"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ download.name }}</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">{{ download.pkgname }} · {{ download.version }}</p>
|
||||
<p class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{{ download.name }}
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ download.pkgname }} · {{ download.version }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
class="space-y-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-slate-500">状态</span>
|
||||
<span
|
||||
class="rounded-full px-3 py-1 text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-700': download.status === 'downloading',
|
||||
'bg-amber-100 text-amber-600': download.status === 'installing',
|
||||
'bg-emerald-100 text-emerald-700': download.status === 'completed',
|
||||
'bg-blue-100 text-blue-700':
|
||||
download.status === 'downloading',
|
||||
'bg-amber-100 text-amber-600':
|
||||
download.status === 'installing',
|
||||
'bg-emerald-100 text-emerald-700':
|
||||
download.status === 'completed',
|
||||
'bg-rose-100 text-rose-600': download.status === 'failed',
|
||||
'bg-slate-200 text-slate-600': download.status === 'paused'
|
||||
}">
|
||||
'bg-slate-200 text-slate-600': download.status === 'paused',
|
||||
}"
|
||||
>
|
||||
{{ getStatusText(download.status) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="download.status === 'downloading'" class="space-y-3">
|
||||
<div class="h-2 rounded-full bg-slate-100 dark:bg-slate-800">
|
||||
<div class="h-full rounded-full bg-brand" :style="{ width: download.progress + '%' }"></div>
|
||||
<div
|
||||
class="h-full rounded-full bg-brand"
|
||||
:style="{ width: download.progress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between text-sm text-slate-500 dark:text-slate-400">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between text-sm text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
<span>{{ download.progress }}%</span>
|
||||
<span v-if="download.downloadedSize && download.totalSize">
|
||||
{{ formatSize(download.downloadedSize) }} / {{ formatSize(download.totalSize) }}
|
||||
{{ formatSize(download.downloadedSize) }} /
|
||||
{{ formatSize(download.totalSize) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="download.speed" class="flex flex-wrap gap-3 text-sm text-slate-500 dark:text-slate-300">
|
||||
<span class="flex items-center gap-2"><i class="fas fa-tachometer-alt"></i>{{ formatSpeed(download.speed) }}</span>
|
||||
<span v-if="download.timeRemaining">剩余 {{ formatTime(download.timeRemaining) }}</span>
|
||||
<div
|
||||
v-if="download.speed"
|
||||
class="flex flex-wrap gap-3 text-sm text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
<span class="flex items-center gap-2"
|
||||
><i class="fas fa-tachometer-alt"></i
|
||||
>{{ formatSpeed(download.speed) }}</span
|
||||
>
|
||||
<span v-if="download.timeRemaining"
|
||||
>剩余 {{ formatTime(download.timeRemaining) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200/60 p-4 text-sm text-slate-600 dark:border-slate-800/60 dark:text-slate-300">
|
||||
<div
|
||||
class="rounded-2xl border border-slate-200/60 p-4 text-sm text-slate-600 dark:border-slate-800/60 dark:text-slate-300"
|
||||
>
|
||||
<div class="flex justify-between py-1">
|
||||
<span class="text-slate-400">下载源</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ download.source || 'APM Store' }}</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{
|
||||
download.source || "APM Store"
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="download.startTime" class="flex justify-between py-1">
|
||||
<span class="text-slate-400">开始时间</span>
|
||||
@@ -76,40 +127,64 @@
|
||||
<span class="text-slate-400">完成时间</span>
|
||||
<span>{{ formatDate(download.endTime) }}</span>
|
||||
</div>
|
||||
<div v-if="download.error" class="flex justify-between py-1 text-rose-500">
|
||||
<div
|
||||
v-if="download.error"
|
||||
class="flex justify-between py-1 text-rose-500"
|
||||
>
|
||||
<span>错误信息</span>
|
||||
<span class="text-right">{{ download.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="download.logs && download.logs.length" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
||||
<div
|
||||
v-if="download.logs && download.logs.length"
|
||||
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="font-semibold text-slate-800 dark:text-slate-100">下载日志</span>
|
||||
<button type="button"
|
||||
<span class="font-semibold text-slate-800 dark:text-slate-100"
|
||||
>下载日志</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300"
|
||||
@click="copyLogs">
|
||||
@click="copyLogs"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
复制日志
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-48 space-y-2 overflow-y-auto rounded-2xl bg-slate-50/80 p-3 font-mono text-xs text-slate-600 dark:bg-slate-900/60 dark:text-slate-300">
|
||||
<div v-for="(log, index) in download.logs" :key="index" class="flex gap-3">
|
||||
<span class="text-slate-400">{{ formatLogTime(log.time) }}</span>
|
||||
<div
|
||||
class="max-h-48 space-y-2 overflow-y-auto rounded-2xl bg-slate-50/80 p-3 font-mono text-xs text-slate-600 dark:bg-slate-900/60 dark:text-slate-300"
|
||||
>
|
||||
<div
|
||||
v-for="(log, index) in download.logs"
|
||||
:key="index"
|
||||
class="flex gap-3"
|
||||
>
|
||||
<span class="text-slate-400">{{
|
||||
formatLogTime(log.time)
|
||||
}}</span>
|
||||
<span>{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-3">
|
||||
<button v-if="download.status === 'failed'" type="button"
|
||||
<button
|
||||
v-if="download.status === 'failed'"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-500 transition hover:bg-rose-50"
|
||||
@click="retry">
|
||||
@click="retry"
|
||||
>
|
||||
<i class="fas fa-redo"></i>
|
||||
重试下载
|
||||
</button>
|
||||
<button v-if="download.status === 'completed'" type="button"
|
||||
<button
|
||||
v-if="download.status === 'completed'"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg"
|
||||
@click="openApp">
|
||||
@click="openApp"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
打开应用
|
||||
</button>
|
||||
@@ -121,7 +196,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DownloadItem } from '../global/typedefinition';
|
||||
import type { DownloadItem } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
@@ -129,17 +204,16 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'pause', download: DownloadItem): void;
|
||||
(e: 'resume', download: DownloadItem): void;
|
||||
(e: 'cancel', download: DownloadItem): void;
|
||||
(e: 'retry', download: DownloadItem): void;
|
||||
(e: 'open-app', download: string): void;
|
||||
(e: "close"): void;
|
||||
(e: "pause", download: DownloadItem): void;
|
||||
(e: "resume", download: DownloadItem): void;
|
||||
(e: "cancel", download: DownloadItem): void;
|
||||
(e: "retry", download: DownloadItem): void;
|
||||
(e: "open-app", download: string): void;
|
||||
}>();
|
||||
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
@@ -148,38 +222,38 @@ const handleOverlayClick = () => {
|
||||
|
||||
const retry = () => {
|
||||
if (props.download) {
|
||||
emit('retry', props.download);
|
||||
emit("retry", props.download);
|
||||
}
|
||||
};
|
||||
|
||||
const openApp = () => {
|
||||
if (props.download) {
|
||||
emit('open-app', props.download.pkgname);
|
||||
emit("open-app", props.download.pkgname);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': '等待中',
|
||||
'downloading': '下载中',
|
||||
'installing': '安装中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败',
|
||||
'paused': '已暂停',
|
||||
'cancelled': '已取消'
|
||||
pending: "等待中",
|
||||
downloading: "下载中",
|
||||
installing: "安装中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
paused: "已暂停",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
if (!bytes) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
||||
};
|
||||
|
||||
const formatSpeed = (bytesPerSecond: number) => {
|
||||
return formatSize(bytesPerSecond) + '/s';
|
||||
return formatSize(bytesPerSecond) + "/s";
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
@@ -190,23 +264,26 @@ const formatTime = (seconds: number) => {
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
return date.toLocaleString("zh-CN");
|
||||
};
|
||||
|
||||
const formatLogTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('zh-CN');
|
||||
return date.toLocaleTimeString("zh-CN");
|
||||
};
|
||||
|
||||
const copyLogs = () => {
|
||||
if (!props.download?.logs) return;
|
||||
const logsText = props.download.logs
|
||||
.map(log => `[${formatLogTime(log.time)}] ${log.message}`)
|
||||
.join('\n');
|
||||
navigator.clipboard?.writeText(logsText).then(() => {
|
||||
alert('日志已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
prompt('请手动复制日志:', logsText);
|
||||
});
|
||||
.map((log) => `[${formatLogTime(log.time)}] ${log.message}`)
|
||||
.join("\n");
|
||||
navigator.clipboard
|
||||
?.writeText(logsText)
|
||||
.then(() => {
|
||||
alert("日志已复制到剪贴板");
|
||||
})
|
||||
.catch(() => {
|
||||
prompt("请手动复制日志:", logsText);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,65 +1,121 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white/95 shadow-2xl backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/90 sm:left-auto sm:right-6 sm:w-96">
|
||||
<div class="flex items-center justify-between px-5 py-4" @click="toggleExpand">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-200">
|
||||
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white/95 shadow-2xl backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/90 sm:left-auto sm:right-6 sm:w-96"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-5 py-4"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
|
||||
>
|
||||
<i class="fas fa-download text-brand"></i>
|
||||
<span>下载队列</span>
|
||||
<span v-if="downloads.length"
|
||||
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">
|
||||
<span
|
||||
v-if="downloads.length"
|
||||
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||
>
|
||||
({{ activeDownloads }}/{{ downloads.length }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="downloads.length" type="button"
|
||||
<button
|
||||
v-if="downloads.length"
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
||||
title="清除已完成" @click.stop="clearCompleted">
|
||||
title="清除已完成"
|
||||
@click.stop="clearCompleted"
|
||||
>
|
||||
<i class="fas fa-broom"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
||||
@click.stop="toggleExpand">
|
||||
<i class="fas" :class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
|
||||
@click.stop="toggleExpand"
|
||||
>
|
||||
<i
|
||||
class="fas"
|
||||
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0" leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 -translate-y-2">
|
||||
<Transition
|
||||
enter-active-class="duration-200 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-show="isExpanded" class="max-h-96 overflow-y-auto px-3 pb-4">
|
||||
<div v-if="downloads.length === 0"
|
||||
class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200/80 px-4 py-12 text-slate-500 dark:border-slate-800/80 dark:text-slate-400">
|
||||
<div
|
||||
v-if="downloads.length === 0"
|
||||
class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200/80 px-4 py-12 text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||
>
|
||||
<i class="fas fa-inbox text-3xl"></i>
|
||||
<p class="mt-3 text-sm">暂无下载任务</p>
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="download in downloads" :key="download.id"
|
||||
<div
|
||||
v-for="download in downloads"
|
||||
:key="download.id"
|
||||
class="flex cursor-pointer items-center gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-3 shadow-sm transition hover:border-brand/40 hover:shadow-lg dark:border-slate-800/70 dark:bg-slate-900"
|
||||
:class="download.status === 'failed' ? 'border-rose-300/70 dark:border-rose-500/40' : ''"
|
||||
@click="showDownloadDetail(download)">
|
||||
<div class="h-12 w-12 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800">
|
||||
<img :src="download.icon" :alt="download.name" class="h-full w-full object-cover" />
|
||||
:class="
|
||||
download.status === 'failed'
|
||||
? 'border-rose-300/70 dark:border-rose-500/40'
|
||||
: ''
|
||||
"
|
||||
@click="showDownloadDetail(download)"
|
||||
>
|
||||
<div
|
||||
class="h-12 w-12 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800"
|
||||
>
|
||||
<img
|
||||
:src="download.icon"
|
||||
:alt="download.name"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="truncate text-sm font-semibold text-slate-800 dark:text-slate-100">{{ download.name }}</p>
|
||||
<p
|
||||
class="truncate text-sm font-semibold text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ download.name }}
|
||||
</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span v-if="download.status === 'downloading'">下载中 {{ download.progress }}%</span>
|
||||
<span v-else-if="download.status === 'installing'">安装中...</span>
|
||||
<span v-if="download.status === 'downloading'"
|
||||
>下载中 {{ download.progress }}%</span
|
||||
>
|
||||
<span v-else-if="download.status === 'installing'"
|
||||
>安装中...</span
|
||||
>
|
||||
<span v-else-if="download.status === 'completed'">已完成</span>
|
||||
<span v-else-if="download.status === 'failed'">失败: {{ download.error }}</span>
|
||||
<span v-else-if="download.status === 'failed'"
|
||||
>失败: {{ download.error }}</span
|
||||
>
|
||||
<span v-else-if="download.status === 'paused'">已暂停</span>
|
||||
<span v-else>等待中...</span>
|
||||
</p>
|
||||
<div v-if="download.status === 'downloading'"
|
||||
class="mt-2 h-1.5 rounded-full bg-slate-100 dark:bg-slate-800">
|
||||
<div class="h-full rounded-full bg-brand" :style="{ width: `${download.progress}%` }"></div>
|
||||
<div
|
||||
v-if="download.status === 'downloading'"
|
||||
class="mt-2 h-1.5 rounded-full bg-slate-100 dark:bg-slate-800"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-brand"
|
||||
:style="{ width: `${download.progress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="download.status === 'failed'" type="button"
|
||||
<button
|
||||
v-if="download.status === 'failed'"
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-rose-200/60 text-rose-500 transition hover:bg-rose-50"
|
||||
title="重试" @click.stop="retryDownload(download)">
|
||||
title="重试"
|
||||
@click.stop="retryDownload(download)"
|
||||
>
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -71,28 +127,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import type { DownloadItem } from '../global/typedefinition';
|
||||
import { ref, computed } from "vue";
|
||||
import type { DownloadItem } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
downloads: DownloadItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'pause', download: DownloadItem): void;
|
||||
(e: 'resume', download: DownloadItem): void;
|
||||
(e: 'cancel', download: DownloadItem): void;
|
||||
(e: 'retry', download: DownloadItem): void;
|
||||
(e: 'clear-completed'): void;
|
||||
(e: 'show-detail', download: DownloadItem): void;
|
||||
(e: "pause", download: DownloadItem): void;
|
||||
(e: "resume", download: DownloadItem): void;
|
||||
(e: "cancel", download: DownloadItem): void;
|
||||
(e: "retry", download: DownloadItem): void;
|
||||
(e: "clear-completed"): void;
|
||||
(e: "show-detail", download: DownloadItem): void;
|
||||
}>();
|
||||
|
||||
|
||||
const isExpanded = ref(false);
|
||||
|
||||
const activeDownloads = computed(() => {
|
||||
return props.downloads.filter(d =>
|
||||
d.status === 'downloading' || d.status === 'installing'
|
||||
return props.downloads.filter(
|
||||
(d) => d.status === "downloading" || d.status === "installing",
|
||||
).length;
|
||||
});
|
||||
|
||||
@@ -101,14 +156,14 @@ const toggleExpand = () => {
|
||||
};
|
||||
|
||||
const retryDownload = (download: DownloadItem) => {
|
||||
emit('retry', download);
|
||||
emit("retry", download);
|
||||
};
|
||||
|
||||
const clearCompleted = () => {
|
||||
emit('clear-completed');
|
||||
emit("clear-completed");
|
||||
};
|
||||
|
||||
const showDownloadDetail = (download: DownloadItem) => {
|
||||
emit('show-detail', download);
|
||||
emit("show-detail", download);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,67 +1,108 @@
|
||||
<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">
|
||||
<div class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<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">来自本机 APM 安装列表</p>
|
||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
已安装应用
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
来自本机 APM 安装列表
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
||||
:disabled="loading" @click="$emit('refresh')">
|
||||
:disabled="loading"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button"
|
||||
<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"
|
||||
@click="$emit('close')" aria-label="关闭">
|
||||
@click="$emit('close')"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<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
|
||||
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">
|
||||
<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="apps.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
|
||||
v-else-if="apps.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">
|
||||
<div v-for="app in apps" :key="app.pkgname"
|
||||
class="flex flex-col 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 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div
|
||||
v-for="app in apps"
|
||||
:key="app.pkgname"
|
||||
class="flex flex-col 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 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-base font-semibold text-slate-900 dark:text-white">{{ app.pkgname }}</p>
|
||||
<span v-if="app.flags && app.flags.includes('automatic')"
|
||||
class="rounded-md bg-rose-100 px-2 py-0.5 text-[11px] font-semibold text-rose-600 dark:bg-rose-500/20 dark:text-rose-400">
|
||||
<p
|
||||
class="text-base font-semibold text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ app.pkgname }}
|
||||
</p>
|
||||
<span
|
||||
v-if="app.flags && app.flags.includes('automatic')"
|
||||
class="rounded-md bg-rose-100 px-2 py-0.5 text-[11px] font-semibold text-rose-600 dark:bg-rose-500/20 dark:text-rose-400"
|
||||
>
|
||||
依赖项
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<div
|
||||
class="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
<span>{{ app.version }}</span>
|
||||
<span>·</span>
|
||||
<span>{{ app.arch }}</span>
|
||||
<template v-if="app.flags && !app.flags.includes('automatic')">
|
||||
<template
|
||||
v-if="app.flags && !app.flags.includes('automatic')"
|
||||
>
|
||||
<span>·</span>
|
||||
<span>{{ app.flags }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-600 transition hover:bg-rose-50 disabled:opacity-50"
|
||||
:disabled="app.currentStatus === 'not-installed'" @click="$emit('uninstall', app)">
|
||||
:disabled="app.currentStatus === 'not-installed'"
|
||||
@click="$emit('uninstall', app)"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
卸载
|
||||
</button>
|
||||
@@ -74,7 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { App } from '../global/typedefinition';
|
||||
import { App } from "../global/typedefinition";
|
||||
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
@@ -84,9 +125,8 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'refresh'): void;
|
||||
(e: 'uninstall', app: App): void;
|
||||
(e: "close"): void;
|
||||
(e: "refresh"): void;
|
||||
(e: "uninstall", app: App): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,34 +1,62 @@
|
||||
<template>
|
||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100" leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100" leave-to-class="opacity-0">
|
||||
<div v-if="show"
|
||||
<Transition
|
||||
enter-active-class="duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-slate-900/80 px-4 py-10"
|
||||
@click.self="closePreview">
|
||||
@click.self="closePreview"
|
||||
>
|
||||
<div class="relative w-full max-w-5xl">
|
||||
<img :src="currentScreenshot" alt="应用截图预览"
|
||||
class="max-h-[80vh] w-full rounded-3xl border border-slate-200/40 bg-black/40 object-contain shadow-2xl dark:border-slate-700" />
|
||||
<div class="absolute inset-x-0 top-4 flex items-center justify-between px-6">
|
||||
<img
|
||||
:src="currentScreenshot"
|
||||
alt="应用截图预览"
|
||||
class="max-h-[80vh] w-full rounded-3xl border border-slate-200/40 bg-black/40 object-contain shadow-2xl dark:border-slate-700"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-x-0 top-4 flex items-center justify-between px-6"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
@click="prevScreen" :disabled="currentScreenIndex === 0" aria-label="上一张">
|
||||
@click="prevScreen"
|
||||
:disabled="currentScreenIndex === 0"
|
||||
aria-label="上一张"
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
@click="nextScreen" :disabled="currentScreenIndex === screenshots.length - 1" aria-label="下一张">
|
||||
@click="nextScreen"
|
||||
:disabled="currentScreenIndex === screenshots.length - 1"
|
||||
aria-label="下一张"
|
||||
>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white"
|
||||
@click="closePreview" aria-label="关闭">
|
||||
@click="closePreview"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="absolute inset-x-0 bottom-6 flex items-center justify-center">
|
||||
<span class="rounded-full bg-black/60 px-4 py-1 text-sm font-medium text-white">{{ previewCounterText }}</span>
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-6 flex items-center justify-center"
|
||||
>
|
||||
<span
|
||||
class="rounded-full bg-black/60 px-4 py-1 text-sm font-medium text-white"
|
||||
>{{ previewCounterText }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
@@ -45,13 +73,13 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'prev'): void;
|
||||
(e: 'next'): void;
|
||||
(e: "close"): void;
|
||||
(e: "prev"): void;
|
||||
(e: "next"): void;
|
||||
}>();
|
||||
|
||||
const currentScreenshot = computed(() => {
|
||||
return props.screenshots[props.currentScreenIndex] || '';
|
||||
return props.screenshots[props.currentScreenIndex] || "";
|
||||
});
|
||||
|
||||
const previewCounterText = computed(() => {
|
||||
@@ -59,14 +87,14 @@ const previewCounterText = computed(() => {
|
||||
});
|
||||
|
||||
const closePreview = () => {
|
||||
emit('close');
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const prevScreen = () => {
|
||||
emit('prev');
|
||||
emit("prev");
|
||||
};
|
||||
|
||||
const nextScreen = () => {
|
||||
emit('next');
|
||||
emit("next");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
<template>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-between rounded-2xl border border-slate-200/80 bg-white/70 px-4 py-3 text-sm font-medium text-slate-600 shadow-sm transition hover:border-brand/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-300"
|
||||
:aria-pressed="isDark" @click="toggle">
|
||||
:aria-pressed="isDark"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fas" :class="isDark ? 'fa-moon text-amber-200' : 'fa-sun text-amber-400'"></i>
|
||||
<span>{{ isDark ? '深色主题' : '浅色主题' }}</span>
|
||||
<i
|
||||
class="fas"
|
||||
:class="isDark ? 'fa-moon text-amber-200' : 'fa-sun text-amber-400'"
|
||||
></i>
|
||||
<span>{{ isDark ? "深色主题" : "浅色主题" }}</span>
|
||||
</span>
|
||||
<span class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700">
|
||||
<span :class="['inline-block h-4 w-4 rounded-full bg-white shadow transition', isDark ? 'translate-x-6' : 'translate-x-1']"></span>
|
||||
<span
|
||||
class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block h-4 w-4 rounded-full bg-white shadow transition',
|
||||
isDark ? 'translate-x-6' : 'translate-x-1',
|
||||
]"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -18,10 +31,10 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle'): void;
|
||||
(e: "toggle"): void;
|
||||
}>();
|
||||
|
||||
const toggle = () => {
|
||||
emit('toggle');
|
||||
emit("toggle");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
||||
@click="handleUpdate" title="启动 apm-update-tool">
|
||||
@click="handleUpdate"
|
||||
title="启动 apm-update-tool"
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
<span>软件更新</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-slate-900/90 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-slate-900/40 transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/40 dark:bg-white/90 dark:text-slate-900"
|
||||
@click="handleList" title="启动 apm-installer --list">
|
||||
@click="handleList"
|
||||
title="启动 apm-installer --list"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
<span>应用管理</span>
|
||||
</button>
|
||||
@@ -17,15 +23,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: 'update'): void;
|
||||
(e: 'list'): void;
|
||||
(e: "update"): void;
|
||||
(e: "list"): void;
|
||||
}>();
|
||||
|
||||
const handleUpdate = () => {
|
||||
emit('update');
|
||||
emit("update");
|
||||
};
|
||||
|
||||
const handleList = () => {
|
||||
emit('list');
|
||||
emit("list");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,64 +1,100 @@
|
||||
<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-center justify-center bg-slate-900/70 p-4"
|
||||
@click.self="handleClose">
|
||||
<div class="relative w-full max-w-lg overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
||||
|
||||
<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-center justify-center bg-slate-900/70 p-4"
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<div
|
||||
class="relative w-full max-w-lg overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-rose-100 to-rose-50 shadow-inner dark:from-rose-900/30 dark:to-rose-800/20">
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-rose-100 to-rose-50 shadow-inner dark:from-rose-900/30 dark:to-rose-800/20"
|
||||
>
|
||||
<i class="fas fa-trash-alt text-2xl text-rose-500"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">卸载应用</h3>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">
|
||||
卸载应用
|
||||
</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
您确定要卸载 <span class="font-semibold text-slate-700 dark:text-slate-200">{{ appName }}</span> 吗?
|
||||
您确定要卸载
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-200">{{
|
||||
appName
|
||||
}}</span>
|
||||
吗?
|
||||
</p>
|
||||
<p class="text-xs text-slate-400 mt-1">{{ appPkg }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output -->
|
||||
<div v-if="uninstalling || completed"
|
||||
class="mb-6 max-h-48 overflow-y-auto rounded-xl border border-slate-200/50 bg-slate-900 p-3 font-mono text-xs text-slate-300 shadow-inner scrollbar-muted dark:border-slate-700">
|
||||
<div v-for="(line, index) in logs" :key="index" class="whitespace-pre-wrap break-all">{{ line }}</div>
|
||||
<div
|
||||
v-if="uninstalling || completed"
|
||||
class="mb-6 max-h-48 overflow-y-auto rounded-xl border border-slate-200/50 bg-slate-900 p-3 font-mono text-xs text-slate-300 shadow-inner scrollbar-muted dark:border-slate-700"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in logs"
|
||||
:key="index"
|
||||
class="whitespace-pre-wrap break-all"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
<div ref="logEnd"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mb-4 rounded-xl border border-rose-200/50 bg-rose-50 p-3 text-sm text-rose-600 dark:border-rose-900/30 dark:bg-rose-900/20 dark:text-rose-400">
|
||||
<div
|
||||
v-if="error"
|
||||
class="mb-4 rounded-xl border border-rose-200/50 bg-rose-50 p-3 text-sm text-rose-600 dark:border-rose-900/30 dark:bg-rose-900/20 dark:text-rose-400"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button v-if="!uninstalling" type="button"
|
||||
<button
|
||||
v-if="!uninstalling"
|
||||
type="button"
|
||||
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||
@click="handleClose">
|
||||
@click="handleClose"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
|
||||
<button v-if="!uninstalling && !completed" type="button"
|
||||
|
||||
<button
|
||||
v-if="!uninstalling && !completed"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-rose-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 transition hover:bg-rose-600 hover:-translate-y-0.5"
|
||||
@click="confirmUninstall">
|
||||
@click="confirmUninstall"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
确认卸载
|
||||
</button>
|
||||
|
||||
<button v-if="completed" type="button"
|
||||
<button
|
||||
v-if="completed"
|
||||
type="button"
|
||||
class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
@click="handleFinish">
|
||||
@click="handleFinish"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, nextTick, onUnmounted } from 'vue';
|
||||
import type { App } from '../global/typedefinition';
|
||||
import { computed, ref, watch, nextTick, onUnmounted } from "vue";
|
||||
import type { App } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
@@ -66,55 +102,55 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'success'): void;
|
||||
(e: "close"): void;
|
||||
(e: "success"): void;
|
||||
}>();
|
||||
|
||||
const uninstalling = ref(false);
|
||||
const completed = ref(false);
|
||||
const logs = ref<string[]>([]);
|
||||
const error = ref('');
|
||||
const error = ref("");
|
||||
const logEnd = ref<HTMLElement | null>(null);
|
||||
|
||||
const appName = computed(() => props.app?.name || '未知应用');
|
||||
const appPkg = computed(() => props.app?.pkgname || '');
|
||||
const appName = computed(() => props.app?.name || "未知应用");
|
||||
const appPkg = computed(() => props.app?.pkgname || "");
|
||||
|
||||
const handleClose = () => {
|
||||
if (uninstalling.value && !completed.value) return; // Prevent closing while uninstalling
|
||||
reset();
|
||||
emit('close');
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
reset();
|
||||
emit('success'); // Parent should refresh list
|
||||
emit('close');
|
||||
emit("success"); // Parent should refresh list
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
uninstalling.value = false;
|
||||
completed.value = false;
|
||||
logs.value = [];
|
||||
error.value = '';
|
||||
error.value = "";
|
||||
};
|
||||
|
||||
const confirmUninstall = () => {
|
||||
if (!appPkg.value) {
|
||||
error.value = '无效的包名';
|
||||
error.value = "无效的包名";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
uninstalling.value = true;
|
||||
logs.value = ['正在请求卸载: ' + appPkg.value + '...'];
|
||||
|
||||
window.ipcRenderer.send('remove-installed', appPkg.value);
|
||||
logs.value = ["正在请求卸载: " + appPkg.value + "..."];
|
||||
|
||||
window.ipcRenderer.send("remove-installed", appPkg.value);
|
||||
};
|
||||
|
||||
// Listeners
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onProgress = (_event: any, chunk: string) => {
|
||||
if (!uninstalling.value) return;
|
||||
// Split by newline but handle chunks correctly?
|
||||
// Split by newline but handle chunks correctly?
|
||||
// For simplicity, just appending lines if chunk contains newlines, or appending to last line?
|
||||
// Let's just push lines. The backend output might come in partial chunks.
|
||||
// A simple way is just to push the chunk and let CSS whitespace-pre-wrap handle it.
|
||||
@@ -124,17 +160,23 @@ const onProgress = (_event: any, chunk: string) => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onComplete = (_event: any, result: { success: boolean; message: any }) => {
|
||||
const onComplete = (
|
||||
_event: any,
|
||||
result: { success: boolean; message: any },
|
||||
) => {
|
||||
if (!uninstalling.value) return; // Ignore if not current session
|
||||
|
||||
const msgObj = typeof result.message === 'string' ? JSON.parse(result.message) : result.message;
|
||||
|
||||
|
||||
const msgObj =
|
||||
typeof result.message === "string"
|
||||
? JSON.parse(result.message)
|
||||
: result.message;
|
||||
|
||||
if (result.success) {
|
||||
logs.value.push('\n[完成] ' + (msgObj.message || '卸载成功'));
|
||||
logs.value.push("\n[完成] " + (msgObj.message || "卸载成功"));
|
||||
completed.value = true;
|
||||
} else {
|
||||
logs.value.push('\n[错误] ' + (msgObj.message || '卸载失败'));
|
||||
error.value = msgObj.message || '卸载失败';
|
||||
logs.value.push("\n[错误] " + (msgObj.message || "卸载失败"));
|
||||
error.value = msgObj.message || "卸载失败";
|
||||
// Allow trying again or closing?
|
||||
// We stay in "uninstalling" state visually or switch to completed=true but with error?
|
||||
// Let's set completed=true so user can click "Finish" (Close).
|
||||
@@ -146,25 +188,27 @@ const onComplete = (_event: any, result: { success: boolean; message: any }) =>
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (logEnd.value) {
|
||||
logEnd.value.scrollIntoView({ behavior: 'smooth' });
|
||||
logEnd.value.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watch(() => props.show, (val) => {
|
||||
if (val) {
|
||||
// specific setup if needed
|
||||
window.ipcRenderer.on('remove-progress', onProgress);
|
||||
window.ipcRenderer.on('remove-complete', onComplete);
|
||||
} else {
|
||||
window.ipcRenderer.off('remove-progress', onProgress);
|
||||
window.ipcRenderer.off('remove-complete', onComplete);
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// specific setup if needed
|
||||
window.ipcRenderer.on("remove-progress", onProgress);
|
||||
window.ipcRenderer.on("remove-complete", onComplete);
|
||||
} else {
|
||||
window.ipcRenderer.off("remove-progress", onProgress);
|
||||
window.ipcRenderer.off("remove-complete", onComplete);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
window.ipcRenderer.off('remove-progress', onProgress);
|
||||
window.ipcRenderer.off('remove-complete', onComplete);
|
||||
window.ipcRenderer.off("remove-progress", onProgress);
|
||||
window.ipcRenderer.off("remove-complete", onComplete);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,74 +1,118 @@
|
||||
<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">
|
||||
<div class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
||||
<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"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">软件更新</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">可更新的 APM 应用</p>
|
||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
软件更新
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
可更新的 APM 应用
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
||||
:disabled="loading" @click="$emit('refresh')">
|
||||
:disabled="loading"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
||||
:disabled="loading || apps.length === 0" @click="$emit('toggle-all')">
|
||||
:disabled="loading || apps.length === 0"
|
||||
@click="$emit('toggle-all')"
|
||||
>
|
||||
<i class="fas fa-check-square"></i>
|
||||
全选/全不选
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40"
|
||||
:disabled="loading || !hasSelected" @click="$emit('upgrade-selected')">
|
||||
:disabled="loading || !hasSelected"
|
||||
@click="$emit('upgrade-selected')"
|
||||
>
|
||||
<i class="fas fa-upload"></i>
|
||||
更新选中
|
||||
</button>
|
||||
<button type="button"
|
||||
<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"
|
||||
@click="$emit('close')" aria-label="关闭">
|
||||
@click="$emit('close')"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<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
|
||||
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">
|
||||
<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="apps.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
|
||||
v-else-if="apps.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="app in apps" :key="app.pkgname"
|
||||
class="flex flex-col 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 sm:flex-row sm:items-center sm:gap-4">
|
||||
<label
|
||||
v-for="app in apps"
|
||||
:key="app.pkgname"
|
||||
class="flex flex-col 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 sm:flex-row sm:items-center sm:gap-4"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<input type="checkbox" class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
||||
v-model="app.selected" :disabled="app.upgrading" />
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
||||
v-model="app.selected"
|
||||
:disabled="app.upgrading"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold text-slate-900 dark:text-white">{{ app.pkgname }}</p>
|
||||
<p class="font-semibold text-slate-900 dark:text-white">
|
||||
{{ app.pkgname }}
|
||||
</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||
当前 {{ app.currentVersion || '-' }} · 更新至 {{ app.newVersion || '-' }}
|
||||
当前 {{ app.currentVersion || "-" }} · 更新至
|
||||
{{ app.newVersion || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:ml-auto">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
||||
:disabled="app.upgrading" @click.prevent="$emit('upgrade-one', app)">
|
||||
:disabled="app.upgrading"
|
||||
@click.prevent="$emit('upgrade-one', app)"
|
||||
>
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
{{ app.upgrading ? '更新中…' : '更新' }}
|
||||
{{ app.upgrading ? "更新中…" : "更新" }}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
@@ -80,7 +124,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UpdateAppItem } from '../global/typedefinition';
|
||||
import type { UpdateAppItem } from "../global/typedefinition";
|
||||
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
@@ -92,11 +136,10 @@ defineProps<{
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'refresh'): void;
|
||||
(e: 'toggle-all'): void;
|
||||
(e: 'upgrade-selected'): void;
|
||||
(e: 'upgrade-one', app: UpdateAppItem): void;
|
||||
(e: "close"): void;
|
||||
(e: "refresh"): void;
|
||||
(e: "toggle-all"): void;
|
||||
(e: "upgrade-selected"): void;
|
||||
(e: "upgrade-one", app: UpdateAppItem): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user