v0.10.25: /solution/ 归档+落地页复刻(archive 组件/分页/摘录24词)、Header 全断点对齐、page_css 导入管线、reviews/populated 宽度/_css_classes/tabs HTML/图片变体修复

This commit is contained in:
bigcode
2026-11-03 03:59:04 +08:00
parent 7559977ae1
commit 9a51994860
17 changed files with 681 additions and 146 deletions
+66 -29
View File
@@ -291,15 +291,18 @@ export function ElementorRenderer({
postsData,
shortcodes,
vars,
pageCss,
}: {
doc: ElDocument;
postsData?: Record<string, PostCardItem[]>;
shortcodes?: Record<string, ReactNode>;
vars?: RendererVars;
/** # 成员变量 页面级生成 CSS(原站 post-<id>.css 翻译成 data-el 选择器,scripts/import-page-css.ts 导入) **/
pageCss?: string;
}) {
const elements = normalizeDoc(doc);
if (!elements.length) return null;
const css = collectCustomCss(elements) + collectResponsiveCss(elements);
const css = (pageCss || "") + collectCustomCss(elements) + collectResponsiveCss(elements);
return (
<VarsContext.Provider value={vars || {}}>
<PostsDataContext.Provider value={postsData || {}}>
@@ -527,8 +530,10 @@ function SectionNode({ el }: { el: ElElement }) {
if (overlay) style.position = style.position || "relative";
const Tag = sectionTag(settings);
const vis = visibilityClass(settings);
// _css_classes(页面级 custom_css 的选择器挂在节点的 class 上,如 ele-secondDep-dif)
const secCls = [vis, s(settings._css_classes)].filter(Boolean).join(" ") || undefined;
return (
<Tag className={vis || undefined} style={style} data-el={el.id}>
<Tag className={secCls} style={style} data-el={el.id}>
{overlay}
{/* 行加 relative:与绝对定位遮罩同属定位层,DOM 靠后绘制在上,内容不被蒙灰 */}
<div className="mx-auto flex flex-wrap" style={{ ...inner, position: "relative" }}>
@@ -605,13 +610,13 @@ function ColumnNode({ el, gapPad, contentPos }: { el: ElElement; gapPad?: string
} else inner.borderWidth = "1px";
}
return (
// 内层(populated)宽 100%:与列同宽、水平 margin 向右溢出(H4 卡实测 populated=272 含
// margin 溢出);H11/H14/H15 等"扣除 margin"语义的列在 app.css 用 !important 覆写为 auto。
// 空列(无子元素)裸渲染:原站空列无 populated 盒(H15 占位格 margin/边框均为 0),
// 仅保留外层弹性宽度与拉伸高度
<div className={"el-col min-w-0 " + visibilityClass(settings)} style={outer} data-el={el.id}>
// 内层(populated)块级宽 auto:与原站 .elementor-element-populated 一致——水平 margin
// 存在时块盒收缩进列宽(6c765c4 卡列 margin 8/8 实测 populated=294=310-16、图片 272);
// margin 为 0 时与 100% 等价;负 margin 自然外扩(H4 图)。空列(无子元素)裸渲染:
// 原站空列无 populated 盒(H15 占位格 margin/边框均为 0),仅保留外层弹性宽度与拉伸高度
<div className={("el-col min-w-0 " + [visibilityClass(settings), s(settings._css_classes)].filter(Boolean).join(" ")).trimEnd()} style={outer} data-el={el.id}>
{(el.elements || []).length ? (
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", width: "100%", ...inner }}>
<div style={{ display: "flex", flexWrap: "wrap", alignContent: "flex-start", ...inner }}>
{(el.elements || []).map((c, i) => (
<ElementNode key={c.id || i} el={c} gapPad={gapPad} />
))}
@@ -1610,28 +1615,62 @@ function TestimonialWidget({ settings }: { settings: Record<string, unknown> })
);
}
/** # 组件 reviews(评价轮播):静态卡片网格(slides 复用:name/title/rating/content/image) */
/** # 组件 reviews(Elementor 原生 reviews widget,swiper 轮播静态化)
* 取证(smart-building 07ecb19,1440 实测 2026-11-03):
* 外层左右 28px 留白(箭头槽);slide 宽 (100%-10)/2(slides_per_view=2、gap 10),
* 白底 radius 5;header flex pad 20 20 18(图 79×79 圆形 + cite 左 20:姓名 24px/700 #1C2022、
* 星 14px #CCD6DF 右距 5、职位 16px #697882);content pad 18 20 20,内容 HTML
* (.ele-sol-review-box 等)由页面级 CSS(pageCss / custom_css)提供。
*/
function ReviewsWidget({ settings }: { settings: Record<string, unknown> }) {
const slides = Array.isArray(settings.slides)
? (settings.slides as { name?: string; title?: string; rating?: string; content?: string; image?: { url?: string; alt?: string } }[])
? (settings.slides as { _id?: string; name?: string; title?: string; rating?: string; content?: string; image?: { url?: string; alt?: string } }[])
: [];
if (!slides.length) return null;
const stars = (n: string) => "★★★★★".slice(0, Math.max(1, Math.min(5, Math.round(Number(n) || 5))));
const perView = Math.max(1, Math.round(Number(s(settings.slides_per_view)) || 2));
return (
<div className="mb-[18px] grid gap-4 min-[760px]:grid-cols-2">
{slides.map((it, i) => (
<div key={i} className="rounded-md border p-4">
<div className="text-[var(--brand)]">{stars(s(it.rating))}</div>
{s(it.content) ? <div className="content-html mt-2" dangerouslySetInnerHTML={{ __html: s(it.content) }} /> : null}
<div className="mt-3 flex items-center gap-3">
{it.image?.url ? <img src={it.image.url} alt={it.image.alt || it.name || ""} className="h-10 w-10 rounded-full object-cover" /> : null}
<div>
<div className="font-medium">{s(it.name)}</div>
<div className="text-sm text-muted-foreground">{s(it.title)}</div>
<div className="el-reviews">
<style>{`
.el-reviews .el-swiper { padding: 0 28px; }
.el-reviews .el-swiper-wrapper { display: grid; grid-template-columns: repeat(${perView}, 1fr); gap: 10px; }
.el-reviews .swiper-slide { background: ${s(settings.slide_background_color) || "#fff"}; border-radius: 5px; }
.el-reviews .elementor-testimonial__header { display: flex; align-items: center; padding: 20px 20px 18px; }
.el-reviews .elementor-testimonial__image { flex: 0 0 auto; display: flex; }
.el-reviews .elementor-testimonial__image img { display: block; width: 79px; height: 79px; border-radius: 999px; object-fit: cover; }
.el-reviews .elementor-testimonial__cite { display: flex; flex-direction: column; margin: 0 0 0 20px; font-style: normal; }
.el-reviews .elementor-testimonial__name { font-size: ${sizeCss(settings.name_typography_font_size) || "24px"}; line-height: 1.5; font-weight: 700; color: #1C2022; }
.el-reviews .elementor-star-rating { font-size: 14px; line-height: 21px; color: #CCD6DF; white-space: nowrap; }
.el-reviews .elementor-star-rating i { display: inline-block; font-style: normal; margin-right: 5px; }
.el-reviews .elementor-testimonial__title { font-size: 16px; color: #697882; }
.el-reviews .elementor-testimonial__content { padding: 18px 20px 20px; font-size: ${sizeCss(settings.content_typography_font_size) || "16px"}; line-height: ${sizeCss(settings.content_typography_line_height) || "24px"}; }
`}</style>
<div className="el-swiper">
<div className="el-swiper-wrapper">
{slides.map((it, i) => (
<div key={it._id || i} className="swiper-slide">
<div className="elementor-testimonial">
<div className="elementor-testimonial__header">
<div className="elementor-testimonial__image">
{it.image?.url ? <img src={it.image.url} alt={it.image.alt || it.name || ""} loading="lazy" /> : null}
</div>
<cite className="elementor-testimonial__cite">
<span className="elementor-testimonial__name">{s(it.name)}</span>
<div className="elementor-star-rating" aria-label={it.rating}>
{[1, 2, 3, 4, 5].map((n) => (
<i key={n} className={"elementor-star-" + (n <= Math.round(Number(it.rating) || 5) ? "full" : "empty")}>★</i>
))}
</div>
<span className="elementor-testimonial__title">{s(it.title)}</span>
</cite>
</div>
<div className="elementor-testimonial__content">
<div className="elementor-testimonial__text" dangerouslySetInnerHTML={{ __html: localSiteUrl(s(it.content)) }} />
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
</div>
);
}
@@ -1944,9 +1983,8 @@ function TabsWidget({ settings }: { settings: Record<string, unknown> }) {
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</div>
dangerouslySetInnerHTML={{ __html: s(t.tab_title) }}
/>
))}
</div>
<div className="elementor-tabs-content-wrapper" role="tablist">
@@ -1959,9 +1997,8 @@ function TabsWidget({ settings }: { settings: Record<string, unknown> }) {
tabIndex={i === active ? 0 : -1}
aria-selected={i === active}
onClick={() => setActive(i)}
>
{s(t.tab_title)}
</div>
dangerouslySetInnerHTML={{ __html: s(t.tab_title) }}
/>
<div
id={`elementor-tab-content-${base + i}`}
className="elementor-tab-content elementor-clearfix"
+6
View File
@@ -126,6 +126,12 @@ const SETTING_KEYS = new Set([
"social_icon_list",
"slides",
"slides_height",
// reviews widget(评价轮播静态化;smart-building 07ecb19)
"slides_per_view",
"slide_background_color",
"name_typography_font_size",
"content_typography_font_size",
"content_typography_line_height",
"tabs",
"tabs_align_horizontal",
"tab_active_color",
+2 -2
View File
@@ -77,8 +77,8 @@ function NavigationMenuHeading({ className, ...props }: React.ComponentProps<"di
/** # 组件 实心下三角指示器(等价原站 dashicons arrow-down,展开不旋转,颜色随文字) */
function CaretDown() {
return (
<svg width="10" height="10" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="shrink-0">
<path d="M4.5 7h11L10 13.5 4.5 7Z" />
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="shrink-0">
<path d="M4.5 7.5h11L10 13.5 4.5 7.5Z" />
</svg>
);
}
+47 -39
View File
@@ -20,7 +20,7 @@
*/
import { useState } from "react";
import { Link } from "@tanstack/react-router";
import { Search, Menu, X, ChevronRight, Mail, User, Star } from "lucide-react";
import { Search, Menu, X, ChevronRight, Mail, MessageCircleMore, Star } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
@@ -118,60 +118,68 @@ export function Header(props: { menu: MenuNode[] }) {
const tops = items.filter((it) => it.title !== "Search");
return (
<header className="sticky top-0 z-[100] border-b border-line bg-white shadow-[0_1px_6px_rgba(16,42,76,0.06)]">
{/* topbar:口号 + 社交图标 + 登录 */}
<div className="bg-footer text-[13px] text-white">
<div className="site-container flex h-10 items-center justify-between">
<a href="/landing-pages/oem-odm/" className="text-white hover:text-[#f5b278]">
<header className="sticky top-0 z-[100] bg-white shadow-[0_1px_6px_rgba(16,42,76,0.06)]">
{/* topbar:口号 + 社交图标(原站实测 2026-11-03:高 36px、容器 1350、文字 16px/16、
图标 36×36 盒 gap 10、顺序 Linkedin/Youtube/Facebook、无登录项;<768 隐藏) */}
<div className="hidden bg-footer text-white min-[768px]:block">
<div className="mx-auto flex h-9 w-full max-w-[1350px] items-center justify-between">
<a href="/landing-pages/oem-odm/" className="text-[16px] leading-4 text-white hover:text-[#f5b278]">
{t("topbar_oem")}
</a>
<div className="flex items-center gap-3.5">
<a href="https://www.youtube.com/@dusuniot" aria-label="YouTube" target="_blank" rel="noreferrer" className="text-white hover:text-[#f5b278]">
<YoutubeGlyph />
</a>
<a href="https://www.facebook.com/dusuniot" aria-label="Facebook" target="_blank" rel="noreferrer" className="text-white hover:text-[#f5b278]">
<FacebookGlyph />
</a>
<a href="https://www.linkedin.com/company/dusun-iot" aria-label="LinkedIn" target="_blank" rel="noreferrer" className="text-white hover:text-[#f5b278]">
<div className="flex items-center gap-[10px]">
<a href="https://www.linkedin.com/company/dusun-iot" aria-label="LinkedIn" target="_blank" rel="noreferrer" className="flex h-9 w-9 items-center justify-center text-white hover:text-[#f5b278]">
<LinkedinGlyph />
</a>
<span className="h-3.5 w-px bg-white/35" aria-hidden />
<a href="/my-account/" className="inline-flex items-center gap-1 text-white hover:text-[#f5b278]">
<User size={15} aria-hidden /> {t("login")}
<a href="https://www.youtube.com/@dusuniot" aria-label="YouTube" target="_blank" rel="noreferrer" className="flex h-9 w-9 items-center justify-center text-white hover:text-[#f5b278]">
<YoutubeGlyph />
</a>
<a href="https://www.facebook.com/dusuniot" aria-label="Facebook" target="_blank" rel="noreferrer" className="flex h-9 w-9 items-center justify-center text-white hover:text-[#f5b278]">
<FacebookGlyph />
</a>
</div>
</div>
</div>
{/* 主导航(原站实测:白底条高 68px,topbar 40px,合计 108px) */}
<div className="site-container flex h-[68px] items-center gap-6">
{/* 主导航(原站实测 2026-11-03:桌面 68px 容器 40/25、logo 后 80px 起菜单、项 pad 8/6 项距 6;
平板 768-959 为汉堡导航(顶栏 36 + 导航 48,logo 缩放,右侧 CONTACT US+邮件);
手机 <768 无顶栏、导航 48、汉堡在 logo 右 96px、右侧橙色气泡+邮件) */}
<div className="flex h-12 items-center px-2.5 pr-[13px] min-[768px]:pr-[68px] min-[960px]:h-[68px] min-[960px]:pl-10 min-[960px]:pr-[25px]">
{/* 直接复用原站 header logo(Elementor site-logo 同一图片,实测渲染 171×42.75px) */}
<Link to="/" className="shrink-0">
<img src="/uploads/2022/06/Dusuniot-logo-R-400-e1669108867399.png" width={300} height={75} alt="Dusun IoT" className="h-auto w-[171px]" />
<img src="/uploads/2022/06/Dusuniot-logo-R-400-e1669108867399.png" width={300} height={75} alt="Dusun IoT" className="block h-auto w-[109px] min-[768px]:w-[103px] min-[880px]:w-[124px] min-[960px]:w-[171px]" />
</Link>
<DesktopNav tops={tops} />
{searchOpen ? (
<form action="/search" method="get" className="flex items-center overflow-hidden rounded-full border border-line bg-soft">
<Input type="search" name="q" placeholder={t("search_placeholder")} aria-label="Search" autoFocus className="w-40 rounded-none border-0 bg-transparent focus-visible:border-0" />
<button type="submit" aria-label="Submit search" className="flex cursor-pointer items-center border-0 bg-transparent px-2.5 py-1.5 text-muted-foreground">
<Search size={16} aria-hidden />
<div className="ml-[80px] hidden min-w-0 min-[960px]:flex min-[960px]:flex-1 min-[960px]:items-center">
<DesktopNav tops={tops} />
{searchOpen ? (
<form action="/search" method="get" className="ml-1.5 flex items-center overflow-hidden rounded-full border border-line bg-soft">
<Input type="search" name="q" placeholder={t("search_placeholder")} aria-label="Search" autoFocus className="w-40 rounded-none border-0 bg-transparent focus-visible:border-0" />
<button type="submit" aria-label="Submit search" className="flex cursor-pointer items-center border-0 bg-transparent px-2.5 py-1.5 text-muted-foreground">
<Search size={16} aria-hidden />
</button>
</form>
) : (
<button aria-label="Open search" onClick={() => setSearchOpen(true)} className="ml-1.5 flex h-[68px] w-10 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-ink">
<Search size={19} aria-hidden />
</button>
</form>
) : (
<button aria-label="Open search" onClick={() => setSearchOpen(true)} className="flex cursor-pointer items-center border-0 bg-transparent p-1.5 text-ink">
<Search size={19} aria-hidden />
)}
</div>
{/* 汉堡:原站为 mega-toggle 块(手机位于 logo 右 96px、平板右聚拢),图标浅灰 */}
<div className="ml-[96px] flex h-12 items-center justify-end min-[768px]:ml-auto min-[960px]:hidden">
<button aria-label="Toggle menu" onClick={() => setOpen(!open)} className="flex h-12 w-10 cursor-pointer items-center justify-center rounded-[4px] border-0 bg-transparent p-0 text-[#c9c9c9]">
{open ? <X size={32} aria-hidden /> : <Menu size={32} aria-hidden />}
</button>
)}
<Button asChild className="hidden whitespace-nowrap px-[18px] py-[9px] min-[960px]:inline-flex">
</div>
<div className="min-w-0 flex-1 min-[960px]:hidden" aria-hidden />
<a href="/contact-us/" aria-label="Contact us" className="flex h-[34px] w-[38px] items-center justify-center text-brand min-[768px]:hidden">
<MessageCircleMore size={28} aria-hidden />
</a>
<Button asChild className="hidden whitespace-nowrap px-[18px] py-[9px] min-[768px]:inline-flex">
<a href="/contact-us/">{t("contact_us")}</a>
</Button>
<a href="/contact-us/" aria-label="Email us" className="hidden items-center p-1 text-brand min-[960px]:flex">
<Mail size={19} aria-hidden />
<a href="/contact-us/" aria-label="Email us" className="flex h-10 w-[52px] items-center justify-center p-0 text-brand">
<Mail size={28} aria-hidden />
</a>
<LangSwitcher />
<button aria-label="Toggle menu" onClick={() => setOpen(!open)} className="ml-auto cursor-pointer rounded-[4px] border-0 bg-transparent p-1.5 text-ink min-[960px]:hidden">
{open ? <X size={24} aria-hidden /> : <Menu size={24} aria-hidden />}
</button>
</div>
{/* 移动端菜单 */}
{open ? (
@@ -201,7 +209,7 @@ export function Header(props: { menu: MenuNode[] }) {
*/
function DesktopNav({ tops }: { tops: MenuNode[] }) {
return (
<div className="hidden min-[960px]:flex min-[960px]:flex-1 min-[960px]:items-center gap-0.5">
<div className="hidden min-[960px]:flex min-[960px]:items-center gap-1.5">
{tops.map((node) => {
const linkHref = TOP_URL_FIX[node.title] ?? normalizeUrl(node.url) ?? undefined;
// 巨幕面板项(全宽视口)
@@ -298,7 +306,7 @@ function TopTrigger({ title, href }: { title: string; href?: string }) {
if (href) {
return (
<NavigationMenuTrigger asChild chevron={false}>
<a href={href}>
<a href={href} className="pl-2 pr-1.5">
{title}
<CaretDown />
</a>
+2 -2
View File
@@ -30,9 +30,9 @@ export function LangSwitcher() {
aria-expanded={open}
type="button"
onClick={() => setOpen(!open)}
className="flex cursor-pointer items-center border-0 bg-transparent p-1 text-brand"
className="flex h-12 w-12 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-brand"
>
<Globe size={19} aria-hidden />
<Globe size={28} aria-hidden />
</button>
{open ? (
<div className="absolute right-0 top-full z-50 max-h-[340px] w-[200px] overflow-auto rounded-md border border-line bg-white py-1 shadow-[0_8px_24px_rgba(16,42,76,0.14)]">
+174
View File
@@ -0,0 +1,174 @@
/**
* # 组件 方案归档页(/solution/ 与 /solution/page/N/ 共用)
* 一比一复刻原站 Elementor archive-posts 归档(widget 2da75291,skin=cards):
* hero 灰底 h1 "Solution" + 卡片网格(3/2/1 列,gap 28)+ 分页。
* 全部几何/配色取自 2026-11-03 playwright 取证(桌面 1440/1280/1920、平板 1024、移动 390):
* - hero:#eee 背景,上下 40px;h1 40px/40px 900 #000 居中;容器 1140+10 内边距
* - 卡片区:白底;桌面左右 192px、平板 100px 25px、移动 0;外层 margin 64px 0(移动 0)
* - 卡片:radius 3、阴影 0 0 10px rgba(0,0,0,.15)(hover 30px)、无边框
* - 缩略图:图高按原比例置于顶部 + padding-bottom 66%(移动 50%)占位,::after 黑 35%→75% 透明渐变
* (base opacity 1,hover 0.5,transition .3s ease-out)
* - 标题 21px/1.2 600,链接 #ED6C00 400;摘录 14px/1.5 #777,底部 33px;文字区左右 30px
* - 分页:居中,页码 margin 0 10px,当前页 #FFA11B,当前页/端点为 span 其余为 a
*/
import { Link } from "@tanstack/react-router";
import { getLayoutData, Layout } from "~/components/website/Layout";
import { listSolutionLandings } from "~/server/data";
import { useT } from "~/i18n";
/** # 类型 方案归档页 props **/
export type SolutionArchiveProps = { page: number };
/** # 函数 加载方案归档数据(layout + 分页列表) **/
export async function loadSolutionArchive(page: number) {
const [layout, list] = await Promise.all([
getLayoutData(),
listSolutionLandings(page, 12).catch(() => ({ items: [], total: 0, page: 1, pageSize: 12 })),
]);
return { layout, list, page };
}
/** # 常量 摘录显示词数上限(原站 archive-posts excerpt_length,取证:12 张卡片全部 ≤24 词、DB 全文更长) **/
const EXCERPT_WORDS = 24;
/** # 函数 按词数截断摘录(WP wp_trim_words 语义:空白分词但 &nbsp; 相连算一个词,无省略号后缀)
* 参数 text DB 摘录全文;limit 词数上限
* 返回值 截断后的显示文本
*/
function trimExcerpt(text: string, limit: number) {
// \s 除 \u00a0 外的分隔符(PHP \s 不含 nbsp,"Smart&nbsp;Building" 是一个词)
const words = text.split(/[^\S\u00a0]+/).filter(Boolean);
return words.length > limit ? words.slice(0, limit).join(" ") : text;
}
/** # 组件 方案归档页(原站 Elementor archive 一比一) */
export function SolutionArchive({ data }: { data: { layout: Awaited<ReturnType<typeof getLayoutData>>; list: Awaited<ReturnType<typeof listSolutionLandings>>; page: number } }) {
const t = useT();
const { list, page } = data;
const totalPages = Math.max(1, Math.ceil(list.total / list.pageSize));
/** 分页链接(原站路径 /solution/ 与 /solution/page/N/) */
const href = (p: number) => (p <= 1 ? "/solution/" : `/solution/page/${p}/`);
return (
<Layout layout={data.layout}>
<style>{SOLUTION_ARCHIVE_CSS}</style>
<div className="ds-solarch">
<div className="elementor-location-archive">
{/* hero:灰底 Solution 大标题 */}
<section className="ds-hero">
<div className="ds-hero-container">
<div className="ds-hero-col">
<div className="ds-widget">
<h1 className="ds-heading">{t("solution_title")}</h1>
</div>
</div>
</div>
</section>
{/* 卡片归档 */}
<section className="ds-posts-section">
<div className="ds-container">
<div className="ds-col">
<div className="ds-widget-wrap">
<div className="ds-widget ds-archive-posts">
<div className="ds-posts">
{list.items.map((post) => (
<article className="ds-post" key={post.id}>
<div className="ds-post-card">
<Link to="/solution/$slug/" params={{ slug: post.slug }} className="ds-thumb-link" tabIndex={-1}>
<div className="ds-thumb">
{post.imageUrl ? (
<img src={post.imageUrl} alt={post.imageAlt || post.title} width={600} height={400} loading="lazy" decoding="async" />
) : null}
</div>
</Link>
<div className="ds-post-text">
<h3 className="ds-post-title">
<Link to="/solution/$slug/" params={{ slug: post.slug }}>{post.title}</Link>
</h3>
{post.excerpt ? (
<div className="ds-post-excerpt">
<p>{trimExcerpt(post.excerpt, EXCERPT_WORDS)}</p>
</div>
) : null}
</div>
</div>
</article>
))}
</div>
{totalPages > 1 ? (
<nav className="ds-pagination" aria-label="Pagination">
{page > 1 ? (
<a className="ds-page-num ds-prev" href={href(page - 1)}>« Previous</a>
) : (
<span className="ds-page-num ds-prev">« Previous</span>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) =>
p === page ? (
<span key={p} aria-current="page" className="ds-page-num ds-current">
<span className="ds-screen-only">Page</span>
{p}
</span>
) : (
<a key={p} className="ds-page-num" href={href(p)}>
<span className="ds-screen-only">Page</span>
{p}
</a>
),
)}
{page < totalPages ? (
<a className="ds-page-num ds-next" href={href(page + 1)}>Next »</a>
) : (
<span className="ds-page-num ds-next">Next »</span>
)}
</nav>
) : null}
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</Layout>
);
}
/** # 常量 方案归档页样式(取证值;断点 1024 平板 / 767 移动) **/
export const SOLUTION_ARCHIVE_CSS = `
.ds-solarch .ds-hero { background: #eee; padding: 40px 0; }
.ds-solarch .ds-hero-container { max-width: 1140px; margin: 0 auto; }
.ds-solarch .ds-hero-col { padding: 0 10px; }
.ds-solarch .ds-heading { margin: 0; font-size: 40px; font-weight: 900; line-height: 40px; color: #000; text-align: center; text-transform: uppercase; }
.ds-solarch .ds-posts-section { background: #fff; margin: 64px 0; padding: 0 192px; }
.ds-solarch .ds-container { display: flex; }
.ds-solarch .ds-col { width: 100%; }
.ds-solarch .ds-widget-wrap { padding: 10px; width: 100%; }
.ds-solarch .ds-posts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; }
.ds-solarch .ds-post { display: flex; }
.ds-solarch .ds-post-card { display: flex; flex-direction: column; flex: 1; background: #fff; border-radius: 3px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.15); transition: background .25s, box-shadow .25s; }
.ds-solarch .ds-post:hover .ds-post-card { box-shadow: 0 0 30px 0 rgba(0,0,0,0.15); }
.ds-solarch .ds-thumb-link { position: relative; display: block; margin: 0 0 20px; color: #ED6C00; }
.ds-solarch .ds-thumb { position: relative; overflow: hidden; padding-bottom: 66%; }
.ds-solarch .ds-thumb img { display: block; width: calc(100% + 1px); height: auto; margin: 0; }
.ds-solarch .ds-thumb-link::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(0deg, rgba(0,0,0,0.35), rgba(0,0,0,0) 75%); opacity: 1; transition: opacity .3s ease-out; pointer-events: none; }
.ds-solarch .ds-post:hover .ds-thumb-link::after { opacity: 0.5; }
.ds-solarch .ds-post-text { margin: 10px 0 0; padding: 0 30px; }
.ds-solarch .ds-post-title { margin: 0 0 12px; font-size: 21px; font-weight: 600; line-height: 1.2; color: #333; }
.ds-solarch .ds-post-title a { color: #ED6C00; font-weight: 400; }
.ds-solarch .ds-post-excerpt { margin: 0 0 33px; }
.ds-solarch .ds-post-excerpt p { margin: 0; font-size: 14px; line-height: 1.5; color: #777; }
.ds-solarch .ds-pagination { margin: 40px 0 0; font-size: 16px; line-height: 24px; text-align: center; }
.ds-solarch .ds-page-num { display: inline; margin: 0 10px; color: #000; text-decoration: none; }
.ds-solarch .ds-page-num.ds-prev { margin: 0 10px 0 0; }
.ds-solarch .ds-page-num.ds-next { margin: 0 0 0 10px; }
.ds-solarch .ds-page-num.ds-current { color: #FFA11B; }
.ds-solarch .ds-screen-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
@media (max-width: 1024px) {
.ds-solarch .ds-posts-section { margin: 64px 0; padding: 100px 25px; }
.ds-solarch .ds-posts { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767px) {
.ds-solarch .ds-posts-section { margin: 0; padding: 0; }
.ds-solarch .ds-posts { grid-template-columns: 1fr; }
.ds-solarch .ds-thumb { padding-bottom: 50%; }
}
`;