Skip to content
0

设备页面


前言

Firefly 主题移植Mizuki了一个简洁的设备展示(Devices)页面,用于展示您使用或拥有的数码设备。这个页面可以帮助访客了解您的设备偏好和技术环境。

  • 本文档详细介绍如何将日记页面功能移植到 Firefly-Hyde 主题中。

  • 在开始之前,请确保你已经按照文件结构创建好对应的目录和文

📁文件结构

md
Firefly/
├── public/
│   └── js/
│   │   └── devices-page-handler.js  # 设备页面处理脚本
├── src/
│   ├── components/      # 组件目录
│   │   ├── features/    # 特性组件目录
│   │   │   └── devices/ # 设备组件目录
│   │   │   │   └── DeviceCard.astro # 设备卡片组件
│   ├── config/          # 配置文件目录
│   │   └── navBarConfig.ts # 导航栏配置文件
│   │   └── siteConfig.ts # 站点基础配置文件
│   ├── constants/       # 常量目录
│   │   └── link-presets.ts # 链接预设常量
│   ├── data/            # 数据目录
│   │   └── devices.ts # 设备数据文件
│   ├── i18n/           # 国际化目录
│   │   ├── languages/ # 语言目录
│   │   │   └── en.ts # 英文语言文件
│   │   │   └── ja.ts # 日文语言文件
│   │   │   └── ru.ts # 俄文语言文件
│   │   │   └── zh_CN.ts # 中文语言文件
│   │   │   └── zh_TW.ts # 中文繁体语言文件
│   │   ├── i18nKey.ts # 国际化键值文件
│   ├── pages/           # 页面目录
│   │   └── devices.astro # 设备页面文件
│   ├── types/           # 类型定义目录
│   │   └── config.ts   # 配置类型定义文件
├── public/              # 静态资源

创建设备页面脚本

文件路径public/js/devices-page-handler.js

js
// 设备页面处理脚本
// 此脚本作为全局脚本加载,不受 Swup 页面切换影响

