惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
N
News and Events Feed by Topic
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
F
Full Disclosure
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
H
Hacker News: Front Page
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
B
Blog RSS Feed
H
Heimdal Security Blog
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
V2EX - 技术
V2EX - 技术
V
Vulnerabilities – Threatpost
Help Net Security
Help Net Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
W
WeLiveSecurity
T
Tenable Blog
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
S
Secure Thoughts
O
OpenAI News
L
LINUX DO - 热门话题
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
IT之家
IT之家
Latest news
Latest news
Cloudbric
Cloudbric

Hyde Blog

Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog 关于我 关于我 Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog
Hyde Blog
Hyde · 2026-05-19 · via Hyde Blog

设备页面 ​


前言 ​

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

siteConfig.tsnavBarConfig.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

config.tslink-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

i18nKey.tsen.tszh_CN.tszh_TW.tsja.tsru.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]: "查看详情",
}

配置教程 ​

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