设备页面
前言
Firefly 主题移植Mizuki了一个简洁的设备展示(Devices)页面,用于展示您使用或拥有的数码设备。这个页面可以帮助访客了解您的设备偏好和技术环境。
本文档详细介绍如何将日记页面功能移植到 Firefly-Hyde 主题中。
在开始之前,请确保你已经按照文件结构创建好对应的目录和文
📁文件结构
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
// 设备页面处理脚本
// 此脚本作为全局脚本加载,不受 Swup 页面切换影响
(() => {
if (typeof window.devicesPageState === "undefined") {
window.devicesPageState = {
eventListeners: [],
mutationObserver: null,
};
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/\"/g, """)
.replace(/'/g, "'");
}
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);
});
}
})();创建设备卡片组件
---
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
// 页面开关配置
pages: {
// ... 其他配置
// 设备页面开关
devices: true,
},// 我的及其子菜单
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
export type SiteConfig = {
// 页面开关配置
pages: {
diary?: boolean; // 日记页面开关
};
}
export enum LinkPreset {
Devices = 8, //✨ 新增
}export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
[LinkPreset.Devices]: {
name: i18n(I18nKey.devices),
url: "/devices/",
icon: "material-symbols:devices",
},
}设备数据管理
文件路径:src/data/devices.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
export enum I18nKey {
// 设备页面
devices = "devices",
devicesSubtitle = "devicesSubtitle",
devicesViewDetails = "devicesViewDetails",
} // 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",// 设备页面
[Key.devices]: "我的设备",
[Key.devicesSubtitle]: "这里展示了我日常使用的各类设备",
[Key.devicesViewDetails]: "查看详情", // 設備頁面
[Key.devices]: "我的設備",
[Key.devicesSubtitle]: "這裡展示了我日常使用的各類設備",
[Key.devicesViewDetails]: "查看詳情", // デバイスページ
[Key.devices]: "デバイス",
[Key.devicesSubtitle]: "日常的に使用しているデバイスを紹介",
[Key.devicesViewDetails]: "詳細を表示",
[Key.albumsPhotoCount]: "件の写真",
[Key.albumsPhotosCount]: "件の写真",
[Key.albumsNoResults]: "一致するアルバムはありません", // Страница устройств
[Key.devices]: "Устройства",
[Key.devicesSubtitle]: "Здесь показаны устройства, которые я использую ежедневно",
[Key.devicesViewDetails]: "Подробнее",新增设备页面组件
文件路径:src/pages/devices.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目录下修改对应的语言文件
export const zh_CN: Translation = {
// 设备页面
[Key.devices]: "设备",
[Key.devicesSubtitle]: "我的设备清单",
[Key.devicesViewDetails]: "查看详情",
}配置教程
有关设备页面配置教程,请参考 设备页面配置教程
