mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
feat: enhance UI and functionality across components
- Added Google Fonts preconnect and stylesheet link in `index.html` for improved typography. - Updated version in `package.json` to `4.9.9alpha3`. - Refined launch configuration by removing deprecated arguments. - Improved app detail modal and card components for better accessibility and visual consistency. - Enhanced download queue and sidebar components with updated styles and functionality. - Implemented new utility classes for better styling control in CSS. - Adjusted various components for improved responsiveness and user experience.
This commit is contained in:
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@@ -30,7 +30,6 @@
|
|||||||
// },
|
// },
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"--remote-debugging-port=9229",
|
"--remote-debugging-port=9229",
|
||||||
"--no-spark",
|
|
||||||
"."
|
"."
|
||||||
],
|
],
|
||||||
"envFile": "${workspaceFolder}/.vscode/.debug.env",
|
"envFile": "${workspaceFolder}/.vscode/.debug.env",
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
|||||||
type: "info",
|
type: "info",
|
||||||
title: "APM 安装成功",
|
title: "APM 安装成功",
|
||||||
message: "恭喜您,APM 已成功安装",
|
message: "恭喜您,APM 已成功安装",
|
||||||
detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。",
|
detail:
|
||||||
|
"APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。",
|
||||||
buttons: ["确定"],
|
buttons: ["确定"],
|
||||||
defaultId: 0,
|
defaultId: 0,
|
||||||
});
|
});
|
||||||
@@ -670,7 +671,6 @@ ipcMain.handle("list-upgradable", async () => {
|
|||||||
return { success: true, apps };
|
return { success: true, apps };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
|
ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
|
||||||
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
|
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
|
||||||
|
|||||||
139
electron/main/backend/telemetry.ts
Normal file
139
electron/main/backend/telemetry.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 启动时遥测:收集系统与商店版本信息并上报至 status.deepinos.org.cn
|
||||||
|
* 仅在 Linux 下执行一次,不阻塞启动,失败静默记录日志。
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
const logger = pino({ name: "telemetry" });
|
||||||
|
const TELEMETRY_URL = "https://status.spark-app.store/upload";
|
||||||
|
|
||||||
|
interface TelemetryPayload {
|
||||||
|
"Distributor ID": string;
|
||||||
|
Release: string;
|
||||||
|
Architecture: string;
|
||||||
|
Store_Version: string;
|
||||||
|
UUID: string;
|
||||||
|
TIME: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileSafe(path: string): string {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path, "utf8").trim();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 /etc/os-release 的 KEY="value" 行 */
|
||||||
|
function parseOsRelease(content: string): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
for (const line of content.split("\n")) {
|
||||||
|
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(?:")?([^"]*)(?:")?$/);
|
||||||
|
if (m) out[m[1]] = m[2].replace(/\\"/g, '"');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistroInfo(): { distributorId: string; release: string } {
|
||||||
|
const osReleasePath = "/etc/os-release";
|
||||||
|
const redhatPath = "/etc/redhat-release";
|
||||||
|
const debianPath = "/etc/debian_version";
|
||||||
|
|
||||||
|
if (fs.existsSync(osReleasePath)) {
|
||||||
|
const content = readFileSafe(osReleasePath);
|
||||||
|
const parsed = parseOsRelease(content);
|
||||||
|
const name = parsed.NAME ?? "Unknown";
|
||||||
|
const versionId = parsed.VERSION_ID ?? "Unknown";
|
||||||
|
return { distributorId: name, release: versionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(redhatPath)) {
|
||||||
|
const content = readFileSafe(redhatPath);
|
||||||
|
const distributorId = content.split(/\s+/)[0] ?? "Unknown";
|
||||||
|
const releaseMatch = content.match(/release\s+([0-9][0-9.]*)/i);
|
||||||
|
const release = releaseMatch ? releaseMatch[1] : "Unknown";
|
||||||
|
return { distributorId, release };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(debianPath)) {
|
||||||
|
const release = readFileSafe(debianPath) || "Unknown";
|
||||||
|
return { distributorId: "Debian", release };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { distributorId: "Unknown", release: "Unknown" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUuid(): string {
|
||||||
|
const content = readFileSafe("/etc/machine-id");
|
||||||
|
return content || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 架构:与 uname -m 一致,使用 Node 的 os.machine() */
|
||||||
|
function getArchitecture(): string {
|
||||||
|
if (typeof os.machine === "function") {
|
||||||
|
return os.machine();
|
||||||
|
}
|
||||||
|
const arch = process.arch;
|
||||||
|
if (arch === "x64") return "x86_64";
|
||||||
|
if (arch === "arm64") return "aarch64";
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(storeVersion: string): TelemetryPayload {
|
||||||
|
const { distributorId, release } = getDistroInfo();
|
||||||
|
const time = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Distributor ID": distributorId,
|
||||||
|
Release: release,
|
||||||
|
Architecture: getArchitecture(),
|
||||||
|
Store_Version: storeVersion,
|
||||||
|
UUID: getUuid(),
|
||||||
|
TIME: time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送遥测数据。仅在 Linux 下执行;非 Linux 直接返回。
|
||||||
|
* 不抛出异常,错误仅写日志。
|
||||||
|
*/
|
||||||
|
export function sendTelemetryOnce(storeVersion: string): void {
|
||||||
|
if (process.platform !== "linux") {
|
||||||
|
logger.debug("Telemetry skipped: not Linux");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildPayload(storeVersion);
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
|
||||||
|
fetch(TELEMETRY_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const code = res.status;
|
||||||
|
if (code === 200) {
|
||||||
|
logger.debug("Telemetry sent successfully");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code === 400) {
|
||||||
|
logger.warn("Telemetry: 客户端请求错误,请检查 JSON 或接口逻辑");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code === 422) {
|
||||||
|
logger.warn("Telemetry: 请求数据无效,请检查字段值");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code === 500) {
|
||||||
|
logger.warn("Telemetry: 服务器内部错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.warn(`Telemetry: 未处理的响应码 ${code}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn({ err }, "Telemetry request failed");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import pino from "pino";
|
|||||||
import { handleCommandLine } from "./deeplink.js";
|
import { handleCommandLine } from "./deeplink.js";
|
||||||
import { isLoaded } from "../global.js";
|
import { isLoaded } from "../global.js";
|
||||||
import { tasks } from "./backend/install-manager.js";
|
import { tasks } from "./backend/install-manager.js";
|
||||||
|
import { sendTelemetryOnce } from "./backend/telemetry.js";
|
||||||
|
|
||||||
// Assure single instance application
|
// Assure single instance application
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
@@ -64,13 +65,21 @@ let win: BrowserWindow | null = null;
|
|||||||
const preload = path.join(__dirname, "../preload/index.mjs");
|
const preload = path.join(__dirname, "../preload/index.mjs");
|
||||||
const indexHtml = path.join(RENDERER_DIST, "index.html");
|
const indexHtml = path.join(RENDERER_DIST, "index.html");
|
||||||
|
|
||||||
// Use app.getVersion() when the app is packaged.
|
/** 与项目 package.json 一致的版本号:打包用 app.getVersion(),未打包时读 package.json */
|
||||||
|
function getAppVersion(): string {
|
||||||
|
if (app.isPackaged) return app.getVersion();
|
||||||
|
const pkgPath = path.join(process.env.APP_ROOT ?? __dirname, "package.json");
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(pkgPath, "utf8");
|
||||||
|
const pkg = JSON.parse(raw) as { version?: string };
|
||||||
|
return typeof pkg.version === "string" ? pkg.version : "dev";
|
||||||
|
} catch {
|
||||||
|
return "dev";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getUserAgent = (): string => {
|
const getUserAgent = (): string => {
|
||||||
const version =
|
return `Spark-Store/${getAppVersion()}`;
|
||||||
app && app.isPackaged
|
|
||||||
? app.getVersion()
|
|
||||||
: process.env.npm_package_version || "dev";
|
|
||||||
return `Spark-Store/${version}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info("User Agent: " + getUserAgent());
|
logger.info("User Agent: " + getUserAgent());
|
||||||
@@ -86,9 +95,8 @@ function getStoreFilterFromArgv(): "spark" | "apm" | "both" {
|
|||||||
return "both";
|
return "both";
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" =>
|
||||||
"get-store-filter",
|
getStoreFilterFromArgv(),
|
||||||
(): "spark" | "apm" | "both" => getStoreFilterFromArgv(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
@@ -204,6 +212,8 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
createWindow();
|
createWindow();
|
||||||
handleCommandLine(process.argv);
|
handleCommandLine(process.argv);
|
||||||
|
// 启动后执行一次遥测(仅 Linux,不阻塞)
|
||||||
|
sendTelemetryOnce(getAppVersion());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
@@ -247,9 +257,7 @@ function resolveIconPath(filename: string): string {
|
|||||||
/** 按优先级返回托盘图标路径:spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG,故不尝试 spark-store.svg */
|
/** 按优先级返回托盘图标路径:spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG,故不尝试 spark-store.svg */
|
||||||
function getTrayIconPath(): string | null {
|
function getTrayIconPath(): string | null {
|
||||||
const ext = process.platform === "win32" ? ".ico" : ".png";
|
const ext = process.platform === "win32" ? ".ico" : ".png";
|
||||||
const candidates = [
|
const candidates = [`spark-store${ext}`];
|
||||||
`spark-store${ext}`
|
|
||||||
];
|
|
||||||
for (const name of candidates) {
|
for (const name of candidates) {
|
||||||
const iconPath = resolveIconPath(name);
|
const iconPath = resolveIconPath(name);
|
||||||
if (fs.existsSync(iconPath)) {
|
if (fs.existsSync(iconPath)) {
|
||||||
@@ -265,7 +273,9 @@ function getTrayIconPath(): string | null {
|
|||||||
const FALLBACK_TRAY_PNG =
|
const FALLBACK_TRAY_PNG =
|
||||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg==";
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg==";
|
||||||
|
|
||||||
function getTrayImage(): string | ReturnType<typeof nativeImage.createFromDataURL> {
|
function getTrayImage():
|
||||||
|
| string
|
||||||
|
| ReturnType<typeof nativeImage.createFromDataURL> {
|
||||||
const iconPath = getTrayIconPath();
|
const iconPath = getTrayIconPath();
|
||||||
if (iconPath) return iconPath;
|
if (iconPath) return iconPath;
|
||||||
return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG);
|
return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/spark-store.svg" />
|
<link rel="icon" type="image/svg+xml" href="/spark-store.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||||
<title>星火应用商店</title>
|
<title>星火应用商店</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "4.9.9",
|
"version": "4.9.9alpha3",
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
"description": "Client for Spark App Store",
|
"description": "Client for Spark App Store",
|
||||||
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
||||||
|
|||||||
27
src/App.vue
27
src/App.vue
@@ -1,16 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex min-h-screen flex-col bg-slate-50 text-slate-900 transition-colors duration-300 dark:bg-slate-950 dark:text-slate-100 lg:flex-row"
|
class="flex min-h-screen flex-col text-slate-900 transition-colors duration-300 dark:text-slate-100 lg:flex-row bg-[#f8fafc] dark:bg-[#0f172a]"
|
||||||
>
|
>
|
||||||
|
<!-- 背景装饰:浅色下为极淡渐变,深色下为 subtle grain -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed inset-0 z-0 bg-gradient-to-br from-slate-50/80 via-transparent to-brand/[0.02] dark:from-slate-950/50 dark:to-transparent"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<!-- 移动端侧边栏遮罩 -->
|
<!-- 移动端侧边栏遮罩 -->
|
||||||
<div
|
<div
|
||||||
v-if="isSidebarOpen"
|
v-if="isSidebarOpen"
|
||||||
class="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm lg:hidden"
|
class="fixed inset-0 z-40 bg-slate-900/30 backdrop-blur-md lg:hidden transition-opacity"
|
||||||
@click="isSidebarOpen = false"
|
@click="isSidebarOpen = false"
|
||||||
></div>
|
/>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-50 w-72 transform border-r border-slate-200/70 bg-white/95 px-5 py-6 backdrop-blur transition-transform duration-300 ease-in-out dark:border-slate-800/70 dark:bg-slate-900 lg:sticky lg:top-0 lg:flex lg:h-screen lg:translate-x-0 lg:flex-col lg:border-b-0"
|
class="fixed inset-y-0 left-0 z-50 w-72 transform border-r border-slate-200/60 bg-white/90 px-5 py-6 shadow-xl shadow-slate-200/20 backdrop-blur-xl transition-transform duration-300 ease-out dark:border-slate-800/50 dark:bg-slate-900/95 dark:shadow-none lg:sticky lg:top-0 lg:flex lg:h-screen lg:translate-x-0 lg:flex-col lg:border-b-0 lg:shadow-none"
|
||||||
:class="
|
:class="
|
||||||
isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
||||||
"
|
"
|
||||||
@@ -26,7 +31,7 @@
|
|||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="flex-1 px-4 py-6 lg:px-10">
|
<main class="relative z-10 flex-1 px-4 py-6 lg:px-10">
|
||||||
<AppHeader
|
<AppHeader
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
:active-category="activeCategory"
|
:active-category="activeCategory"
|
||||||
@@ -395,16 +400,16 @@ const openDetail = async (app: App | Record<string, unknown>) => {
|
|||||||
if (fullApp.isMerged && (fullApp.sparkApp || fullApp.apmApp)) {
|
if (fullApp.isMerged && (fullApp.sparkApp || fullApp.apmApp)) {
|
||||||
const [sparkInstalled, apmInstalled] = await Promise.all([
|
const [sparkInstalled, apmInstalled] = await Promise.all([
|
||||||
fullApp.sparkApp
|
fullApp.sparkApp
|
||||||
? window.ipcRenderer.invoke("check-installed", {
|
? (window.ipcRenderer.invoke("check-installed", {
|
||||||
pkgname: fullApp.sparkApp.pkgname,
|
pkgname: fullApp.sparkApp.pkgname,
|
||||||
origin: "spark",
|
origin: "spark",
|
||||||
}) as Promise<boolean>
|
}) as Promise<boolean>)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
fullApp.apmApp
|
fullApp.apmApp
|
||||||
? window.ipcRenderer.invoke("check-installed", {
|
? (window.ipcRenderer.invoke("check-installed", {
|
||||||
pkgname: fullApp.apmApp.pkgname,
|
pkgname: fullApp.apmApp.pkgname,
|
||||||
origin: "apm",
|
origin: "apm",
|
||||||
}) as Promise<boolean>
|
}) as Promise<boolean>)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
]);
|
]);
|
||||||
if (sparkInstalled && !apmInstalled) {
|
if (sparkInstalled && !apmInstalled) {
|
||||||
@@ -417,9 +422,9 @@ const openDetail = async (app: App | Record<string, unknown>) => {
|
|||||||
|
|
||||||
const displayAppForScreenshots =
|
const displayAppForScreenshots =
|
||||||
fullApp.viewingOrigin !== undefined && fullApp.isMerged
|
fullApp.viewingOrigin !== undefined && fullApp.isMerged
|
||||||
? (fullApp.viewingOrigin === "spark"
|
? ((fullApp.viewingOrigin === "spark"
|
||||||
? fullApp.sparkApp
|
? fullApp.sparkApp
|
||||||
: fullApp.apmApp) ?? fullApp
|
: fullApp.apmApp) ?? fullApp)
|
||||||
: fullApp;
|
: fullApp;
|
||||||
|
|
||||||
currentApp.value = fullApp;
|
currentApp.value = fullApp;
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", "system-ui", "-apple-system", "Segoe UI", "sans-serif";
|
--font-sans: "Plus Jakarta Sans", "Inter", "system-ui", "-apple-system", "Segoe UI", "sans-serif";
|
||||||
|
|
||||||
--color-brand: #2563eb;
|
--color-brand: #2563eb;
|
||||||
--color-brand-dark: #1d4ed8;
|
--color-brand-dark: #1d4ed8;
|
||||||
--color-brand-soft: #60a5fa;
|
--color-brand-soft: #60a5fa;
|
||||||
|
|
||||||
--color-surface-light: #f5f7fb;
|
--color-surface-light: #f1f5f9;
|
||||||
--color-surface-dark: #0b1220;
|
--color-surface-dark: #0f172a;
|
||||||
|
|
||||||
--color-card-light: #ffffff;
|
--color-card-light: #ffffff;
|
||||||
--color-card-dark: #151c2c;
|
--color-card-dark: #1e293b;
|
||||||
|
|
||||||
--shadow-glass: 0 10px 30px rgba(15,23,42,0.08);
|
--shadow-glass: 0 4px 24px rgba(15, 23, 42, 0.06);
|
||||||
--shadow-glassDark: 0 20px 45px rgba(0,0,0,0.45);
|
--shadow-glassDark: 0 24px 48px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-card-hover: 0 12px 40px rgba(15, 23, 42, 0.12);
|
||||||
|
|
||||||
--radius-xl: 1rem;
|
--radius-xl: 1rem;
|
||||||
|
--radius-2xl: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@variant dark (&:where(.dark, .dark *));
|
@variant dark (&:where(.dark, .dark *));
|
||||||
@@ -27,11 +30,12 @@
|
|||||||
background-color: var(--color-surface-light);
|
background-color: var(--color-surface-light);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.dark body {
|
:root.dark body {
|
||||||
background-color: var(--color-surface-dark);
|
background-color: var(--color-surface-dark);
|
||||||
color: #f8fafc;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@@ -40,6 +44,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar-muted {
|
.scrollbar-muted {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #94a3b8 transparent;
|
scrollbar-color: #94a3b8 transparent;
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<article
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
@click="openDetail"
|
@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"
|
@keydown.enter="openDetail"
|
||||||
|
@keydown.space.prevent="openDetail"
|
||||||
|
class="group flex h-full cursor-pointer gap-4 rounded-2xl border border-slate-200/60 bg-white/95 p-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-brand/30 hover:shadow-md hover:shadow-slate-200/50 dark:border-slate-700/50 dark:bg-slate-800/70 dark:hover:border-brand/40 dark:hover:shadow-slate-900/50"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
class="flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-gradient-to-br from-slate-100 to-slate-200/80 shadow-inner ring-1 ring-slate-200/50 dark:from-slate-700 dark:to-slate-800 dark:ring-slate-600/30"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref="iconImg"
|
ref="iconImg"
|
||||||
:src="loadedIcon"
|
:src="loadedIcon"
|
||||||
alt="icon"
|
alt=""
|
||||||
:class="[
|
:class="[
|
||||||
'h-full w-full object-cover transition-opacity duration-300',
|
'h-full w-full object-cover transition-opacity duration-300',
|
||||||
isLoaded ? 'opacity-100' : 'opacity-0',
|
isLoaded ? 'opacity-100' : 'opacity-0',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col gap-1 overflow-hidden">
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div
|
<h3
|
||||||
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
||||||
>
|
>
|
||||||
{{ app.name || "" }}
|
{{ app.name || "" }}
|
||||||
</div>
|
</h3>
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm',
|
'shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider',
|
||||||
app.isMerged
|
app.isMerged
|
||||||
? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
? 'bg-violet-100 text-violet-600 dark:bg-violet-900/40 dark:text-violet-300'
|
||||||
: app.origin === 'spark'
|
: app.origin === 'spark'
|
||||||
? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
|
||||||
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
: 'bg-sky-100 text-sky-600 dark:bg-sky-900/40 dark:text-sky-300',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
@@ -42,14 +46,18 @@
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
<p
|
||||||
|
class="truncate text-xs font-medium text-slate-500 dark:text-slate-400"
|
||||||
|
>
|
||||||
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
||||||
</div>
|
</p>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
<p
|
||||||
|
class="line-clamp-2 text-sm leading-snug text-slate-500 dark:text-slate-400"
|
||||||
|
>
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/70 p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/60 backdrop-blur-sm p-4"
|
||||||
@click.self="closeModal"
|
@click.self="closeModal"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-2xl border border-slate-200/60 bg-white/98 p-6 shadow-2xl dark:border-slate-700/50 dark:bg-slate-900/98"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
<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 flex-1 items-center gap-4">
|
||||||
<div
|
<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"
|
class="flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-br from-slate-100 to-slate-200/80 shadow-inner ring-1 ring-slate-200/50 dark:from-slate-700 dark:to-slate-800 dark:ring-slate-600/30"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="app"
|
v-if="app"
|
||||||
@@ -133,11 +133,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200/60 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
aria-label="关闭"
|
aria-label="关闭"
|
||||||
>
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!loading"
|
v-if="!loading"
|
||||||
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||||
>
|
>
|
||||||
<AppCard
|
<AppCard
|
||||||
v-for="(app, index) in apps"
|
v-for="(app, index) in apps"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<header class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:gap-5">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-slate-200/70 bg-white/80 text-slate-500 shadow-sm backdrop-blur transition hover:bg-slate-50 lg:hidden dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-400 dark:hover:bg-slate-800"
|
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200/60 bg-white/90 text-slate-500 shadow-sm backdrop-blur-sm transition hover:border-slate-300 hover:bg-white hover:text-slate-700 lg:hidden dark:border-slate-700/60 dark:bg-slate-800/80 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200"
|
||||||
@click="$emit('toggle-sidebar')"
|
@click="$emit('toggle-sidebar')"
|
||||||
title="切换侧边栏"
|
title="切换侧边栏"
|
||||||
|
aria-label="切换侧边栏"
|
||||||
>
|
>
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -16,16 +17,19 @@
|
|||||||
@open-install-settings="$emit('open-install-settings')"
|
@open-install-settings="$emit('open-install-settings')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex-1">
|
<div class="w-full flex-1 min-w-0">
|
||||||
<label for="searchBox" class="sr-only">搜索应用</label>
|
<label for="searchBox" class="sr-only">搜索应用</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<i
|
<span
|
||||||
class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"
|
class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100/80 text-slate-400 dark:bg-slate-800/80 dark:text-slate-500"
|
||||||
></i>
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<i class="fas fa-search text-sm"></i>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
id="searchBox"
|
id="searchBox"
|
||||||
v-model="localSearchQuery"
|
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"
|
class="w-full rounded-xl border border-slate-200/70 bg-white/95 py-3 pl-12 pr-4 text-sm text-slate-800 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/60 focus:ring-2 focus:ring-brand/20 dark:border-slate-700/70 dark:bg-slate-800/80 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus:border-brand/50 dark:focus:ring-brand/20"
|
||||||
placeholder="搜索应用名 / 包名 / 标签,按回车键搜索"
|
placeholder="搜索应用名 / 包名 / 标签,按回车键搜索"
|
||||||
@keydown.enter="handleSearch"
|
@keydown.enter="handleSearch"
|
||||||
@focus="handleSearchFocus"
|
@focus="handleSearchFocus"
|
||||||
@@ -35,12 +39,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="activeCategory !== 'home'"
|
v-if="activeCategory !== 'home'"
|
||||||
class="text-sm text-slate-500 dark:text-slate-400"
|
|
||||||
id="currentCount"
|
id="currentCount"
|
||||||
|
class="text-sm font-medium text-slate-500 dark:text-slate-400"
|
||||||
>
|
>
|
||||||
共 {{ appsCount }} 个应用
|
共
|
||||||
</div>
|
<span class="tabular-nums text-slate-700 dark:text-slate-300">{{
|
||||||
|
appsCount
|
||||||
|
}}</span>
|
||||||
|
个应用
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,62 +1,87 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6">
|
<div class="flex h-full flex-col gap-5">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<img
|
<div
|
||||||
:src="amberLogo"
|
class="flex h-12 w-12 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-br from-brand/20 to-brand/5 ring-1 ring-slate-200/60 dark:ring-slate-700/50"
|
||||||
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-8 w-8 object-contain" />
|
||||||
/>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col min-w-0">
|
||||||
<span
|
<span
|
||||||
class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400"
|
class="text-[10px] font-medium uppercase tracking-widest text-slate-400 dark:text-slate-500"
|
||||||
>Spark Store</span
|
>Spark Store</span
|
||||||
>
|
>
|
||||||
<span class="text-lg font-semibold text-slate-900 dark:text-white"
|
<span
|
||||||
|
class="truncate text-base font-bold tracking-tight text-slate-800 dark:text-white"
|
||||||
>星火应用商店</span
|
>星火应用商店</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-2xl text-slate-400 hover:bg-slate-100 lg:hidden dark:hover:bg-slate-800"
|
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 lg:hidden dark:hover:bg-slate-800 dark:hover:text-slate-300"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
title="关闭侧边栏"
|
title="关闭侧边栏"
|
||||||
|
aria-label="关闭侧边栏"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
||||||
<StoreModeSwitcher />
|
<StoreModeSwitcher />
|
||||||
|
|
||||||
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
|
<nav
|
||||||
|
class="flex-1 space-y-1 overflow-y-auto scrollbar-muted pr-1"
|
||||||
|
aria-label="分类导航"
|
||||||
|
>
|
||||||
<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="nav-item flex w-full items-center gap-3 rounded-xl border border-transparent px-3.5 py-2.5 text-left text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
||||||
:class="
|
:class="
|
||||||
activeCategory === 'home'
|
activeCategory === 'home'
|
||||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
? 'nav-item-active border-brand/20 bg-brand/10 text-brand dark:bg-brand/20 dark:text-brand-soft'
|
||||||
: ''
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-800 dark:text-slate-400 dark:hover:bg-slate-800/80 dark:hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="selectCategory('home')"
|
@click="selectCategory('home')"
|
||||||
>
|
>
|
||||||
<span>主页</span>
|
<span
|
||||||
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||||
|
:class="
|
||||||
|
activeCategory === 'home'
|
||||||
|
? 'bg-brand/15 text-brand dark:bg-brand/25 dark:text-brand-soft'
|
||||||
|
: 'bg-slate-100/80 text-slate-500 dark:bg-slate-800/80 dark:text-slate-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-home"></i>
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 truncate">主页</span>
|
||||||
</button>
|
</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="nav-item flex w-full items-center gap-3 rounded-xl border border-transparent px-3.5 py-2.5 text-left text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
||||||
:class="
|
:class="
|
||||||
activeCategory === 'all'
|
activeCategory === 'all'
|
||||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
? 'nav-item-active border-brand/20 bg-brand/10 text-brand dark:bg-brand/20 dark:text-brand-soft'
|
||||||
: ''
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-800 dark:text-slate-400 dark:hover:bg-slate-800/80 dark:hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="selectCategory('all')"
|
@click="selectCategory('all')"
|
||||||
>
|
>
|
||||||
<span>全部应用</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"
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||||
|
:class="
|
||||||
|
activeCategory === 'all'
|
||||||
|
? 'bg-brand/15 text-brand dark:bg-brand/25 dark:text-brand-soft'
|
||||||
|
: 'bg-slate-100/80 text-slate-500 dark:bg-slate-800/80 dark:text-slate-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-th-large"></i>
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 truncate">全部应用</span>
|
||||||
|
<span
|
||||||
|
class="rounded-full bg-slate-200/80 px-2.5 py-0.5 text-xs font-semibold tabular-nums text-slate-600 dark:bg-slate-700/80 dark:text-slate-300"
|
||||||
>{{ categoryCounts.all || 0 }}</span
|
>{{ categoryCounts.all || 0 }}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -65,25 +90,31 @@
|
|||||||
v-for="(category, key) in categories"
|
v-for="(category, key) in categories"
|
||||||
:key="key"
|
:key="key"
|
||||||
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="nav-item flex w-full items-center gap-3 rounded-xl border border-transparent px-3.5 py-2.5 text-left text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
||||||
:class="
|
:class="
|
||||||
activeCategory === key
|
activeCategory === key
|
||||||
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
? 'nav-item-active border-brand/20 bg-brand/10 text-brand dark:bg-brand/20 dark:text-brand-soft'
|
||||||
: ''
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-800 dark:text-slate-400 dark:hover:bg-slate-800/80 dark:hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="selectCategory(key)"
|
@click="selectCategory(key)"
|
||||||
>
|
>
|
||||||
<span class="flex flex-col">
|
|
||||||
<span>
|
|
||||||
<div class="text-left">{{ category.zh }}</div>
|
|
||||||
</span>
|
|
||||||
</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"
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||||
|
:class="
|
||||||
|
activeCategory === key
|
||||||
|
? 'bg-brand/15 text-brand dark:bg-brand/25 dark:text-brand-soft'
|
||||||
|
: 'bg-slate-100/80 text-slate-500 dark:bg-slate-800/80 dark:text-slate-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder"></i>
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 truncate text-left">{{ category.zh }}</span>
|
||||||
|
<span
|
||||||
|
class="rounded-full bg-slate-200/80 px-2.5 py-0.5 text-xs font-semibold tabular-nums text-slate-600 dark:bg-slate-700/80 dark:text-slate-300"
|
||||||
>{{ categoryCounts[key] || 0 }}</span
|
>{{ categoryCounts[key] || 0 }}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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"
|
class="fixed inset-x-4 bottom-4 z-40 rounded-2xl border border-slate-200/60 bg-white/95 shadow-xl shadow-slate-200/30 backdrop-blur-xl dark:border-slate-700/50 dark:bg-slate-900/95 dark:shadow-none sm:left-auto sm:right-6 sm:w-[22rem]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between px-5 py-4"
|
class="flex cursor-pointer items-center justify-between px-4 py-3.5 transition hover:bg-slate-50/80 dark:hover:bg-slate-800/50"
|
||||||
@click="toggleExpand"
|
@click="toggleExpand"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex items-center gap-2.5">
|
||||||
class="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
|
<span
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-xl bg-brand/10 text-brand dark:bg-brand/20"
|
||||||
|
>
|
||||||
|
<i class="fas fa-download text-sm"></i>
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100"
|
||||||
|
>下载队列</span
|
||||||
>
|
>
|
||||||
<i class="fas fa-download text-brand"></i>
|
|
||||||
<span>下载队列</span>
|
|
||||||
<span
|
<span
|
||||||
v-if="downloads.length"
|
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"
|
class="rounded-full bg-slate-200/80 px-2 py-0.5 text-xs font-semibold tabular-nums text-slate-600 dark:bg-slate-700/80 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
({{ activeDownloads }}/{{ downloads.length }})
|
{{ activeDownloads }}/{{ downloads.length }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
v-if="downloads.length"
|
v-if="downloads.length"
|
||||||
type="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"
|
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-700 dark:hover:text-slate-300"
|
||||||
title="清除已完成"
|
title="清除已完成"
|
||||||
@click.stop="clearCompleted"
|
@click.stop="clearCompleted"
|
||||||
>
|
>
|
||||||
<i class="fas fa-broom"></i>
|
<i class="fas fa-broom text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-700 dark:hover:text-slate-300"
|
||||||
@click.stop="toggleExpand"
|
@click.stop="toggleExpand"
|
||||||
|
:aria-expanded="isExpanded"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="fas"
|
class="fas text-sm transition-transform"
|
||||||
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"
|
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -49,19 +54,22 @@
|
|||||||
leave-from-class="opacity-100 translate-y-0"
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
leave-to-class="opacity-0 -translate-y-2"
|
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-show="isExpanded"
|
||||||
|
class="max-h-80 overflow-y-auto border-t border-slate-200/60 px-3 py-3 scrollbar-muted dark:border-slate-700/50"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="downloads.length === 0"
|
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"
|
class="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-200/80 py-10 text-slate-500 dark:border-slate-700/80 dark:text-slate-400"
|
||||||
>
|
>
|
||||||
<i class="fas fa-inbox text-3xl"></i>
|
<i class="fas fa-inbox text-2xl opacity-60"></i>
|
||||||
<p class="mt-3 text-sm">暂无下载任务</p>
|
<p class="mt-2 text-sm font-medium">暂无下载任务</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="download in downloads"
|
v-for="download in downloads"
|
||||||
:key="download.id"
|
: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="flex cursor-pointer items-center gap-3 rounded-xl border border-slate-200/60 bg-white/95 p-3 transition hover:border-brand/30 hover:bg-slate-50/80 dark:border-slate-700/50 dark:bg-slate-800/80 dark:hover:border-brand/40 dark:hover:bg-slate-800/90"
|
||||||
:class="
|
:class="
|
||||||
download.status === 'failed'
|
download.status === 'failed'
|
||||||
? 'border-rose-300/70 dark:border-rose-500/40'
|
? 'border-rose-300/70 dark:border-rose-500/40'
|
||||||
@@ -70,7 +78,7 @@
|
|||||||
@click="showDownloadDetail(download)"
|
@click="showDownloadDetail(download)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="h-12 w-12 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800"
|
class="h-11 w-11 shrink-0 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-700/80 ring-1 ring-slate-200/50 dark:ring-slate-600/50"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="download.icon"
|
:src="download.icon"
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-8">
|
||||||
<div v-if="loading" class="text-center text-slate-500">
|
<div
|
||||||
正在加载首页内容…
|
v-if="loading"
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-slate-500 dark:text-slate-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-spinner fa-spin text-2xl mb-3"></i>
|
||||||
|
<span class="text-sm">正在加载首页内容…</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="error"
|
||||||
|
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-6 py-4 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-300"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error" class="text-center text-rose-600">{{ error }}</div>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="grid gap-4 auto-fit-grid">
|
<div class="grid gap-4 auto-fit-grid">
|
||||||
<a
|
<a
|
||||||
@@ -11,29 +20,40 @@
|
|||||||
:key="link.url + link.name"
|
:key="link.url + link.name"
|
||||||
:href="link.type === '_blank' ? undefined : link.url"
|
:href="link.type === '_blank' ? undefined : link.url"
|
||||||
@click.prevent="onLinkClick(link)"
|
@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"
|
class="flex flex-col items-start gap-3 rounded-2xl border border-slate-200/60 bg-white/95 p-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-brand/30 hover:shadow-md dark:border-slate-700/50 dark:bg-slate-800/70 dark:hover:border-brand/40"
|
||||||
:title="link.more as string"
|
:title="link.more as string"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-16 w-full items-center justify-center overflow-hidden rounded-xl bg-slate-50 dark:bg-slate-800/80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="computedImgUrl(link)"
|
:src="computedImgUrl(link)"
|
||||||
class="h-20 w-full object-contain"
|
class="max-h-14 w-auto object-contain"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div class="text-base font-semibold text-slate-900">
|
</div>
|
||||||
|
<div class="text-base font-semibold text-slate-900 dark:text-white">
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500">{{ link.more }}</div>
|
<div class="text-sm text-slate-500 dark:text-slate-400 line-clamp-2">
|
||||||
|
{{ link.more }}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 mt-6">
|
<div class="space-y-6 mt-8">
|
||||||
<section v-for="section in lists" :key="section.title">
|
<section
|
||||||
<div class="flex items-center justify-between">
|
v-for="section in lists"
|
||||||
<h3 class="text-lg font-semibold text-slate-900">
|
:key="section.title"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-bold tracking-tight text-slate-800 dark:text-slate-100 border-b border-slate-200/70 dark:border-slate-700/70 pb-2"
|
||||||
|
>
|
||||||
{{ section.title }}
|
{{ section.title }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
<div class="grid gap-4 auto-fit-grid">
|
||||||
<div class="mt-3 grid gap-4 auto-fit-grid">
|
|
||||||
<AppCard
|
<AppCard
|
||||||
v-for="app in section.apps"
|
v-for="app in section.apps"
|
||||||
:key="app.pkgname"
|
:key="app.pkgname"
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="flex w-full items-center justify-between rounded-xl border border-slate-200/60 bg-white/80 px-3.5 py-2.5 text-sm font-medium text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-700/60 dark:bg-slate-800/80 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:bg-slate-800"
|
||||||
:aria-pressed="themeMode === 'dark'"
|
:aria-pressed="themeMode === 'dark'"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2.5">
|
||||||
<i class="fas" :class="iconClass"></i>
|
<span
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-lg bg-slate-100/80 dark:bg-slate-700/80"
|
||||||
|
>
|
||||||
|
<i class="fas text-sm" :class="iconClass"></i>
|
||||||
|
</span>
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700"
|
class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-200/90 transition dark:bg-slate-600/80"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'inline-block h-4 w-4 rounded-full bg-white shadow transition',
|
'absolute top-1/2 h-5 w-5 -translate-y-1/2 rounded-full bg-white shadow-md ring-2 ring-slate-200/50 transition-all duration-200 dark:ring-slate-600/50',
|
||||||
togglePosition,
|
togglePosition,
|
||||||
]"
|
]"
|
||||||
></span>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,8 +54,8 @@ const iconClass = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const togglePosition = computed(() => {
|
const togglePosition = computed(() => {
|
||||||
if (props.themeMode === "auto") return "translate-x-4";
|
if (props.themeMode === "auto") return "left-1/2 -translate-x-1/2";
|
||||||
if (props.themeMode === "dark") return "translate-x-7";
|
if (props.themeMode === "dark") return "left-6";
|
||||||
return "translate-x-1";
|
return "left-0.5";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-brand to-brand-dark px-3.5 py-2 text-sm font-semibold text-white shadow-md transition hover:shadow-lg hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
|
||||||
@click="handleUpdate"
|
@click="handleUpdate"
|
||||||
title="检查可更新的软件包列表"
|
title="检查可更新的软件包列表"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt text-xs"></i>
|
||||||
<span>软件更新</span>
|
<span>软件更新</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
class="inline-flex items-center gap-2 rounded-xl border border-slate-200/70 bg-white/90 px-3.5 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:border-slate-300 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-200 dark:hover:bg-slate-700/80"
|
||||||
@click="handleSettings"
|
@click="handleSettings"
|
||||||
title="安装设置"
|
title="安装设置"
|
||||||
>
|
>
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog text-xs"></i>
|
||||||
<span>安装设置</span>
|
<span>安装设置</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- <button
|
<!-- <button
|
||||||
|
|||||||
Reference in New Issue
Block a user