(() => {
	if (typeof window.devicesPageState === "undefined") {
		window.devicesPageState = {
			eventListeners: [],
			mutationObserver: null,
		};
	}

	function escapeHtml(value) {
		return String(value ?? "")
			.replace(/&/g, "&")
			.replace(/</g, "&lt;")
			.replace(/>/g, "&gt;")
			.replace(/\"/g, "&quot;")
			.replace(/'/g, "&#39;");
	}

	function cleanupListeners() {
		const state = window.devicesPageState;
		for (let i = 0; i < state.eventListeners.length; i++) {
			const [element, type, handler] = state.eventListeners[i];
			if (element && element.removeEventListener) {
				element.removeEventListener(type, handler);
			}
		}
		state.eventListeners = [];
	}

	function createDeviceCardHTML(device, index, viewDetailsText) {
		const imgSection =
			'<div class="relative p-6 pb-0"><div class="flex justify-center items-center h-48 bg-gradient-to-br from-[var(--card-bg)] to-[var(--btn-regular-bg)] rounded-lg overflow-hidden relative"><div class="absolute inset-0 bg-[var(--primary)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div><img src="' +
			escapeHtml(device.image) +
			'" alt="' +
			escapeHtml(device.name) +
			'" class="w-auto h-full max-h-full object-contain group-hover:scale-110 transition-all duration-500 drop-shadow-md relative z-10" loading="lazy"></div></div>';

		const priceSection = device.price
			? '<div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gradient-to-r from-[var(--primary)]/10 to-[var(--primary)]/5 text-[var(--primary)] text-sm font-bold mb-3 w-fit"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg><span>' +
			  escapeHtml(device.price) +
			  '</span></div>'
			: "";

		const infoSection =
			'<div class="p-6 pt-4 relative z-10"><div class="flex items-start justify-between mb-3"><h3 class="text-lg font-bold text-black/90 dark:text-white/90 group-hover:text-[var(--primary)] transition-colors duration-300">' +
			escapeHtml(device.name) +
			'</h3><div class="p-1.5 rounded-full bg-[var(--primary)]/10 text-[var(--primary)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"><svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg></div></div><div class="mb-4"><div class="flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--btn-regular-bg)] text-black/70 dark:text-white/70 text-sm mb-3 w-fit"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg><span class="font-medium">' +
			escapeHtml(device.specs) +
			'</span></div>' +
			priceSection +
			'<p class="text-sm text-black/60 dark:text-white/60 leading-relaxed line-clamp-2">' +
			escapeHtml(device.description) +
			'</p></div><div class="flex items-center justify-between pt-3 border-t border-[var(--line-divider)] border-dashed opacity-0 group-hover:opacity-100 transition-all duration-300"><span class="text-sm font-medium text-[var(--primary)]">' +
			escapeHtml(viewDetailsText) +
			'</span><svg class="w-5 h-5 text-[var(--primary)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg></div></div>';

		return (
			'<a href="' +
			escapeHtml(device.link) +
			'" target="_blank" rel="noopener noreferrer" class="device-card group relative overflow-hidden rounded-xl border border-[var(--line-divider)] bg-[var(--card-bg)] transition-all duration-300 hover:border-[var(--primary)]/50 hover:shadow-md hover:shadow-black/5 dark:hover:shadow-white/5 hover:scale-[1.02] hover:-translate-y-0.5 block cursor-pointer" style="animation-delay:' +
			index * 100 +
			'ms; animation: fadeInUp 0.6s cubic-bezier(0.25, 0.1, 0.25, 1) forwards; opacity: 0;">' +
			imgSection +
			infoSection +
			"</a>"
		);
	}

	function initDevicesPage() {
		const brandTabs = document.querySelectorAll(".filter-tag[data-brand]");
		const devicesContainer = document.getElementById("devices-container");
		const devicesDataElement = document.getElementById("devices-data");
		const i18nDataElement = document.getElementById("i18n-data");

		if (
			!brandTabs.length ||
			!devicesContainer ||
			!devicesDataElement ||
			!i18nDataElement
		) {
			return false;
		}

		const devicesData = JSON.parse(devicesDataElement.textContent || "{}");
		const i18nData = JSON.parse(i18nDataElement.textContent || "{}");

		cleanupListeners();

		brandTabs.forEach((tab) => {
			const clickHandler = () => {
				const brand = tab.dataset.brand;
				if (!brand) {
					return;
				}

				brandTabs.forEach((item) => item.classList.remove("active"));
				tab.classList.add("active");

				const brandDevices = devicesData[brand] || [];
				devicesContainer.innerHTML = brandDevices
					.map((device, index) =>
						createDeviceCardHTML(
							device,
							index,
							i18nData.viewDetails || "",
						),
					)
					.join("");
			};

			tab.addEventListener("click", clickHandler);
			window.devicesPageState.eventListeners.push([
				tab,
				"click",
				clickHandler,
			]);
		});

		return true;
	}

	function tryInit(retries) {
		retries = retries || 0;
		if (initDevicesPage()) {
			return;
		}
		if (retries < 5) {
			setTimeout(() => {
				tryInit(retries + 1);
			}, 100);
		}
	}

	function setupMutationObserver() {
		if (window.devicesPageState.mutationObserver) {
			window.devicesPageState.mutationObserver.disconnect();
		}

		window.devicesPageState.mutationObserver = new MutationObserver(
			(mutations) => {
				let shouldInit = false;

				for (let i = 0; i < mutations.length; i++) {
					const mutation = mutations[i];
					if (
						!mutation.addedNodes ||
						mutation.addedNodes.length === 0
					) {
						continue;
					}

					for (let j = 0; j < mutation.addedNodes.length; j++) {
						const node = mutation.addedNodes[j];
						if (node.nodeType !== 1) {
							continue;
						}

						if (
							node.id === "devices-container" ||
							node.id === "devices-data" ||
							(node.querySelector &&
								(node.querySelector("#devices-container") ||
									node.querySelector("#devices-data")))
						) {
							shouldInit = true;
							break;
						}
					}

					if (shouldInit) {
						break;
					}
				}

				if (shouldInit) {
					setTimeout(() => {
						tryInit();
					}, 50);
				}
			},
		);

		window.devicesPageState.mutationObserver.observe(document.body, {
			childList: true,
			subtree: true,
		});
	}

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", () => {
			tryInit();
		});
	} else {
		tryInit();
	}

	setupMutationObserver();

	const events = [
		"swup:contentReplaced",
		"swup:pageView",
		"astro:page-load",
		"astro:after-swap",
		"mizuki:page:loaded",
	];

	for (let i = 0; i < events.length; i++) {
		const eventName = events[i];
		document.addEventListener(eventName, () => {
			setTimeout(() => {
				tryInit();
			}, 100);
		});
	}
})();

