mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
添加主页功能,支持加载和展示首页数据,包括链接和推荐应用列表
This commit is contained in:
41
AGENTS.md
41
AGENTS.md
@@ -325,6 +325,47 @@ export const APM_STORE_BASE_URL = 'https://erotica.spark-app.store';
|
|||||||
// /{arch}/{category}/{pkgname}/icon.png - App icon
|
// /{arch}/{category}/{pkgname}/icon.png - App icon
|
||||||
// /{arch}/{category}/{pkgname}/screen_N.png - Screenshots (1-5)
|
// /{arch}/{category}/{pkgname}/screen_N.png - Screenshots (1-5)
|
||||||
// /{arch}/categories.json - Categories mapping
|
// /{arch}/categories.json - Categories mapping
|
||||||
|
|
||||||
|
### Home (主页) 页面数据
|
||||||
|
|
||||||
|
The store may provide a special `home` directory under `{arch}` for a localized homepage. Two JSON files are expected:
|
||||||
|
|
||||||
|
- `homelinks.json` — 用于构建首页的轮播或链接块。每个条目示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "交流反馈",
|
||||||
|
"more": "前往论坛交流讨论",
|
||||||
|
"imgUrl": "/home/links/bbs.png",
|
||||||
|
"type": "_blank",
|
||||||
|
"url": "https://bbs.spark-app.store/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `homelist.json` — 描述若干推荐应用列表,每项引用一个 JSON 列表(`jsonUrl`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name":"装机必备", "type":"appList", "jsonUrl":"/home/lists/NecessaryforInstallation.json" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Parsing rules used by the app:
|
||||||
|
- Resolve `imgUrl` by prefixing: `${APM_STORE_BASE_URL}/{arch}${imgUrl}`.
|
||||||
|
- `type: _blank` → 使用系统浏览器打开链接;`type: _self` → 在当前页面打开。
|
||||||
|
- For `homelist.json` entries with `type: "appList"`, fetch the referenced `jsonUrl` and map each item to the app shape used by the UI:
|
||||||
|
- `Name` → `name`
|
||||||
|
- `Pkgname` → `pkgname`
|
||||||
|
- `Category` → `category`
|
||||||
|
- `More` → `more`
|
||||||
|
|
||||||
|
Where to implement:
|
||||||
|
- Renderer: `src/App.vue` loads and normalizes `homelinks.json` and `homelist.json` on selecting the `home` category and exposes data to a new `HomeView` component.
|
||||||
|
- Component: `src/components/HomeView.vue` renders link cards and recommended app sections (re-uses `AppCard.vue`).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The `home` directory path is: `/{arch}/home/` under the configured `APM_STORE_BASE_URL`.
|
||||||
|
- Missing or partially invalid files are handled gracefully — individual failures don't block showing other home sections.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Axios Usage
|
### Axios Usage
|
||||||
|
|||||||
@@ -160,6 +160,24 @@ ipcMain.handle("run-update-tool", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 启动安装设置脚本(可能需要提升权限)
|
||||||
|
ipcMain.handle("open-install-settings", async () => {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import("node:child_process");
|
||||||
|
const scriptPath = "/opt/durapps/spark-store/bin/update-upgrade/ss-update-controler.sh";
|
||||||
|
const child = spawn("/opt/spark-store/extras/host-spawn", [scriptPath], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
logger.info(`Launched ${scriptPath}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "Failed to launch install settings script");
|
||||||
|
return { success: false, message: (err as Error)?.message || String(err) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Set User-Agent for client
|
// Set User-Agent for client
|
||||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
|
|||||||
99
src/App.vue
99
src/App.vue
@@ -32,15 +32,30 @@
|
|||||||
:active-category="activeCategory"
|
:active-category="activeCategory"
|
||||||
:apps-count="filteredApps.length"
|
:apps-count="filteredApps.length"
|
||||||
@update-search="handleSearchInput"
|
@update-search="handleSearchInput"
|
||||||
|
@search-focus="handleSearchFocus"
|
||||||
@update="handleUpdate"
|
@update="handleUpdate"
|
||||||
@list="handleList"
|
@list="handleList"
|
||||||
|
@open-install-settings="handleOpenInstallSettings"
|
||||||
@toggle-sidebar="isSidebarOpen = !isSidebarOpen"
|
@toggle-sidebar="isSidebarOpen = !isSidebarOpen"
|
||||||
/>
|
/>
|
||||||
|
<template v-if="activeCategory === 'home'">
|
||||||
|
<div class="pt-6">
|
||||||
|
<HomeView
|
||||||
|
:links="homeLinks"
|
||||||
|
:lists="homeLists"
|
||||||
|
:loading="homeLoading"
|
||||||
|
:error="homeError"
|
||||||
|
@open-detail="openDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<AppGrid
|
<AppGrid
|
||||||
:apps="filteredApps"
|
:apps="filteredApps"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@open-detail="openDetail"
|
@open-detail="openDetail"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AppDetailModal
|
<AppDetailModal
|
||||||
@@ -125,6 +140,7 @@ import pino from "pino";
|
|||||||
import AppSidebar from "./components/AppSidebar.vue";
|
import AppSidebar from "./components/AppSidebar.vue";
|
||||||
import AppHeader from "./components/AppHeader.vue";
|
import AppHeader from "./components/AppHeader.vue";
|
||||||
import AppGrid from "./components/AppGrid.vue";
|
import AppGrid from "./components/AppGrid.vue";
|
||||||
|
import HomeView from "./components/HomeView.vue";
|
||||||
import AppDetailModal from "./components/AppDetailModal.vue";
|
import AppDetailModal from "./components/AppDetailModal.vue";
|
||||||
import ScreenPreview from "./components/ScreenPreview.vue";
|
import ScreenPreview from "./components/ScreenPreview.vue";
|
||||||
import DownloadQueue from "./components/DownloadQueue.vue";
|
import DownloadQueue from "./components/DownloadQueue.vue";
|
||||||
@@ -177,7 +193,7 @@ const isDarkTheme = computed(() => {
|
|||||||
|
|
||||||
const categories: Ref<Record<string, string>> = ref({});
|
const categories: Ref<Record<string, string>> = ref({});
|
||||||
const apps: Ref<App[]> = ref([]);
|
const apps: Ref<App[]> = ref([]);
|
||||||
const activeCategory = ref("all");
|
const activeCategory = ref("home");
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const isSidebarOpen = ref(false);
|
const isSidebarOpen = ref(false);
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
@@ -279,6 +295,9 @@ const selectCategory = (category: string) => {
|
|||||||
activeCategory.value = category;
|
activeCategory.value = category;
|
||||||
searchQuery.value = "";
|
searchQuery.value = "";
|
||||||
isSidebarOpen.value = false;
|
isSidebarOpen.value = false;
|
||||||
|
if (category === "home") {
|
||||||
|
loadHome();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDetail = (app: App) => {
|
const openDetail = (app: App) => {
|
||||||
@@ -334,6 +353,67 @@ const closeScreenPreview = () => {
|
|||||||
showPreview.value = false;
|
showPreview.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Home data
|
||||||
|
const homeLinks = ref<any[]>([]);
|
||||||
|
const homeLists = ref<Array<{ title: string; apps: any[] }>>([]);
|
||||||
|
const homeLoading = ref(false);
|
||||||
|
const homeError = ref("");
|
||||||
|
|
||||||
|
const loadHome = async () => {
|
||||||
|
homeLoading.value = true;
|
||||||
|
homeError.value = "";
|
||||||
|
homeLinks.value = [];
|
||||||
|
homeLists.value = [];
|
||||||
|
try {
|
||||||
|
const base = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/home`;
|
||||||
|
// homelinks.json
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/homelinks.json`);
|
||||||
|
if (res.ok) {
|
||||||
|
homeLinks.value = await res.json();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore single file failures
|
||||||
|
console.warn("Failed to load homelinks.json", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// homelist.json
|
||||||
|
try {
|
||||||
|
const res2 = await fetch(`${base}/homelist.json`);
|
||||||
|
if (res2.ok) {
|
||||||
|
const lists = await res2.json();
|
||||||
|
for (const item of lists) {
|
||||||
|
if (item.type === "appList" && item.jsonUrl) {
|
||||||
|
try {
|
||||||
|
const url = `${APM_STORE_BASE_URL}/${window.apm_store.arch}${item.jsonUrl}`;
|
||||||
|
const r = await fetch(url);
|
||||||
|
if (r.ok) {
|
||||||
|
const appsJson = await r.json();
|
||||||
|
const apps = (appsJson || []).map((a: any) => ({
|
||||||
|
name: a.Name || a.name || a.Pkgname || a.PkgName || "",
|
||||||
|
pkgname: a.Pkgname || a.pkgname || "",
|
||||||
|
category: a.Category || a.category || "unknown",
|
||||||
|
more: a.More || a.more || "",
|
||||||
|
version: a.Version || "",
|
||||||
|
}));
|
||||||
|
homeLists.value.push({ title: item.name || "推荐", apps });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load home list", item, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load homelist.json", e);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
homeError.value = (error as Error)?.message || "加载首页失败";
|
||||||
|
} finally {
|
||||||
|
homeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const prevScreen = () => {
|
const prevScreen = () => {
|
||||||
if (currentScreenIndex.value > 0) {
|
if (currentScreenIndex.value > 0) {
|
||||||
currentScreenIndex.value--;
|
currentScreenIndex.value--;
|
||||||
@@ -357,6 +437,17 @@ const handleUpdate = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenInstallSettings = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipcRenderer.invoke("open-install-settings");
|
||||||
|
if (!result || !result.success) {
|
||||||
|
logger.warn(`启动安装设置失败: ${result?.message || "未知错误"}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`调用安装设置时出错: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleList = () => {
|
const handleList = () => {
|
||||||
openInstalledModal();
|
openInstalledModal();
|
||||||
};
|
};
|
||||||
@@ -705,11 +796,17 @@ const handleSearchInput = (value: string) => {
|
|||||||
searchQuery.value = value;
|
searchQuery.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchFocus = () => {
|
||||||
|
if (activeCategory.value === "home") activeCategory.value = "all";
|
||||||
|
};
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initTheme();
|
initTheme();
|
||||||
|
|
||||||
await loadCategories();
|
await loadCategories();
|
||||||
|
// 默认加载主页数据
|
||||||
|
await loadHome();
|
||||||
// 先显示 loading,并异步开始分批加载应用列表。
|
// 先显示 loading,并异步开始分批加载应用列表。
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
loadApps(() => {
|
loadApps(() => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
<TopActions @update="$emit('update')" @list="$emit('list')" />
|
<TopActions @update="$emit('update')" @list="$emit('list')" @open-install-settings="$emit('open-install-settings')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex-1">
|
<div class="w-full flex-1">
|
||||||
<label for="searchBox" class="sr-only">搜索应用</label>
|
<label for="searchBox" class="sr-only">搜索应用</label>
|
||||||
@@ -24,12 +24,13 @@
|
|||||||
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"
|
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="搜索应用名 / 包名 / 标签,按回车键搜索"
|
placeholder="搜索应用名 / 包名 / 标签,按回车键搜索"
|
||||||
@keydown.enter="handleSearch"
|
@keydown.enter="handleSearch"
|
||||||
|
@focus="handleSearchFocus"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400" id="currentCount">
|
<div v-if="activeCategory !== 'home'" class="text-sm text-slate-500 dark:text-slate-400" id="currentCount">
|
||||||
共 {{ appsCount }} 个应用 · 在任何主流 Linux 发行上安装应用
|
共 {{ appsCount }} 个应用
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,6 +49,8 @@ const emit = defineEmits<{
|
|||||||
(e: "update-search", query: string): void;
|
(e: "update-search", query: string): void;
|
||||||
(e: "update"): void;
|
(e: "update"): void;
|
||||||
(e: "list"): void;
|
(e: "list"): void;
|
||||||
|
(e: "search-focus"): void;
|
||||||
|
(e: "open-install-settings"): void;
|
||||||
(e: "toggle-sidebar"): void;
|
(e: "toggle-sidebar"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -57,6 +60,10 @@ const handleSearch = () => {
|
|||||||
emit("update-search", localSearchQuery.value);
|
emit("update-search", localSearchQuery.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchFocus = () => {
|
||||||
|
emit("search-focus");
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.searchQuery,
|
() => props.searchQuery,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
|
|||||||
@@ -30,6 +30,19 @@
|
|||||||
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
||||||
|
|
||||||
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
|
<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 === 'home'
|
||||||
|
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="selectCategory('home')"
|
||||||
|
>
|
||||||
|
<span>主页</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="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="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"
|
||||||
|
|||||||
85
src/components/HomeView.vue
Normal file
85
src/components/HomeView.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div v-if="loading" class="text-center text-slate-500">正在加载首页内容…</div>
|
||||||
|
<div v-else-if="error" class="text-center text-rose-600">{{ error }}</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="grid gap-4 auto-fit-grid">
|
||||||
|
<a
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.url + link.name"
|
||||||
|
:href="link.type === '_blank' ? undefined : link.url"
|
||||||
|
@click.prevent="onLinkClick(link)"
|
||||||
|
class="flex flex-col items-start gap-2 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm hover:shadow-lg transition"
|
||||||
|
:title="link.more"
|
||||||
|
>
|
||||||
|
<img :src="computedImgUrl(link.imgUrl)" class="h-20 w-full object-contain" />
|
||||||
|
<div class="text-base font-semibold text-slate-900">{{ link.name }}</div>
|
||||||
|
<div class="text-sm text-slate-500">{{ link.more }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 mt-6">
|
||||||
|
<section v-for="section in lists" :key="section.title">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-slate-900">{{ section.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 grid gap-4 auto-fit-grid">
|
||||||
|
<AppCard
|
||||||
|
v-for="app in section.apps"
|
||||||
|
:key="app.pkgname"
|
||||||
|
:app="app"
|
||||||
|
@open-detail="$emit('open-detail', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AppCard from "./AppCard.vue";
|
||||||
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
links: Array<any>;
|
||||||
|
lists: Array<{ title: string; apps: any[] }>;
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "open-detail", app: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const computedImgUrl = (imgUrl: string) => {
|
||||||
|
if (!imgUrl) return "";
|
||||||
|
// imgUrl is like /home/links/bbs.png -> join with base
|
||||||
|
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}${imgUrl}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLinkClick = (link: any) => {
|
||||||
|
if (link.type === "_blank") {
|
||||||
|
window.open(link.url, "_blank");
|
||||||
|
} else {
|
||||||
|
// open in same page: navigate to url
|
||||||
|
window.location.href = link.url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auto-fit-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* slight gap tuning for small screens */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.auto-fit-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,22 +18,23 @@
|
|||||||
alt="应用截图预览"
|
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"
|
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="absolute left-4 top-1/2 transform -translate-y-1/2">
|
||||||
>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex h-12 w-12 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"
|
@click="prevScreen"
|
||||||
:disabled="currentScreenIndex === 0"
|
:disabled="currentScreenIndex === 0"
|
||||||
aria-label="上一张"
|
aria-label="上一张"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute right-4 top-1/2 transform -translate-y-1/2">
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex h-12 w-12 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"
|
@click="nextScreen"
|
||||||
:disabled="currentScreenIndex === screenshots.length - 1"
|
:disabled="currentScreenIndex === screenshots.length - 1"
|
||||||
aria-label="下一张"
|
aria-label="下一张"
|
||||||
@@ -41,6 +42,9 @@
|
|||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 仍保留右上关闭按钮 -->
|
||||||
|
<div class="absolute top-4 right-4">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
||||||
|
|||||||
@@ -9,6 +9,15 @@
|
|||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
<span>软件更新</span>
|
<span>软件更新</span>
|
||||||
</button>
|
</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="handleSettings"
|
||||||
|
title="安装设置"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
<span>安装设置</span>
|
||||||
|
</button>
|
||||||
<!-- <button
|
<!-- <button
|
||||||
type="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"
|
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"
|
||||||
@@ -25,6 +34,7 @@
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "update"): void;
|
(e: "update"): void;
|
||||||
(e: "list"): void;
|
(e: "list"): void;
|
||||||
|
(e: "open-install-settings"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
@@ -34,4 +44,8 @@ const handleUpdate = () => {
|
|||||||
const handleList = () => {
|
const handleList = () => {
|
||||||
emit("list");
|
emit("list");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSettings = () => {
|
||||||
|
emit("open-install-settings");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user