2151 字
11 分钟
Fuwari改造计划之赞助页面
成果展示

修改过程
32 collapsed lines
enum I18nKey { home = "home", about = "about", archive = "archive", search = "search",
tags = "tags", categories = "categories", recentPosts = "recentPosts",
comments = "comments",
untitled = "untitled", uncategorized = "uncategorized", noTags = "noTags",
wordCount = "wordCount", wordsCount = "wordsCount", minuteCount = "minuteCount", minutesCount = "minutesCount", postCount = "postCount", postsCount = "postsCount",
themeColor = "themeColor",
lightMode = "lightMode", darkMode = "darkMode", systemMode = "systemMode",
more = "more",
author = "author", publishedAt = "publishedAt", license = "license",
sponsor = "sponsor",}
export default I18nKey;35 collapsed lines
import Key from "../i18nKey";import type { Translation } from "../translation";
export const zh_CN: Translation = { [Key.home]: "主页", [Key.about]: "关于", [Key.archive]: "归档", [Key.search]: "搜索",
[Key.tags]: "标签", [Key.categories]: "分类", [Key.recentPosts]: "最新文章",
[Key.comments]: "评论",
[Key.untitled]: "无标题", [Key.uncategorized]: "未分类", [Key.noTags]: "无标签",
[Key.wordCount]: "字", [Key.wordsCount]: "字", [Key.minuteCount]: "分钟", [Key.minutesCount]: "分钟", [Key.postCount]: "篇文章", [Key.postsCount]: "篇文章",
[Key.themeColor]: "主题色",
[Key.lightMode]: "亮色", [Key.darkMode]: "暗色", [Key.systemMode]: "跟随系统",
[Key.more]: "更多",
[Key.author]: "作者", [Key.publishedAt]: "发布于", [Key.license]: "许可协议",
[Key.sponsor]: "赞助",};---import { getEntry } from "astro:content";import Markdown from "@components/misc/Markdown.astro";import SponsorCard from "@components/SponsorCard.astro";import I18nKey from "../i18n/i18nKey";import { i18n } from "../i18n/translation";import MainGridLayout from "../layouts/MainGridLayout.astro";import SupportAndShare from "@components/misc/SupportAndShare.astro";import MarkdownIt from 'markdown-it';
const sponsorPost = await getEntry("spec", "sponsor");
if (!sponsorPost) { throw new Error("sponsor page content not found");}
const md = new MarkdownIt({ html: true, linkify: true, typographer: true});
interface SponsorBlock { name: string; img: string; icon: string; color: string; description: string; index: number; // 在内容中的位置}
interface ContentSegment { type: 'markdown' | 'sponsors'; content?: string; htmlContent?: string; sponsors?: SponsorBlock[];}
function parseContentWithSponsors(content: string): { segments: ContentSegment[], sponsorBlocks: SponsorBlock[] } { const segments: ContentSegment[] = []; const sponsorBlocks: SponsorBlock[] = []; const regex = /:::sponsor\s*([\s\S]*?):::/g; let lastIndex = 0; let match; let sponsorIndex = 0; let consecutiveSponsors: SponsorBlock[] = [];
const renderMd = (text: string) => { const processed = text.replace(/:::(tip|note|important|caution|warning)\s*([\s\S]*?):::/g, (_, type, inner) => { const renderedInner = md.render(inner.trim()); return `<blockquote class="admonition bdm-${type}"><div class="bdm-title">${type.toUpperCase()}</div>${renderedInner}</blockquote>`; }); return md.render(processed); };
while ((match = regex.exec(content)) !== null) { const markdownBetween = content.substring(lastIndex, match.index).trim();
if (markdownBetween) { if (consecutiveSponsors.length > 0) { segments.push({ type: 'sponsors', sponsors: [...consecutiveSponsors] }); consecutiveSponsors = []; } segments.push({ type: 'markdown', content: markdownBetween, htmlContent: renderMd(markdownBetween) }); }
const blockContent = match[1]; const block: Record<string, string> = {};
blockContent.split('\n').forEach(line => { const trimmed = line.trim(); if (!trimmed) return; const colonIdx = trimmed.indexOf(':'); if (colonIdx === -1) return; const key = trimmed.substring(0, colonIdx).trim(); let value = trimmed.substring(colonIdx + 1).trim(); value = value.replace(/,\s*$/, ''); block[key] = value; });
const sponsor: SponsorBlock = { name: block.name || '', img: block.img || '', icon: block.icon || '', color: block.color || '', description: block.description || '', index: sponsorIndex++ }; sponsorBlocks.push(sponsor); consecutiveSponsors.push(sponsor);
lastIndex = match.index + match[0].length; }
if (consecutiveSponsors.length > 0) { segments.push({ type: 'sponsors', sponsors: [...consecutiveSponsors] }); }
if (lastIndex < content.length) { const markdownContent = content.substring(lastIndex).trim(); if (markdownContent) { segments.push({ type: 'markdown', content: markdownContent, htmlContent: renderMd(markdownContent) }); } }
return { segments, sponsorBlocks };}
const { segments, sponsorBlocks } = parseContentWithSponsors(sponsorPost.body);---<MainGridLayout title={i18n(I18nKey.sponsor)} description={i18n(I18nKey.sponsor)}> <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 "> <!-- Header section with Flutter-like M3 style --> <div class="flex flex-col mb-8 mt-4"> <h1 class="text-3xl font-bold text-neutral-900 dark:text-neutral-100 mb-2">{i18n(I18nKey.sponsor)}</h1> </div>
<!-- 渲染分段内容:Markdown 和 Sponsor Cards 交替出现 --> {segments.map((segment, idx) => { if (segment.type === 'markdown' && segment.htmlContent) { return ( <Markdown class="mt-2 text-75 leading-relaxed max-w-2xl mx-auto"> <div set:html={segment.htmlContent} /> </Markdown> ); } else if (segment.type === 'sponsors' && segment.sponsors) { const sponsors = segment.sponsors; const isSingleCard = sponsors.length === 1;
return isSingleCard ? ( <div class="flex justify-center mt-12 mb-12 px-2 sm:px-10"> <SponsorCard name={sponsors[0].name} img={sponsors[0].img} icon={sponsors[0].icon} color={sponsors[0].color} description={sponsors[0].description} layout="horizontal" isSingleCard={true} /> </div> ) : ( <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-12 mb-12 px-2 sm:px-10 max-w-4xl mx-auto"> {sponsors.map((sponsor) => ( <SponsorCard name={sponsor.name} img={sponsor.img} icon={sponsor.icon} color={sponsor.color} description={sponsor.description} layout="vertical" /> ))} </div> ); } })} </div> </div></MainGridLayout>---import { Icon } from "astro-icon/components";
interface Props { name: string; img: string; icon: string; color: string; description: string; layout?: "vertical" | "horizontal"; isSingleCard?: boolean;}
const { name, img, icon, color, description, layout = "vertical", isSingleCard = false } = Astro.props;
const imagePath = img.startsWith("./") ? `/src/content/spec/${img.slice(2)}` : img;---<div class="block md:hidden"> <div class="group relative flex flex-col items-center p-8 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800"> <div class="absolute top-6 left-8 opacity-20 group-hover:opacity-100 transition-opacity duration-300"> <Icon name={icon} class="text-3xl" style={`color: ${color}`} /> </div> <div class="relative w-48 h-48 mb-6 p-3 bg-white rounded-[var(--radius-large)]"> <img src={imagePath} alt={name} class="w-full h-full object-cover rounded-[var(--radius-large)]" /> <!-- Decorative corners --> <div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div> <div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div> </div> <h3 class="text-xl font-bold text-90 mb-1">{name}</h3> <p class="text-sm text-50 font-normal">{description}</p> </div></div>
<div class="hidden md:block"> {layout === "vertical" ? ( <div class="group relative flex flex-col items-center p-8 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800"> <div class="absolute top-6 left-8 opacity-20 group-hover:opacity-100 transition-opacity duration-300"> <Icon name={icon} class="text-3xl" style={`color: ${color}`} /> </div> <div class="relative w-48 h-48 mb-6 p-3 bg-white rounded-[var(--radius-large)]"> <img src={imagePath} alt={name} class="w-full h-full object-cover rounded-[var(--radius-large)]" /> <!-- Decorative corners --> <div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div> <div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div> </div> <h3 class="text-xl font-bold text-90 mb-1">{name}</h3> <p class="text-sm text-50 font-normal">{description}</p> </div> ) : ( <div class="group relative flex flex-row items-center p-10 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800 gap-10 max-w-3xl"> <div class="relative w-64 h-64 p-4 bg-white rounded-[var(--radius-large)] flex-shrink-0"> {!isSingleCard && ( <div class="absolute -top-6 -left-6 opacity-20 group-hover:opacity-100 transition-opacity duration-300 z-10"> <Icon name={icon} class="text-4xl" style={`color: ${color}`} /> </div> )} <img src={imagePath} alt={name} class="w-full h-full object-cover rounded-[calc(var(--radius-large)-0.25rem)]" /> <!-- Decorative corners --> <div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div> <div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div> </div> <div class="flex flex-col justify-center items-start flex-1"> <h3 class="text-2xl font-bold text-90 mb-3">{name}</h3> <p class="text-base text-50 font-normal leading-relaxed mb-6">{description}</p> </div> {isSingleCard && ( <div class="absolute bottom-6 right-6 opacity-20 group-+hover:opacity-100 transition-opacity duration-300 z-10"> <Icon name={icon} class="text-8xl" style={`color: +${color}`} /> </div> )} </div> )}</div>42 collapsed lines
import type { ExpressiveCodeConfig, LicenseConfig, NavBarConfig, ProfileConfig, SiteConfig,} from "./types/config";import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = { title: "樱花庄驿", subtitle: "SakuraVillage Post", lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc. themeColor: { hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345 fixed: false, // Hide the theme color picker for visitors }, banner: { enable: true, src: "content/assets/images/banner.jpg", // Relative to the /src directory. Relative to the /public directory if it starts with '/' position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default credit: { enable: false, // Display the credit text of the banner image text: "", // Credit text to be displayed url: "", // (Optional) URL link to the original artwork or artist's page }, }, toc: { enable: true, // Display the table of contents on the right side of the post depth: 2, // Maximum heading depth to show in the table, from 1 to 3 }, favicon: [ // Leave this array empty to use the default favicon // { // src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory // theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode // sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes // } ],};
export const navBarConfig: NavBarConfig = { links: [ LinkPreset.Home, LinkPreset.Archive, LinkPreset.About, LinkPreset.Sponsor, { name: "GitHub", url: "https://github.com/SakuraVillager/", // Internal links should not include the base path, as it is automatically added external: true, // Show an external link icon and will open in a new tab }, ],33 collapsed lines
};
export const profileConfig: ProfileConfig = { avatar: "/src/content/assets/images/avatar.gif", // Relative to the /src directory. Relative to the /public directory if it starts with '/' name: "SakuraVillager", bio: "一个普通的樱花庄的居民。", links: [ { name: "BiliBili", icon: "fa6-brands:bilibili", // Visit https://icones.js.org/ for icon codes // You will need to install the corresponding icon set if it's not already included // `pnpm add @iconify-json/<icon-set-name>` url: "https://space.bilibili.com/646905644", }, { name: "GitHub", icon: "fa6-brands:github", url: "https://github.com/SakuraVillager/", }, ],};
export const licenseConfig: LicenseConfig = { enable: true, name: "CC BY-NC-SA 4.0", url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",};
export const expressiveCodeConfig: ExpressiveCodeConfig = { // Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file. // Please select a dark theme, as this blog theme currently only supports dark background color theme: "github-dark",};14 collapsed lines
import I18nKey from "@i18n/i18nKey";import { i18n } from "@i18n/translation";import { LinkPreset, type NavBarLink } from "@/types/config";
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = { [LinkPreset.Home]: { name: i18n(I18nKey.home), url: "/", }, [LinkPreset.About]: { name: i18n(I18nKey.about), url: "/about/", }, [LinkPreset.Archive]: { name: i18n(I18nKey.archive), url: "/archive/", }, [LinkPreset.Sponsor]: { name: i18n(I18nKey.sponsor), url: "/sponsor/", },};47 collapsed lines
import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants";
export type SiteConfig = { title: string; subtitle: string;
lang: | "en" | "zh_CN" | "zh_TW" | "ja" | "ko" | "es" | "th" | "vi" | "tr" | "id";
themeColor: { hue: number; fixed: boolean; }; banner: { enable: boolean; src: string; position?: "top" | "center" | "bottom"; credit: { enable: boolean; text: string; url?: string; }; }; toc: { enable: boolean; depth: 1 | 2 | 3; };
favicon: Favicon[];};
export type Favicon = { src: string; theme?: "light" | "dark"; sizes?: string;};
export enum LinkPreset { Home = 0, Archive = 1, About = 3, Sponsor = 4,}
export type NavBarLink = { name: string;50 collapsed lines
url: string; external?: boolean;};
export type NavBarConfig = { links: (NavBarLink | LinkPreset)[];};
export type ProfileConfig = { avatar?: string; name: string; bio?: string; links: { name: string; url: string; icon: string; }[];};
export type LicenseConfig = { enable: boolean; name: string; url: string;};
export type LIGHT_DARK_MODE = | typeof LIGHT_MODE | typeof DARK_MODE | typeof AUTO_MODE;
export type BlogPostData = { body: string; title: string; published: Date; description: string; tags: string[]; draft?: boolean; image?: string; category?: string; series?: string; pinned?: boolean; prevTitle?: string; prevSlug?: string; nextTitle?: string; nextSlug?: string;};
export type ExpressiveCodeConfig = { theme: string;};使用示例
在 spec 文件夹下新建一个文件叫做 sponsor.md,随后新建一个文件夹叫做 assets,在 assets 下新建一个文件夹叫做 sponsor,随后把支付宝、微信的二维码保存在这个文件夹下。
可以在markdown文件中这样写:
# 赞助
通过支付宝或者微信给博主打赏吧!
:::sponsorname: 支付宝,img: ./assets/sponsor/你的支付码名字.jpg,icon: fa6-brands:alipay,color: #1677FF,description: 请作者喝一杯咖啡吧,:::
:::sponsorname: 微信支付,img: ./assets/sponsor/你的支付码名字.png,icon: fa6-brands:weixin,color: #07C160,description: 感谢您对我的支持,:::
# 赞助列表
其他内容支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
正在绘制分享海报...