创建设备卡片组件

DeviceCard.astro
astro
---
import type { Device } from "../../../data/devices";

interface Props {
	device: Device;
	index: number;
	viewDetailsText: string;
}

const { device, index, viewDetailsText } = Astro.props;
---

<a
	href={device.link}
	target="_blank"
	rel="noopener noreferrer"
	class="device-card group relative overflow-hidden rounded-xl border border-[var(--line-divider)] bg-[var(--card-bg)] transition-all duration-300 hover:border-[var(--primary)]/50 hover:shadow-md hover:shadow-black/5 dark:hover:shadow-white/5 hover:scale-[1.02] hover:-translate-y-0.5 block cursor-pointer"
	style={`animation-delay: ${index * 100}ms`}
>
	<!-- 设备图片区域 -->
	<div class="relative p-6 pb-0">
		<div
			class="flex justify-center items-center h-48 bg-gradient-to-br from-[var(--card-bg)] to-[var(--btn-regular-bg)] rounded-lg overflow-hidden relative"
		>
			<div
				class="absolute inset-0 bg-[var(--primary)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
			>
			</div>
			<img
				src={device.image}
				alt={device.name}
				class="w-auto h-full max-h-full object-contain group-hover:scale-110 transition-all duration-500 drop-shadow-md relative z-10"
				loading="lazy"
			/>
		</div>
	</div>

	<!-- 设备信息区域 -->
	<div class="p-6 pt-4 relative z-10">
		<div class="flex items-start justify-between mb-3">
			<h3
				class="text-lg font-bold text-black/90 dark:text-white/90 group-hover:text-[var(--primary)] transition-colors duration-300"
			>
				{device.name}
			</h3>
			<div
				class="p-1.5 rounded-full bg-[var(--primary)]/10 text-[var(--primary)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"
			>
				<svg
					class="w-5 h-5"
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
					></path>
				</svg>
			</div>
		</div>

		<div class="mb-4">
			<div
				class="flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--btn-regular-bg)] text-black/70 dark:text-white/70 text-sm mb-3 w-fit"
			>
				<svg
					class="w-4 h-4"
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
					></path>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
				</svg>
				<span class="font-medium">{device.specs}</span>
			</div>

			<!-- 价格显示 -->
			{device.price && (
				<div
					class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gradient-to-r from-[var(--primary)]/10 to-[var(--primary)]/5 text-[var(--primary)] text-sm font-bold mb-3 w-fit"
				>
					<svg
						class="w-4 h-4"
						xmlns="http://www.w3.org/2000/svg"
						fill="none"
						viewBox="0 0 24 24"
						stroke="currentColor"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							stroke-width="2"
							d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
						></path>
					</svg>
					<span>{device.price}</span>
				</div>
			)}

			<p
				class="text-sm text-black/60 dark:text-white/60 leading-relaxed line-clamp-2"
			>
				{device.description}
			</p>
		</div>

		<!-- 查看详情按钮 -->
		<div
			class="flex items-center justify-between pt-3 border-t border-[var(--line-divider)] border-dashed opacity-0 group-hover:opacity-100 transition-all duration-300"
		>
			<span class="text-sm font-medium text-[var(--primary)]"
				>{viewDetailsText}</span
			>
			<svg
				class="w-5 h-5 text-[var(--primary)]"
				xmlns="http://www.w3.org/2000/svg"
				fill="none"
				viewBox="0 0 24 24"
				stroke="currentColor"
			>
				<path
					stroke-linecap="round"
					stroke-linejoin="round"
					stroke-width="2"
					d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
			</svg>
		</div>
	</div>
</a>

<style>
	.device-card {
		animation: fadeInUp 0.6s cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
		opacity: 0;
	}

	.line-clamp-2 {
		display: -webkit-box;
		-webkit-line-clamp: 2;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}
</style>

页面开关导航栏配置

  • SiteConfig 类型中添加页面开关配置,文件路径src/config/siteConfig.ts

  • navBarConfig 类型中更新导航栏配置,文件路径src/config/navBarConfig.ts

ts
// 页面开关配置
pages: {
    // ... 其他配置
    // 设备页面开关
	devices: true,
},
ts
// 我的及其子菜单
	links.push({
		name: "我的",
		url: "/my/",
		icon: "material-symbols:person",
		children: [
		// 根据配置决定是否添加设备,在siteConfig关闭pages.devices时导航栏不显示设备
			...(siteConfig.pages.devices ? [LinkPreset.Devices] : []),
		],
	});

更新类型配置链接

  • types 类型的 config 中添加类型定义,文件路径src/types/config.ts

  • constants 类型的 link-presets 添加到导航链接,文件路径src/constants/link-presets.ts

ts
export type SiteConfig = {
	// 页面开关配置
	pages: {
	diary?: boolean; // 日记页面开关		
	};
}

export enum LinkPreset {
	Devices = 8,  //✨ 新增
}
ts
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
	[LinkPreset.Devices]: {
		name: i18n(I18nKey.devices),
		url: "/devices/",
		icon: "material-symbols:devices",
	},
}

设备数据管理

文件路径src/data/devices.ts

ts
// 设备数据配置文件

export interface Device {
	name: string;
	image: string;
	specs: string;
	description: string;
	link: string;
	price?: string;
}

// 设备类别类型,支持品牌和自定义类别
export type DeviceCategory = Record<string, Device[]> & {
	自定义?: Device[];
};

export const devicesData: DeviceCategory = {
	数码: [
		{
			name: "iPhone 17 Pro",
			image: "/images/device/iPhone 17 Pro.webp",
			specs: "深蓝色 / 12G + 256G",
			description:
				"创新设计,打造巅峰性能和超长续航",
			link: "https://www.apple.com.cn/iphone-17-pro/",
			price:"8999元"
		},
		{
			name: "Xiaomi 17 Ultra 徕卡版",
			image: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1766544998.81921201.png",
			specs: "米白色/16G+1TB",
			description:
				"聚焦所见,忠于表达",
			link: "https://www.mi.com/shop/buy/detail?product_id=22423",
			price:"8999元"
		},
		{
			name: "XiaoMi 10 Pro",
			image: "/images/device/MI 10Pro.webp",
			specs: "珍珠白/12G+256G",
			description:
				"小米十周年梦幻之作",
			link: "https://www.mi.com/hk/buy/product/mi-10-pro?gid=4201400021",
			price:"4783元"
		},
		{
			name: "XiaoMi 6",
			image: "/images/device/XiaoMi 6.webp",
			specs: "黑色 / 6G + 64G",
			description:
				"变焦双摄,拍人更美",
			link: "https://www.mi.com/mi6",
			price:"2499元"
		},
		{
			name: "OPPO Enco Air4 Pro",
			image: "/images/device/OPPO Enco Air4 Pro.webp",
			specs: "晨曦白",
			description:
				"真无线降噪蓝牙耳机",
			link: "https://www.opposhop.cn/cn/web/products/27614.html",
			price:"219元"
		},
		{
			name: "小米AI音箱(第二代)",
			image: "/images/device/小米AI音箱(第二代).webp",
			specs: "白色",
			description:
				"经典延续,体验升级",
			link: "https://www.mi.com/shop/buy/detail?product_id=13878",
			price:"179元"
		},
	],
	运动相机:[
		{
			name: "影石Insta360 Ace Pro 2",
			image: "/images/device/Insta360 Ace Pro 2.webp",
			specs: "极夜黑 / 街拍银灰",
			description:
				"AI双芯,旗舰影像",
			link: "https://store.insta360.com/cn/product/ace-pro-2?c=3611&from=nav_product",
			price:"2359元"
		},
		{
			name: "Osmo Pocket 4",
			image: "/images/device/Osmo Pocket 4.webp",
			specs: "标准套装",
			description:
				"一寸万象,光影随行",
			link: "https://www.dji.com/cn/osmo-pocket-4",
			price:"2999元"
		},
	],
	路由器: [
		{
			name: "RG-X30E",
			image: "/images/device/RG-X30E.webp",
			specs: "1000Mbps / 2.5G",
			description:
				"锐捷雪豹电竞WiFi 6 路由器",
			link: "https://item.jd.com/100084856711.html",
			price:"109元"
		},
	],
};

添加国际化翻译

  • 添加翻译键,文件路径src/i18n/i18nKey.ts

  • 添加英文翻译,文件路径src/i18n/languages/en.ts

  • 添加中文翻译,文件路径src/i18n/languages/zh_CN.ts

  • 添加繁体中文翻译,文件路径src/i18n/languages/zh_TW.ts

  • 添加日文翻译,文件路径src/i18n/languages/ja.ts

  • 添加俄文翻译,文件路径src/i18n/languages/ru.ts

ts
export enum I18nKey {
    // 设备页面
	devices = "devices",
	devicesSubtitle = "devicesSubtitle",
	devicesViewDetails = "devicesViewDetails",
}
ts
	// Devices Page
	[Key.devices]: "My Devices",
	[Key.devicesSubtitle]: "Here are the devices I use in my daily life",
	[Key.devicesViewDetails]: "View Details",
	[Key.albumsPhotoCount]: "photo",
	[Key.albumsPhotosCount]: "photos",
	[Key.albumsNoResults]: "No matching albums",
ts
// 设备页面
	[Key.devices]: "我的设备",
	[Key.devicesSubtitle]: "这里展示了我日常使用的各类设备",
	[Key.devicesViewDetails]: "查看详情",
ts
	// 設備頁面
	[Key.devices]: "我的設備",
	[Key.devicesSubtitle]: "這裡展示了我日常使用的各類設備",
	[Key.devicesViewDetails]: "查看詳情",
ts
	// デバイスページ
	[Key.devices]: "デバイス",
	[Key.devicesSubtitle]: "日常的に使用しているデバイスを紹介",
	[Key.devicesViewDetails]: "詳細を表示",
	[Key.albumsPhotoCount]: "件の写真",
	[Key.albumsPhotosCount]: "件の写真",
	[Key.albumsNoResults]: "一致するアルバムはありません",
ts
	// Страница устройств
	[Key.devices]: "Устройства",
	[Key.devicesSubtitle]: "Здесь показаны устройства, которые я использую ежедневно",
	[Key.devicesViewDetails]: "Подробнее",

新增设备页面组件

文件路径src/pages/devices.astro

astro
---
import DeviceCard from "@components/features/devices/DeviceCard.astro";

import { siteConfig } from "../config/siteConfig";
import { devicesData } from "../data/devices";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import MainGridLayout from "../layouts/MainGridLayout.astro";

export const prerender = true;

// 检查设备页面是否启用
if (!siteConfig.pages.devices) {
	return Astro.redirect("/404/");
}

// 设备数据
const devices = devicesData;
const brands = Object.keys(devices);
const viewDetailsText = i18n(I18nKey.devicesViewDetails);
---

<MainGridLayout title={i18n(I18nKey.devices)}>
	<!-- 引入右侧边栏布局管理器 -->
	<!-- <script>
		import("../scripts/right-sidebar-layout.js");
	</script> -->
	<div
		class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32"
	>
		<div class="card-base z-10 px-9 py-6 relative w-full">
			<!-- 页面标题 -->
			<div class="flex flex-col items-start justify-center mb-8">
				<h1
					class="text-4xl font-bold text-black/90 dark:text-white/90 mb-2 relative before:w-1 before:h-8 before:rounded-md before:bg-[var(--primary)] before:absolute before:top-1/2 before:-translate-y-1/2 before:-left-4"
				>
					{i18n(I18nKey.devices)}
				</h1>
				<p class="text-lg text-black/60 dark:text-white/60">
					{i18n(I18nKey.devicesSubtitle)}
				</p>
			</div>

			<!-- 过滤按钮 -->
			<div class="mb-6">
				<div class="filter-container flex flex-wrap gap-2">
					{
						brands.map((brand, index) => (
							<button
								data-brand={brand}
								class={`filter-tag px-6 py-2.5 rounded-lg font-medium transition-all ${
									index === 0 ? "active" : ""
								}`}
							>
								{brand}
							</button>
						))
					}
				</div>
			</div>

			<!-- 设备列表 -->
			<div
				id="devices-container"
				class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
			>
				{
					brands.length > 0 &&
						devices[brands[0]].map((device, index) => (
							<DeviceCard
								device={device}
								index={index}
								viewDetailsText={viewDetailsText}
							/>
						))
				}
			</div>

			<!-- 隐藏数据用于客户端渲染 -->
			<script
				is:inline
				id="devices-data"
				type="application/json"
				set:html={JSON.stringify(devices)}
			/>
			<script
				is:inline
				id="i18n-data"
				type="application/json"
				set:html={JSON.stringify({ viewDetails: viewDetailsText })}
			/>
		</div>
	</div>

	<!-- 使用全局脚本,确保 swup 切换后仍能重新初始化 -->
	<script is:inline src="/js/devices-page-handler.js"></script>
</MainGridLayout>

<style>
	.filter-container {
		display: flex;
		flex-wrap: wrap;
		gap: 0.75rem;
		margin-bottom: 1rem;
	}

	.filter-tag {
		padding: 0.625rem 1.25rem;
		border: 1px solid var(--line-divider);
		border-radius: var(--radius-large);
		background: var(--btn-regular-bg);
		color: var(--btn-content);
		font-size: 0.875rem;
		font-weight: 500;
		cursor: pointer;
		transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
		white-space: nowrap;
		position: relative;
		overflow: hidden;
	}

	.filter-tag::before {
		content: "";
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background: var(--primary);
		opacity: 0;
		transition: opacity 0.3s ease;
		z-index: -1;
	}

	.filter-tag:hover:not(.active) {
		background: var(--btn-hover-bg);
		border-color: var(--primary);
		transform: translateY(-2px);
		box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
	}

	.filter-tag.active {
		background: var(--primary);
		color: white;
		border-color: var(--primary);
		transform: translateY(-1px);
		box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
	}

	.filter-tag.active:hover {
		transform: translateY(-2px);
		box-shadow: 0 6px 16px rgba(var(--color-primary-rgb), 0.4);
	}

	/* 设备卡片动画效果 */
	@keyframes fadeInUp {
		from {
			opacity: 0;
			transform: translateY(20px);
		}
		to {
			opacity: 1;
			transform: translateY(0);
		}
	}

	.onload-animation {
		animation: fadeInUp 0.6s cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
		opacity: 0;
	}

	/* 卡片内容文本截断 */
	.line-clamp-2 {
		display: -webkit-box;
		-webkit-line-clamp: 2;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}

	/* 设备卡片微妙的悬停效果 */
	.device-card::after {
		content: "";
		position: absolute;
		inset: 0;
		border-radius: 0.75rem;
		background: linear-gradient(
			135deg,
			rgba(var(--color-primary-rgb), 0.05),
			transparent
		);
		opacity: 0;
		transition: opacity 0.4s ease;
		pointer-events: none;
	}

	.device-card:hover::after {
		opacity: 1;
	}

	/* 设备名称和规格的文本样式 */
	.device-card h3 {
		text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
	}

	/* 响应式调整 */
	@media (max-width: 768px) {
		.filter-container {
			gap: 0.5rem;
		}

		.filter-tag {
			padding: 0.5rem 1rem;
			font-size: 0.8rem;
		}

		.device-card .relative.p-6\.pb-0 {
			padding: 1rem 1rem 0;
		}

		.device-card .p-6\.pt-4 {
			padding: 1rem;
		}
	}

	/* 在深色模式下微妙的阴影效果 */
	@media (prefers-color-scheme: dark) {
		.device-card:hover {
			box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
		}
	}
</style>

修改名称

  • src\i18n\languages\zh_CN.ts 中的 对应 字段,修改为你想要的名称

提示

如果对应其他语言文件也需要修改,请在src\i18n\languages目录下修改对应的语言文件

ts
export const zh_CN: Translation = {
	// 设备页面
	[Key.devices]: "设备",
	[Key.devicesSubtitle]: "我的设备清单",
	[Key.devicesViewDetails]: "查看详情",
}

配置教程

有关设备页面配置教程,请参考 设备页面配置教程

最近更新