75 Commits

Author SHA1 Message Date
ed5a976735 Merge pull request 'dev' (#13) from dev into main
Reviewed-on: #13
2025-11-12 00:56:16 +08:00
jimleerx
f4e7112d4c 修复断言错误 2025-11-12 00:55:22 +08:00
jimleerx
bc4155ce82 fix unuse import 2025-11-12 00:41:11 +08:00
jimleerx
f1bda537b4 隐藏光标 2025-11-12 00:39:45 +08:00
jimleerx
1b2f79f65c 增加光晕特效 2025-11-12 00:33:12 +08:00
jimleerx
62383aaf9e 给格子加上光晕特效 2025-11-12 00:18:29 +08:00
jimleerx
dcc5654603 修复改为hexagon实现后遮挡背景图的问题 2025-11-12 00:17:50 +08:00
jimleerx
cf36b71990 调整快捷方式gird的实现为代码实现hexagon,而不是图片和边距 2025-11-12 00:12:49 +08:00
jimleerx
9cda45f112 patch shikijs冲突 2025-11-12 00:12:49 +08:00
ef7ef44033 修复某些地方点击会显示光标 2025-11-11 02:00:06 +08:00
3ca63f62fb 调整sm窗口快捷方式偏移量 2025-11-11 01:38:23 +08:00
4c255a2672 调整移动端主页格子大小 2025-11-11 01:03:43 +08:00
bc940a7a6e Merge pull request '修改readme说明中的首页图片' (#12) from dev into main
Reviewed-on: #12
2025-11-10 23:07:11 +08:00
0f4eaf8523 修改readme说明中的首页图片 2025-11-10 23:06:42 +08:00
6755f95e5e Merge pull request '优化整体布局,切换主题为拟物化样式' (#11) from dev into main
Reviewed-on: #11
2025-11-10 23:01:52 +08:00
b29f5c7c1e 修复代码中的错误 2025-11-10 22:54:58 +08:00
20b01c6f51 调整首页blog标题行高,因为字符g显示不全
修复原有圆角元素替换为直角
2025-11-10 22:36:24 +08:00
57e1724c34 调整整体菜单样式 2025-11-10 22:18:01 +08:00
c720e605bf 修改为妙妙屋主题 2025-11-10 21:31:16 +08:00
6575bf0c62 更新footer图标 2025-11-10 16:34:18 +08:00
639e3b54db 添加project菜单和妙妙屋项目 2025-11-10 16:07:12 +08:00
7be284fbd5 buildnginx.mdx update 2025-11-10 10:23:33 +08:00
c74c99e776 Merge branch 'dev' of code.2ha.me:dev.2ha.me/dev.2ha.me into dev 2025-10-16 15:54:05 +08:00
8026dd0321 更新开发时间 2025-10-16 15:53:48 +08:00
351c398553 Merge pull request 'dev: 合并近期新增博客与资源调整' (#10) from dev into main
Reviewed-on: #10
2025-10-09 13:47:22 +08:00
35c1ebe9f1 Merge branch 'main' into dev 2025-10-09 13:46:59 +08:00
b9255a78ae 替换探针为网站统计 2025-10-09 13:44:40 +08:00
de76217b85 增加fail2ban封禁bot文章 2025-10-07 01:14:01 +08:00
cca8f502f7 merge-latest-dev 2025-10-06 04:41:23 +08:00
44e437175e merge-latest-dev 2025-10-06 04:41:16 +08:00
bea9917330 add react-compiler-runtime 2025-10-06 04:39:05 +08:00
90b53fc225 Merge pull request '更新blog' (#9) from dev-20251006 into main
Reviewed-on: #9
2025-10-06 03:48:11 +08:00
4c91a029a3 更新blog 2025-10-06 03:47:50 +08:00
23c445ceca Merge pull request '更新blog' (#8) from dev-20251006 into main
Reviewed-on: #8
2025-10-06 03:46:31 +08:00
5281507386 更新blog 2025-10-06 03:46:13 +08:00
bb8b999644 Merge pull request '更新blog' (#7) from dev-20251006 into main
Reviewed-on: #7
2025-10-06 03:43:24 +08:00
5eb497db3f 更新blog 2025-10-06 03:42:57 +08:00
7376c3ee0b Merge pull request '更新blog' (#5) from dev-20251006 into main
Reviewed-on: #5
2025-10-06 03:35:35 +08:00
daecb6c9d7 更新blog 2025-10-06 03:29:44 +08:00
cf443d730c 修改maven仓库图标 2025-05-29 11:04:55 +08:00
9cbc974cda 修改媒体订阅工具link配置 2025-05-29 10:59:49 +08:00
d240cfe5a3 修改maven仓库图标 2025-05-29 10:57:34 +08:00
1eda3310ea 修改api接口地址为api.2ha.me 2025-05-16 15:21:41 +08:00
c3d250c9b1 修改文章astro代码块增强踩坑首图 2025-05-15 23:05:39 +08:00
2a422883c4 修复导入错误 2025-05-15 13:41:10 +08:00
10d5dfa639 移除冗余引用katex 2025-05-15 11:54:34 +08:00
450a7a0eb2 移除部分动画背景的展示 2025-05-15 11:25:00 +08:00
1969c79fac 替换loading.mp4为gif 2025-05-14 18:27:02 +08:00
7cf8a4fa55 修改首页video样式 2025-05-14 16:42:33 +08:00
253d2774a5 修复博客头图线条错误 2025-05-14 13:15:03 +08:00
a5e04fc004 替换png资源为webp 2025-05-14 13:13:19 +08:00
479bf095cf 优化markdown文档中代码块展示 2025-05-14 11:25:45 +08:00
80037a96c1 nginx编译文档新增httpv3module 2025-05-14 11:02:39 +08:00
0342feb3bd 添加常用代码块 2025-05-13 17:24:33 +08:00
c055535e80 增加github代理快捷方式 2025-05-13 15:54:50 +08:00
6156b512e9 Merge pull request 'dev' (#4) from dev into main
Reviewed-on: #4
build 1.1.2 version
2025-05-13 00:22:22 +08:00
9e5d98e2e2 🆗 增加shikijs踩坑记录blog
🆗 调整markdown代码块copy success的图标颜色
🆗 去除首页blog图片的左边距
2025-05-13 00:06:48 +08:00
101db5aa83 增加copy按钮添加时改的shikijs包代码说明 2025-05-12 18:57:01 +08:00
9ef5139d93 代码块添加复制按钮 2025-05-12 18:51:21 +08:00
f999f0912e 添加网易云音乐vip歌曲的自建backup服务地址 2025-05-12 16:08:05 +08:00
75cd6ce315 Merge branch 'dev' of code.2ha.me:dev.2ha.me/dev.2ha.me into dev 2025-05-09 15:53:04 +08:00
fe3978ddbf 添加缺少的gif资源 2025-05-09 15:53:01 +08:00
f76e5719a2 Merge pull request 'main' (#3) from main into dev
Reviewed-on: #3
2025-05-06 15:21:46 +08:00
79db601591 fix linux build error 2025-05-06 10:52:10 +08:00
3b12d07b51 fix loading display 2025-04-29 23:40:44 +08:00
6e13092027 修复loadingbug 2025-04-29 23:39:52 +08:00
ee31225f15 删除冗余代码 2025-04-29 22:58:49 +08:00
cbe2957f80 优化首页动画背景加载loading显示 2025-04-29 22:34:41 +08:00
c3e50ccff2 调整首页视频背景与loading的交互方式 2025-04-29 18:30:05 +08:00
e0a0576ee1 调整动画背景为播放完成后切换,而不是循环播放 2025-04-27 23:49:06 +08:00
b60cdafc0a add 莉可莉丝 bg 2025-04-27 18:35:02 +08:00
fcf44c1d15 修改主页背景图为mp4格式 2025-04-27 15:46:01 +08:00
246eb86d8a 增加猫娘随机Bg 2025-04-26 00:09:46 +08:00
db2e8936f3 修改about页图片展示比例 2025-04-25 19:12:22 +08:00
5e9389f5de 更新about小猫女仆说明 2025-04-25 17:33:56 +08:00
82 changed files with 5669 additions and 681 deletions

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ pnpm-debug.log*
teaser.pptx
~$teaser.pptx
.claude

View File

@@ -18,6 +18,7 @@ import remarkToc from 'remark-toc'
import sectionize from '@hbsnow/rehype-sectionize'
import { transformerNotationSkip } from './src/lib/transformerNotationSkip'
import { transformerDiffHighlight } from './src/lib/transformerDiffHighlight'
import { transformerCopyButton } from './src/lib/transformerCopyButton'
import icon from 'astro-icon'
@@ -64,6 +65,11 @@ export default defineConfig({
transformerRenderWhitespace(),
transformerNotationSkip(),
transformerDiffHighlight(),
transformerCopyButton({
duration: 1000,
successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
})
],
},
],

BIN
dist.zip

Binary file not shown.

2639
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,8 @@
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}"
"prettier": "prettier --write ./src/**/*.{astro,ts,tsx,css}",
"postinstall": "patch-package"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
@@ -42,6 +43,7 @@
"lucide-react": "^0.439.0",
"react": "^18.3.1",
"react-activity-calendar": "^2.7.5",
"react-compiler-runtime": "^19.1.0-rc.3",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-use-lanyard": "^0.3.2",
@@ -59,6 +61,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"patch-package": "^8.0.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-astro-organize-imports": "^0.4.11",
@@ -86,5 +89,8 @@
}
}
]
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
}
}

View File

@@ -0,0 +1,14 @@
diff --git a/node_modules/@shikijs/transformers/dist/index.mjs b/node_modules/@shikijs/transformers/dist/index.mjs
index db4db63..c7f3ea2 100644
--- a/node_modules/@shikijs/transformers/dist/index.mjs
+++ b/node_modules/@shikijs/transformers/dist/index.mjs
@@ -410,6 +410,9 @@ function transformerRenderWhitespace(options = {}) {
return token;
if (position === "trailing" && index !== last)
return token;
+ if (token.children.length === 0) {
+ return token;
+ }
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

View File

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

BIN
public/static/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

1
public/static/mmw.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

View File

@@ -37,7 +37,7 @@ const socialLinks: SocialLink[] = [
---
<div
class="overflow-hidden rounded-xl border p-4 transition-colors duration-300 ease-in-out has-[a:hover]:bg-secondary/50"
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div class="flex flex-wrap gap-4">
<Link

192
src/components/Badge.astro Normal file
View File

@@ -0,0 +1,192 @@
---
import { Badge as BadgeComponent } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
interface Props {
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
className?: string
children?: any
text?: string
showIcon?: boolean
}
const {
variant = 'secondary',
className = '',
children,
text,
showIcon = true,
} = Astro.props
const categoryMappings = [
{
keywords: ['crypto'],
style: {
color:
'bg-yellow-50 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-200',
icon: 'lucide:key',
},
},
{
keywords: ['web'],
style: {
color: 'bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-200',
icon: 'lucide:globe',
},
},
{
keywords: ['reverse', 'rev'],
style: {
color:
'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rotate-ccw',
},
},
{
keywords: ['pwn', 'binary exploitation'],
style: {
color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:zap',
},
},
{
keywords: ['misc'],
style: {
color:
'bg-stone-50 text-stone-700 dark:bg-stone-950/30 dark:text-stone-200',
icon: 'lucide:puzzle',
},
},
{
keywords: ['forensic'],
style: {
color:
'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:search',
},
},
{
keywords: ['osint'],
style: {
color:
'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:eye',
},
},
{
keywords: ['blockchain'],
style: {
color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:link',
},
},
{
keywords: ['ppc', 'programming'],
style: {
color:
'bg-indigo-50 text-indigo-700 dark:bg-indigo-950/30 dark:text-indigo-200',
icon: 'lucide:code',
},
},
{
keywords: ['commercial'],
style: {
color: 'text-foreground bg-foreground/10',
icon: 'lucide:building-2',
},
},
{
keywords: ['personal'],
style: {
// color: 'bg-sky-50 text-sky-700 dark:bg-sky-950/30 dark:text-sky-200',
icon: 'lucide:user',
},
},
{
keywords: ['open-source'],
style: {
// color: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-200',
icon: 'lucide:git-branch',
},
},
{
keywords: ['freelance'],
style: {
// color: 'bg-teal-50 text-teal-700 dark:bg-teal-950/30 dark:text-teal-200',
icon: 'lucide:briefcase',
},
},
{
keywords: ['team'],
style: {
// color: 'bg-violet-50 text-violet-700 dark:bg-violet-950/30 dark:text-violet-200',
icon: 'lucide:users',
},
},
{
keywords: ['contract'],
style: {
// color: 'bg-rose-50 text-rose-700 dark:bg-rose-950/30 dark:text-rose-200',
icon: 'lucide:file-text',
},
},
{
keywords: ['astro'],
style: {
// color: 'bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-200',
icon: 'lucide:rocket',
},
},
{
keywords: ['shopify'],
style: {
// color: 'bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-200',
icon: 'lucide:shopping-bag',
},
},
{
keywords: ['html'],
style: {
// color: 'bg-red-50 text-red-700 dark:bg-red-950/30 dark:text-red-200',
icon: 'lucide:code-2',
},
},
{
keywords: ['figma'],
style: {
// color: 'bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-200',
icon: 'lucide:palette',
},
},
]
const getCategoryStyle = (content: string) => {
const lowerContent = content.toLowerCase()
const match = categoryMappings.find((category) =>
category.keywords.some((keyword) => lowerContent.includes(keyword)),
)
return match?.style || null
}
const content = text || (typeof children === 'string' ? children : '')
const categoryStyle = getCategoryStyle(content)
---
<BadgeComponent
variant={categoryStyle ? 'secondary' : variant}
className={cn(categoryStyle?.color, className)}
client:load
>
{
showIcon && (
<Icon
name={categoryStyle ? categoryStyle.icon : 'lucide:tag'}
class="size-3"
/>
)
}
<slot>{text}</slot>
</BadgeComponent>

View File

@@ -24,13 +24,14 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === entry.data.title)
const totalBody = [entry.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const readTime = readingTime(totalBody)
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
const authors = await parseAuthors(entry.data.authors ?? [])
---
<div
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
class="not-prose border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<Link
href={`/${entry.collection}/${entry.id}`}

View File

@@ -0,0 +1,111 @@
---
import { DEV_LINKS } from '@/consts'
import { Icon } from 'astro-icon/components'
import Link from './Link.astro'
// interface ShortCutProps {
// href: string | undefined
// title: string
// ariaLabel: string
// icon: string
// }
// const SHORT_CUTS: ShortCutProps[] = [
// {
// href: getDevLinkHref('Gitea'),
// title: "代码仓库",
// ariaLabel: "代码仓库",
// icon: "mdi:git"
// },
// {
// href: getDevLinkHref('Nexus'),
// title: "Maven仓库",
// ariaLabel: "Maven仓库",
// icon: "mdi:chart-doughnut-variant"
// },
// {
// href: getDevLinkHref('Bytebase'),
// title: "数据库管理",
// ariaLabel: "数据库管理",
// icon: "mdi:database-cog"
// },
// {
// href: getDevLinkHref('Zfile'),
// title: "网盘",
// ariaLabel: "网盘",
// icon: "mdi:cloud-arrow-up"
// },
// {
// href: getDevLinkHref('FileServer'),
// title: "文件服务器",
// ariaLabel: "文件服务器",
// icon: "mdi:file-arrow-up-down-outline"
// },
// {
// href: getDevLinkHref('immich'),
// title: "相册",
// ariaLabel: "相册",
// icon: "mdi:camera"
// },
// {
// href: getDevLinkHref('Emby'),
// title: "Emby",
// ariaLabel: "Emby",
// icon: "mdi:emby"
// },
// {
// href: getDevLinkHref('Emby'),
// title: "Emby",
// ariaLabel: "Emby",
// icon: "mdi:emby"
// }
// ]
const SHORTS_CUTS_CLASS_NAMES: string[] = [
'z-10 max-w max-h mt-[2.6em] inline-block sm:mt-[2.35em]',
'z-10 max-w max-h ml-[0.1em] mt-[-4.7em] inline-block sm:ml-[0em] sm:mt-[-3.4em]',
'z-10 max-w max-h ml-[-0.2em] mt-[3.0em] inline-block sm:ml-[0em] sm:mt-[2.5em]',
'z-10 max-w max-h mt-[-5.2em] inline-block sm:mt-[-3em]',
'z-10 max-w max-h mt-[0.1em] inline-block sm:mt-[0.6em]',
'z-10 max-w max-h ml-[0.15em] mt-[-7.5em] inline-block sm:ml-[0em] sm:mt-[-4.8em]',
'z-10 max-w max-h ml-[-0.15em] mt-[0.2em] inline-block sm:mt-[0.9em] sm:ml-[0em]',
'z-10 max-w max-h mt-[-7.5em] inline-block sm:mt-[-4.8em]',
'z-10 max-w max-h mt-[1.88em] inline-block sm:mt-[1em]',
'z-10 max-w max-h ml-[0.15em] mt-[-5.8em] inline-block sm:ml-[0em] sm:mt-[-4em]'
]
---
{
DEV_LINKS.map((item, index) => (
<div
class={SHORTS_CUTS_CLASS_NAMES[index]}
>
<Link
href={item.href} || '#'}
title={item.title}
aria-label={item.label}
target="_blank"
>
<div
class="bottom-0 right-0 w-fit items-end rounded-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_30%,hsl(var(--border)))] bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:scale-110 hover:border-[color-mix(in_srgb,hsl(var(--primary))_50%,hsl(var(--border)))]"
>
{item.icon.startsWith('mdi:') ? (
<Icon
style="color: rgb(233, 211, 182);"
name={item.icon}
class="z-[1] size-1/2 size-8 text-primary sm:size-8"
aria-hidden="true"
/>
) : (
<img
src={item.icon}
alt={item.title}
class="z-[1] size-1/2 size-8 sm:size-8"
aria-hidden="true"
/>
)}
</div>
</Link>
</div>
))
}

View File

@@ -0,0 +1,406 @@
---
import { DEV_LINKS } from '@/consts'
import { Icon } from 'astro-icon/components'
// ------------- Hex math helpers (ported from react-hexgrid) -------------
type Point = { x: number; y: number }
type HexCoord = { q: number; r: number; s: number }
type Orientation = {
f0: number
f1: number
f2: number
f3: number
b0: number
b1: number
b2: number
b3: number
startAngle: number
}
type LayoutDimension = {
size: Point
spacing: number
origin: Point
orientation: Orientation
}
const SQRT3 = Math.sqrt(3)
const ORIENTATION_FLAT: Orientation = {
f0: 3 / 2,
f1: 0,
f2: SQRT3 / 2,
f3: SQRT3,
b0: 2 / 3,
b1: 0,
b2: -1 / 3,
b3: SQRT3 / 3,
startAngle: 0,
}
const BASE_HEX_SIZE = 104
const HEX_SIZE = 168
const BORDER_WIDTH = 24
const layout: LayoutDimension = {
size: { x: HEX_SIZE, y: HEX_SIZE },
spacing: 1,
origin: { x: 0, y: 0 },
orientation: ORIENTATION_FLAT,
}
const GRID_WIDTH = 5
const GRID_HEIGHT = 5
function calculatePolygonPoints(size: number, flat = false) {
const angleOffset = flat ? 0 : Math.PI / 6
const corners: Point[] = []
for (let i = 0; i < 6; i++) {
const angle = (2 * Math.PI * i) / 6 + angleOffset
corners.push({
x: size * Math.cos(angle),
y: size * Math.sin(angle),
})
}
return corners.map((corner) => `${corner.x.toFixed(2)},${corner.y.toFixed(2)}`).join(' ')
}
function coordKey(hex: HexCoord) {
return `${hex.q},${hex.r},${hex.s}`
}
function hexToPixel(hex: HexCoord, layout: LayoutDimension): Point {
const { orientation: M, size, spacing, origin } = layout
let x = (M.f0 * hex.q + M.f1 * hex.r) * size.x
let y = (M.f2 * hex.q + M.f3 * hex.r) * size.y
x *= spacing
y *= spacing
return { x: x + origin.x, y: y + origin.y }
}
function rectangle(mapWidth: number, mapHeight: number): HexCoord[] {
const cells: HexCoord[] = []
for (let r = 0; r < mapHeight; r++) {
const offset = Math.floor(r / 2)
for (let q = -offset; q < mapWidth - offset; q++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
function hexagon(mapRadius: number): HexCoord[] {
const cells: HexCoord[] = []
for (let q = -mapRadius; q <= mapRadius; q++) {
const r1 = Math.max(-mapRadius, -q - mapRadius)
const r2 = Math.min(mapRadius, -q + mapRadius)
for (let r = r1; r <= r2; r++) {
cells.push({ q, r, s: -q - r })
}
}
return cells
}
// 根据cells的下标与对应值控制显示在哪些格子里
const ACTIVE_COORDS: HexCoord[] = [
// { q: -1, r: 0, s: 1 },
// { q: 0, r: 0, s: 0 },
// { q: 1, r: 0, s: -1 },
{ q: 2, r: 0, s: -2 },
{ q: 3, r: 0, s: -3 },
{ q: 0, r: 1, s: -1 },
{ q: 1, r: 1, s: -2 },
{ q: 2, r: 1, s: -3 },
{ q: 0, r: 2, s: -2 },
{ q: 1, r: 2, s: -3 },
{ q: 2, r: 2, s: -4 },
{ q: 0, r: 3, s: -3 },
{ q: 1, r: 3, s: -4 },
{ q: 2, r: 3, s: -5 },
]
const gridCoords = rectangle(GRID_WIDTH, GRID_HEIGHT)
const gridKeyMap = new Map(gridCoords.map((hex) => [coordKey(hex), hex]))
const preferredCoords = ACTIVE_COORDS.map((hex) => gridKeyMap.get(coordKey(hex))).filter(
Boolean,
) as HexCoord[]
const preferredKeySet = new Set(preferredCoords.map((hex) => coordKey(hex)))
const remainingGridCoords = gridCoords.filter((hex) => !preferredKeySet.has(coordKey(hex)))
let orderedActiveSlots = [...preferredCoords, ...remainingGridCoords]
if (DEV_LINKS.length > orderedActiveSlots.length) {
const usedKeys = new Set(orderedActiveSlots.map((hex) => coordKey(hex)))
let radius = Math.max(GRID_WIDTH, GRID_HEIGHT)
while (orderedActiveSlots.length < DEV_LINKS.length) {
radius += 1
for (const hex of hexagon(radius)) {
const key = coordKey(hex)
if (usedKeys.has(key)) continue
orderedActiveSlots.push(hex)
usedKeys.add(key)
if (orderedActiveSlots.length === DEV_LINKS.length) break
}
}
}
const assignedCoords = orderedActiveSlots.slice(0, DEV_LINKS.length)
const coordToLink = new Map<string, (typeof DEV_LINKS)[number]>()
assignedCoords.forEach((hex, index) => {
const link = DEV_LINKS[index]
if (!hex || !link) return
coordToLink.set(coordKey(hex), link)
})
const renderCoordMap = new Map<string, HexCoord>()
gridCoords.forEach((hex) => renderCoordMap.set(coordKey(hex), hex))
assignedCoords.forEach((hex) => {
if (!hex) return
renderCoordMap.set(coordKey(hex), hex)
})
const renderCoords = Array.from(renderCoordMap.values())
const hexagonPoints = calculatePolygonPoints(layout.size.x, true)
// const circleRadius = layout.size.x * 0.84
const circleRadius = 80
const baseCells = renderCoords.map((hex) => ({
hex,
link: coordToLink.get(coordKey(hex)) ?? null,
center: hexToPixel(hex, layout),
}))
const xs = baseCells.map((cell) => cell.center.x)
const ys = baseCells.map((cell) => cell.center.y)
const padding = layout.size.x * 0.45
const minX = Math.min(...xs) - padding
const maxX = Math.max(...xs) + padding
const minY = Math.min(...ys) - padding
const maxY = Math.max(...ys) + padding
const sizeScale = HEX_SIZE / BASE_HEX_SIZE
const rawWidth = maxX - minX
const rawHeight = maxY - minY
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const viewWidth = rawWidth / sizeScale
const viewHeight = rawHeight / sizeScale
const viewMinX = centerX - viewWidth / 2
const viewMinY = centerY - viewHeight / 2
const viewBox = `${viewMinX} ${viewMinY} ${viewWidth} ${viewHeight}`
const iconClipId = 'hexIconClip'
const cells = baseCells.map((cell) => ({
...cell,
showContent: Boolean(cell.link),
}))
---
<svg
class="hex-grid hex-grid--polygons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<g class="hex-grid__group">
{cells.map(({ link, center }, index) => {
const isGhost = !link
return (
<polygon
class={`hexagon-bg ${isGhost ? 'hexagon-bg--ghost' : ''}`}
points={hexagonPoints}
fill="transparent"
stroke="#252525"
stroke-width={BORDER_WIDTH}
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
/>
)
})}
</g>
</svg>
<svg
class="hex-grid hex-grid--icons"
width="100%"
height="100%"
viewBox={viewBox}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid slice"
>
<defs>
<clipPath id={iconClipId} clipPathUnits="userSpaceOnUse">
<circle cx="0" cy="0" r={circleRadius} />
</clipPath>
</defs>
<g class="hex-grid__group">
{cells.map(({ link, center, showContent }, index) => {
if (!link || !showContent) return null
return (
<g
class="hexagon-wrapper"
transform={`translate(${center.x}, ${center.y})`}
data-index={index}
>
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
>
<title>{link.title}</title>
<g class="icon-group">
<circle
r={circleRadius}
fill="hsl(var(--secondary))"
fill-opacity="0.45"
stroke="hsl(var(--border))"
stroke-width={Math.max(4, BORDER_WIDTH * 0.32)}
class="icon-circle z-10"
/>
{link.icon.startsWith('mdi:') ? (
<foreignObject
x={-(layout.size.x * 0.7)}
y={-(layout.size.y * 0.7)}
width={layout.size.x * 1.4}
height={layout.size.y * 1.4}
clip-path={`url(#${iconClipId})`}
class="icon-foreign"
>
<div class="icon-container">
<Icon
name={link.icon}
class="hexagon-icon"
style="color: rgb(233, 211, 182);"
aria-hidden="true"
/>
</div>
</foreignObject>
) : (
<image
href={link.icon}
x={-layout.size.x * 0.3}
y={-layout.size.y * 0.3}
width={layout.size.x * 0.6}
height={layout.size.y * 0.6}
clip-path={`url(#${iconClipId})`}
class="hexagon-image"
/>
)}
</g>
</a>
</g>
)
})}
</g>
</svg>
<style>
.hex-grid {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.hex-grid--polygons {
z-index: 1;
}
.hex-grid--icons {
z-index: 20;
}
.hex-grid__group {
transform-origin: center;
transform: translateX(-114px) translateY(60px) scale(1.08);
}
.hexagon-wrapper {
cursor: pointer;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.hexagon-wrapper a {
outline: none;
-webkit-tap-highlight-color: transparent;
pointer-events: auto;
cursor: pointer;
}
.hexagon-wrapper a title {
pointer-events: none;
}
.hexagon-bg,
.icon-circle,
.icon-group {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.icon-circle {
stroke-width: 4px;
transform-origin: center;
transform-box: fill-box;
}
.icon-group {
transform-origin: center;
transform-box: fill-box;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.45));
}
.hexagon-wrapper:hover .icon-circle {
fill-opacity: 0.85;
stroke: hsl(var(--primary));
stroke-width: 4px;
transform: scale(1.08);
}
.hexagon-wrapper:hover .icon-group {
transform: rotate(12deg);
transform-origin: center;
}
.hexagon-bg--ghost {
stroke-dasharray: none;
}
.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 9999px;
}
.hexagon-icon {
width: 100% !important;
height: 100% !important;
max-width: 88px;
max-height: 88px;
}
.hexagon-image {
object-fit: contain;
}
.icon-foreign {
overflow: visible;
}
:global(.hexagon-icon) {
color: rgb(233, 211, 182) !important;
}
</style>

View File

@@ -1,6 +1,6 @@
---
import '../styles/global.css'
import '../styles/katex.css'
// import '../styles/katex.css'
import { SITE } from '@/consts'
import { ClientRouter } from 'astro:transitions'

View File

@@ -4,7 +4,9 @@ import Link from '@/components/Link.astro'
import MobileMenu from '@/components/ui/mobile-menu'
import { NAV_LINKS } from '@/consts'
import { Image } from 'astro:assets'
import { Icon } from 'astro-icon/components'
import logo from '../../public/static/logo.webp'
---
<header
@@ -13,18 +15,19 @@ import logo from '../../public/static/logo.webp'
>
<Container>
<div class="flex flex-wrap items-center justify-between gap-4 py-4 ">
<Link href="/">
<Image src={logo} alt="Logo" class="size-8 -scale-x-100" />
<Link href="/" class="logo-link">
<Image src={logo} alt="Logo" class="size-8 -scale-x-100 border-2 border-[color:rgba(241,140,110,0.4)] shadow-[4px_4px_0_rgba(0,0,0,0.2)] transition-all duration-300" />
</Link>
<div class="flex items-center gap-2 sm:gap-4">
<nav class="hidden items-center gap-4 text-base sm:flex sm:gap-6">
<nav class="hidden items-center gap-2 text-base sm:flex sm:gap-3">
{
NAV_LINKS.map((item) => (
<Link
href={item.href}
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
class="pixel-button inline-flex items-center justify-center gap-2 px-4 py-2 h-9 text-sm font-semibold uppercase tracking-wider bg-background/75 text-foreground border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)] transition-all"
>
{item.label}
<Icon name={item.icon} class="size-[18px] shrink-0" />
<span>{item.label}</span>
</Link>
))
}
@@ -35,3 +38,31 @@ import logo from '../../public/static/logo.webp'
</div>
</Container>
</header>
<style>
.logo-link:hover img {
border-color: hsl(var(--primary));
box-shadow:
0 0 12px rgba(241, 140, 110, 0.6),
0 0 24px rgba(241, 140, 110, 0.4),
0 0 36px rgba(241, 140, 110, 0.2),
4px 4px 0 rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.2) !important;
}
.dark nav :global(.pixel-button:hover) {
box-shadow:
0 0 12px rgba(241, 140, 110, 0.5),
0 0 24px rgba(241, 140, 110, 0.3),
0 0 36px rgba(241, 140, 110, 0.15),
4px 4px 0 rgba(0, 0, 0, 0.4) !important;
}
</style>

View File

@@ -0,0 +1,36 @@
---
import { SITE } from '@/consts'
interface Props {
title?: string
description?: string
noindex?: boolean
}
const {
title = SITE.TITLE,
description = SITE.DESCRIPTION,
noindex = false,
} = Astro.props
const image = new URL('/static/twitter-card.png', Astro.site)
---
<title>{`${title} | ${SITE.TITLE}`}</title>
<meta name="description" content={description} />
<link rel="canonical" href={SITE.SITEURL} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<meta property="og:title" content={`${title} | ${SITE.TITLE}`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:alt" content={title} />
<meta property="og:type" content="website" />
<meta property="og:locale" content={SITE.locale} />
<meta property="og:site_name" content={SITE.TITLE} />
<meta property="og:url" content={Astro.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:card" content="summary_large_image" />

View File

@@ -1,10 +1,13 @@
---
import Badge from '@/components/Badge.astro'
import Link from '@/components/Link.astro'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { extractDomain, formatMonthYear } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets'
import type { CollectionEntry } from 'astro:content'
type Props = {
interface Props {
project: CollectionEntry<'projects'>
}
@@ -12,30 +15,63 @@ const { project } = Astro.props
---
<div
class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50"
class="border-2 border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] bg-secondary/25 p-4 [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] transition-all duration-200 hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<Link href={project.data.link} class="block">
<Link
href={project.data.link}
class="flex flex-col gap-4 sm:flex-row"
target="_blank"
rel="noopener noreferrer"
>
{
project.data.image && (
<div class="max-w-[225px] sm:flex-shrink-0">
<Image
src={project.data.image}
alt={project.data.name}
width={400}
height={200}
class="w-full object-cover"
width={1200}
height={630}
class="object-cover"
/>
<div class="p-4">
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
<p class="mb-4 text-sm text-muted-foreground">
</div>
)
}
<div class="grow">
<h3 class="mb-1 text-lg font-semibold">
{project.data.name}
</h3>
<p class="text-muted-foreground mb-2 text-sm">
{project.data.description}
</p>
<div class="flex flex-wrap gap-2">
{
project.data.tags.map((tag) => (
<Badge variant="secondary" showHash={false}>
{tag}
</Badge>
))
}
project.data.startDate && (
<div class="text-muted-foreground/70 mb-2 flex flex-wrap items-center gap-x-2 text-xs">
<span class="flex items-center gap-x-1.5">
<Icon name="lucide:calendar" class="size-3" />
<span>
{formatMonthYear(project.data.startDate)}
{project.data.endDate
? ` → ${formatMonthYear(project.data.endDate)}`
: ' → Present'}
</span>
</span>
<Separator orientation="vertical" className="h-4!" />
<span class="flex items-center gap-x-1">
<Icon name="lucide:external-link" class="size-3" />
<span>{extractDomain(project.data.link)}</span>
</span>
</div>
)
}
{
project.data.tags && (
<div class="flex flex-wrap gap-2">
{project.data.tags.map((tag: string) => (
<Badge text={tag} />
))}
</div>
)
}
</div>
</Link>
</div>

View File

@@ -22,6 +22,7 @@ const iconMap = {
Gitea: 'mdi:git',
Maven: 'mavenrepo',
DevIntro: 'lucide:info',
HubProxy: 'lucide:rocket',
}
const getSocialLink = ({ href, label }: SocialLink) => ({

View File

@@ -3,7 +3,7 @@ import AvatarComponent from '@/components/ui/avatar'
const AuthorPresence = () => {
return (
<div className="relative overflow-hidden sm:aspect-square">
<div className="relative overflow-hidden sm:aspect-square select-none" style={{ cursor: 'default' }}>
<div className="grid size-full grid-rows-4">
<div className="bg-secondary/50"></div>
<div className="row-span-3 flex flex-col gap-3 p-3">
@@ -26,22 +26,22 @@ const AuthorPresence = () => {
/>
</div>
</div>
<div className="flex flex-col gap-y-1 rounded-xl bg-secondary/50 p-3">
<span className="text-base leading-none">jimlee</span>
<span className="text-xs leading-none text-muted-foreground">
<div className="flex flex-col gap-y-1 rounded-xl bg-secondary/50 p-3 select-none">
<span className="text-base leading-none select-none cursor-default">jimlee</span>
<span className="text-xs leading-none text-muted-foreground select-none cursor-default">
li@2ha.me
</span>
</div>
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2">
<div className="flex size-full flex-col items-center justify-center gap-1">
<div className="flex grow rounded-xl bg-secondary/50 px-3 py-2 select-none">
<div className="flex size-full flex-col items-center justify-center gap-1 select-none">
<img
src="/static/images/lieflat.svg"
alt="No Status Image"
width={64}
height={64}
className="h-full rounded-lg"
className="h-full rounded-lg select-none"
/>
<div className="text-[10px] text-muted-foreground">
<div className="text-[10px] text-muted-foreground select-none cursor-default">
</div>
</div>

View File

@@ -55,12 +55,12 @@ const WakatimeGraph = ({ }: Props) => {
useEffect(() => {
setLanguages([
{ name: 'java', hours: 1009, fill: 'hsl(var(--chart-1))' },
{ name: 'kotlin', hours: 346, fill: 'hsl(var(--chart-2))' },
{ name: 'javascript', hours: 311, fill: 'hsl(var(--chart-3))' },
{ name: 'typescript', hours: 287, fill: 'hsl(var(--chart-4))' },
{ name: 'python', hours: 120, fill: 'hsl(var(--chart-5))' },
{ name: 'react', hours: 85, fill: 'hsl(var(--chart-6))' },
{ name: 'go', hours: 9, fill: 'hsl(var(--chart-7))' },
{ name: 'javascript', hours: 476, fill: 'hsl(var(--chart-2))' },
{ name: 'kotlin', hours: 405, fill: 'hsl(var(--chart-3))' },
{ name: 'typescript', hours: 401, fill: 'hsl(var(--chart-4))' },
{ name: 'react', hours: 257, fill: 'hsl(var(--chart-5))' },
{ name: 'go', hours: 125, fill: 'hsl(var(--chart-6))' },
{ name: 'python', hours: 120, fill: 'hsl(var(--chart-7))' },
])
setIsLoading(false)
}, [])

View File

@@ -11,7 +11,7 @@ import Calendar, {
async function fetchCalendarData(): Promise<ApiResponse> {
const response = await fetch(
`https://dev.2ha.me/api/Calendar`,
`https://api.2ha.me/api/Calendar`,
)
const data: ApiResponse | ApiErrorResponse = await response.json()
if (!response.ok) {

View File

@@ -10,10 +10,11 @@ interface Track {
image: { '#text': string }[] //
url: string
'@attr'?: { nowplaying: string },
outerurl: string
outerurl: string,
backupurl: string
}
const SpotifyPresence = () => {
const Music163Player = () => {
const [displayData, setDisplayData] = useState<Track | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isPlaying, setIsPlaying] = useState(false);
@@ -34,7 +35,7 @@ const SpotifyPresence = () => {
};
useEffect(() => {
fetch('https://dev.2ha.me/api/v1/play/record?uid=91859315&type=1')
fetch('https://api.2ha.me/api/v1/play/record?uid=91859315&type=1')
.then((response) => response.json())
.then((data) => {
let lastweekFirstSong = data.weekData[0].song
@@ -47,9 +48,10 @@ const SpotifyPresence = () => {
album: {
'#text': lastweekFirstSong.al.name
},
image: [{'#text': lastweekFirstSong.al.picUrl}],
image: [{'#text': lastweekFirstSong.al.picUrl.replace('http:', 'https:')}],
url: 'https://music.163.com/song?id=' + lastweekFirstSong.id,
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3"
outerurl: "https://music.163.com/song/media/outer/url?id=" + lastweekFirstSong.id + ".mp3",
backupurl: "https://f.2ha.me/music/" + lastweekFirstSong.id + ".mp3"
}
setDisplayData(track)
setIsLoading(false)
@@ -60,6 +62,13 @@ const SpotifyPresence = () => {
})
}, [])
// Set audio volume to 50%
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = 0.5
}
}, [displayData])
if (isLoading) {
return (
<div className="relative flex h-full w-full flex-col justify-between rounded-3xl p-6">
@@ -82,7 +91,7 @@ const SpotifyPresence = () => {
if (!displayData) return <p>Something absolutely horrible has gone wrong</p>
const { name: song, artist, album, image, url, outerurl } = displayData
const { name: song, artist, album, image, url, outerurl, backupurl } = displayData
return (
<>
@@ -100,7 +109,7 @@ const SpotifyPresence = () => {
alt="Album art"
width={128}
height={128}
className="mb-2 w-[55%] rounded-xl border border-border"
className="mb-2 w-[55%] border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))]"
/>
</a>
<div className="flex min-w-0 flex-1 flex-col justify-end overflow-hidden">
@@ -139,7 +148,10 @@ const SpotifyPresence = () => {
<span className="w-[85%] truncate text-xs text-muted-foreground">
<span className="font-semibold text-secondary-foreground">
<div>
<audio ref={audioRef} src={outerurl} />
<audio ref={audioRef}>
<source src={outerurl} type="audio/mp3" />
<source src={backupurl} type="audio/mp3" />
</audio>
</div>
</span>
</span>
@@ -160,4 +172,4 @@ const SpotifyPresence = () => {
)
}
export default SpotifyPresence
export default Music163Player

View File

@@ -18,7 +18,7 @@ const SpotifyPresence = () => {
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetch('https://dev.2ha.me/api/v1/play/record?uid=91859315&type=1')
fetch('https://api.2ha.me/api/v1/play/record?uid=91859315&type=1')
.then((response) => response.json())
.then((data) => {
let lastweekFirstSong = data.weekData[0].song

View File

@@ -0,0 +1,71 @@
import { useEffect, useRef, useState } from "react";
export const videoBackgrounds: string[] = [
'225.mp4',
'830.mp4',
// 'guduyaogun.mp4',
'guduyaogun1.mp4',
'guduyaogun2.mp4',
'lige.mp4',
'maoliang.mp4',
// 'miku.mp4',
'miku2.mp4',
// 'sanlian.mp4',
'lycoris2.mp4',
]
const RandomAnimeBackground = () => {
const [index, setIndex] = useState<number>(0)
const [isLoading, setIsLoading] = useState(true)
const videoRef = useRef<HTMLVideoElement | null>(null);
const [bindEvent, setBindEvent] = useState<boolean>(true);
const handleVideoEnded = () => {
setIsLoading(true)
setIndex(getRandomIndex())
if (bindEvent && videoRef.current) {
videoRef.current.addEventListener('canplay', handleCanPlayThrough);
setBindEvent(false)
}
};
const handleCanPlayThrough = () => {
setTimeout(() => {
setIsLoading(false)
}, 200);
}
const getRandomIndex = () => {
return Math.floor(Math.random() * videoBackgrounds.length);
};
useEffect(() => {
setIndex(getRandomIndex())
// setTimeout(() => {
setIsLoading(false)
// }, 100);
}, [])
// if (isLoading) {
// return (
// <video className="no-repeat relative w-full justify-center rounded-[1.4em] object-cover"
// src='/static/anime-bg/loading.mp4'
// autoPlay muted loop>
// Your browser does not support the video tag.
// </video>
// );
// }
return (
<video ref={videoRef} width="100" height="100" className="no-repeat relative w-full justify-center object-cover"
src={'/static/anime-bg/' + videoBackgrounds[index]}
style={{ display: isLoading ? 'none' : 'block' }}
onEnded={handleVideoEnded}
autoPlay muted>
Your browser does not support the video tag.
</video>
)
}
export default RandomAnimeBackground

View File

@@ -24,7 +24,7 @@ export default function GradientText({
// console.log(children)
return (
<div
className={`relative mx-auto ml-2 md:max-h-[1em] flex max-w-fit cursor-pointer flex-row items-center justify-center overflow-hidden rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
className={`relative mx-auto ml-2 flex max-w-fit cursor-pointer flex-row items-center justify-center rounded-[1.25rem] font-medium backdrop-blur transition-shadow duration-500 ${className}`}
>
{showBorder && (
<div

View File

@@ -280,7 +280,7 @@ const LetterGlitch = ({
}, [glitchSpeed, smooth])
return (
<div className="relative h-full w-full overflow-hidden rounded-3xl bg-black">
<div className="relative h-full w-full overflow-hidden bg-black">
<canvas ref={canvasRef} className="block h-full w-full" />
{outerVignette && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-full bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]"></div>

View File

@@ -5,25 +5,25 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-2',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
'bg-primary text-primary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] active:[box-shadow:1px_1px_0_rgba(0,0,0,0.16)] active:translate-y-0 dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)] dark:active:[box-shadow:1px_1px_0_rgba(0,0,0,0.35)]',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
'bg-destructive text-destructive-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
'border-input bg-background [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-accent hover:text-accent-foreground hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)] dark:hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.55)]',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
'bg-secondary text-secondary-foreground [box-shadow:3px_3px_0_rgba(0,0,0,0.18)] hover:bg-secondary/80 hover:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] hover:-translate-y-[1px] dark:[box-shadow:3px_3px_0_rgba(0,0,0,0.45)]',
ghost: 'hover:bg-accent hover:text-accent-foreground border-transparent',
link: 'text-primary underline-offset-4 hover:underline border-transparent',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
icon: 'h-9 w-9',
},
},

View File

@@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className,
)}
@@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
@@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'z-50 min-w-[8rem] overflow-hidden border-2 border-[color:rgba(241,140,110,0.22)] bg-popover p-1 text-popover-foreground [box-shadow:4px_4px_0_rgba(0,0,0,0.22)] dark:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)]',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
@@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className,
)}
@@ -101,7 +101,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
@@ -125,7 +125,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}

View File

@@ -7,7 +7,20 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { NAV_LINKS } from '@/consts'
import { Menu } from 'lucide-react'
import { Menu, Home, FileText, FolderGit2, BadgeInfo } from 'lucide-react'
const getIconComponent = (iconName?: string) => {
if (!iconName) return Home
const iconMap: Record<string, any> = {
'lucide:home': Home,
'lucide:file-text': FileText,
'lucide:folder-git-2': FolderGit2,
'lucide:badge-info': BadgeInfo,
}
return iconMap[iconName] || Home
}
const MobileMenu = () => {
const [isOpen, setIsOpen] = useState(false)
@@ -33,25 +46,29 @@ const MobileMenu = () => {
<Button
variant="outline"
size="icon"
className="sm:hidden"
className="sm:hidden pixel-button h-9 w-9 bg-background/75 border-[color:rgba(137,110,96,0.45)] hover:bg-primary/20 hover:text-primary hover:border-[color:rgba(217,119,87,0.65)] dark:bg-input/30 dark:border-[color:rgba(255,255,255,0.18)] dark:hover:bg-primary/25 dark:hover:text-primary dark:hover:border-[color:rgba(241,140,110,0.75)]"
title="Menu"
>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-background">
{NAV_LINKS.map((item) => (
<DropdownMenuItem key={item.href} asChild>
<DropdownMenuContent align="end" className="bg-background min-w-[140px] w-auto p-0">
{NAV_LINKS.map((item) => {
const Icon = getIconComponent(item.icon)
return (
<DropdownMenuItem key={item.href} asChild className="p-0">
<a
href={item.href}
className="w-full text-lg font-medium capitalize"
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 text-base font-semibold uppercase tracking-wider cursor-pointer hover:bg-primary/20 hover:text-primary focus:bg-primary/20 focus:text-primary dark:hover:bg-primary/25 dark:hover:text-primary border-b border-[color:rgba(241,140,110,0.15)] last:border-b-0 transition-colors"
onClick={() => setIsOpen(false)}
>
{item.label}
<Icon className="size-[18px] shrink-0" />
<span>{item.label}</span>
</a>
</DropdownMenuItem>
))}
)
})}
</DropdownMenuContent>
</DropdownMenu>
)

View File

@@ -5,11 +5,13 @@ export type Site = {
NUM_POSTS_ON_HOMEPAGE: number
POSTS_PER_PAGE: number
SITEURL: string
locale: string
}
export type Link = {
href: string
label: string
icon?: string
}
export type DevLink = {
@@ -26,19 +28,20 @@ export const SITE: Site = {
NUM_POSTS_ON_HOMEPAGE: 2,
POSTS_PER_PAGE: 4,
SITEURL: 'https://dev.2ha.me',
locale: 'zh-CN',
}
export const NAV_LINKS: Link[] = [
{ href: '/', label: '主页' },
{ href: '/blog', label: '博客' },
{ href: '/tags', label: '标签' },
// { href: '/authors', label: '作者' },
{ href: '/authors', label: '关于' },
{ href: '/', label: '主页', icon: 'lucide:home' },
{ href: '/blog', label: '博客', icon: 'lucide:file-text' },
// { href: '/tags', label: '标签' },
{ href: '/projects', label: '项目', icon: 'lucide:folder-git-2'},
{ href: '/authors', label: '关于', icon: 'lucide:badge-info' },
]
export const SOCIAL_LINKS: Link[] = [
{ href: 'https://github.com/jimleerx', label: 'GitHub' },
{ href: 'https://maven.2ha.me', label: 'Maven' },
{ href: 'https://1ms.cc', label: 'HubProxy' },
{ href: 'https://code.2ha.me', label: 'Gitea' },
{ href: 'li@2ha.me', label: 'Email' },
]
@@ -51,10 +54,10 @@ export const DEV_LINKS: DevLink[] = [
icon: 'mdi:git',
},
{
href: 'https://maven.2ha.me',
href: 'https://img.2ha.me',
label: 'Nexus',
title: 'Maven仓库',
icon: 'mdi:chart-doughnut-variant',
title: '图床',
icon: 'mdi:image-multiple',
},
{
href: 'https://dms.2ha.me',
@@ -66,13 +69,13 @@ export const DEV_LINKS: DevLink[] = [
href: 'https://p.2ha.me',
label: 'Zfile',
title: '网盘',
icon: 'mdi:cloud-arrow-up',
icon: 'mdi:harddisk',
},
{
href: 'https://photo.2ha.me',
label: 'immich',
title: '相册',
icon: 'mdi:camera',
href: 'https://tz.2ha.me',
label: 'VPS Monitor',
title: '探针',
icon: 'mdi:chart-areaspline',
},
{
href: 'https://f.2ha.me',
@@ -80,17 +83,36 @@ export const DEV_LINKS: DevLink[] = [
title: '文件服务器',
icon: 'mdi:file-arrow-up-down-outline',
},
{ href: 'https://v.2ha.me', label: 'Emby', title: 'Emby', icon: 'mdi:emby' },
{
href: 'https://plex.2ha.me',
label: 'Plex',
title: 'Plex',
icon: 'mdi:plex',
href: 'https://status.2ha.me',
label: 'Domain Status',
title: '站点检测',
icon: 'mdi:cloud-check'
},
{
href: 'https://mr.2ha.me',
label: 'MovieRobot',
title: '媒体订阅工具',
icon: 'mdi:fruit-cherries',
href: 'https://in.2ha.me',
label: '2ha.me statistics',
title: '统计',
icon: 'mdi:sun-azimuth',
},
{
href: 'https://mmwdemo.2ha.me/docs',
label: '妙妙屋',
title: '个人Clash订阅管理工具',
icon: '/static/mmw.svg',
// icon: 'mdi:cat',
},
{
href: 'https://1ms.cc',
label: 'hubproxy',
title: 'GitHub&DockerHub代理',
icon: 'mdi:rocket-launch-outline',
},
]
export const getDevLinkHref = (label: string): string => {
return DEV_LINKS.find((link) => link.label === label)?.href || '#'
}
export const getDevLink = (label: string): DevLink | undefined => {
return DEV_LINKS.find((link) => link.label === label)
}

View File

@@ -11,6 +11,7 @@ const blog = defineCollection({
image: image().optional(),
tags: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().optional(),
hidden: z.boolean().optional(),
parentTitle: z.string().optional(),
@@ -66,6 +67,9 @@ const projects = defineCollection({
tags: z.array(z.string()),
image: image(),
link: z.string().url(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
order: z.number().optional(),
}),
})

View File

@@ -10,31 +10,39 @@ authors: ['jimlee']
## 在debian上安装nginx
debian默认软件库的nginx没有fancy-index模块, fancy-index是一个html文件服务器, 下面就开始手动给nginx添加fancy-index模块。
## 1. 安装编译环境
```shellscript
包含pcre(正则库), zlib(压缩库), ssl(https)
```shellscript title="shell"
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev
# 包含pcre(正则库), zlib(压缩库), ssl(https)
```
## 2. 下载并解压nginx源码
官网查看最新版本(当前20250111为1.26.3)
官网查看最新版本(当前20251111为1.29.3)
https://nginx.org/en/download.html
```shellscript
wget https://nginx.org/download/nginx-1.26.3.tar.gz
tar -xf nginx-1.26.3.tar.gz
```shellscript title="shell"
wget https://nginx.org/download/nginx-1.29.3.tar.gz
tar -xf nginx-1.29.3.tar.gz
```
## 3. 下载并解压nginx-fancyindex模块
```shellscript
cd nginx-1.26.3
```shellscript title="shell"
cd nginx-1.29.3
wget https://github.com/aperezdc/ngx-fancyindex/releases/download/v0.5.2/ngx-fancyindex-0.5.2.tar.xz
tar -xf ngx-fancyindex-0.5.2.tar.xz
```
## 4. 配置需要编译的模块
```shellscript
# NGINX_ROOT_PATH = nginx的安装目录, 需要替换成你想要安装的目录
创建目录
```shellscript title="shell"
mkdir -p /var/cache/nginx
mkdir -p /var/log/nginx
```
NGINX_ROOT_PATH = nginx的安装目录, 需要替换成你想要安装的目录
```
export NGINX_ROOT_PATH=/usr/local/nginx
```
```shellscript
./configure --add-module=./ngx-fancyindex-0.5.2 \
--prefix=${NGINX_ROOT_PATH} \
--user=jimlee \
--group=jimlee \
--user=$(whoami) \
--group=$(id -Gn) \
--sbin-path=${NGINX_ROOT_PATH}/sbin/nginx \
--conf-path=${NGINX_ROOT_PATH}/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
@@ -68,15 +76,20 @@ mkdir -p /var/cache/nginx
--with-stream \
--with-stream_realip_module \
--with-stream_ssl_module \
--with-stream_ssl_preread_module
--with-stream_ssl_preread_module \
--with-http_v3_module
```
## 5. 编译
```shellscript
```shellscript title="shell"
sudo make && sudo make install
```
## 6. 创建systemctl服务
vim编辑service文件
```systemd
vim /usr/lib/systemd/system/nginx.service
```
输入如下内容
```systemd
[Unit]
Description=nginx
After=network.target
@@ -92,11 +105,11 @@ PrivateTmp=true
WantedBy=multi-user.target
```
## 7. 通过systemctl管理nginx
```shellscript
```shellscript title="shell"
systemctl start nginx
systemctl status nginx
```
## 8. 修改setcap让普通用户可以使用1024以下端口
```shellscript
```shellscript title="shell"
setcap cap_net_bind_service=+eip ${NGINX_ROOT_PATH}/sbin/nginx
```

View File

@@ -2,20 +2,39 @@
title: '常用代码块合集'
description: '常用的kotlin, python, shell, regex等代码块合集'
date: 2025-04-10
tags: ['kotlin', 'python', 'regex', 'linux', 'shell', 'javascript']
tags: ['kotlin', 'python', 'regex', 'linux', 'shell', 'javascript', 'nginx']
image: 'assets/commoncodebanner.webp'
authors: ['jimlee']
---
## kotlin
### 添加全局logger
```kotlin
```kotlin title="kotlin"
val <T : Any> T.LOGGER: Logger
get() = LoggerFactory.getLogger(this::class.java)
```
## windows
### 上传公钥到服务器
```cmd title="cmd"
type %USERPROFILE%\.ssh\id_rsa.pub | ssh user@host "cat >> ~/.ssh/authorized_keys"
```
## linux
### 打印文件树
```shellscript
```shellscript title="shell"
find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
```
### linux lo 网卡缺少ip
```shellscript
ip addr add 127.0.0.1/8 dev lo
ip addr add ::1/128 dev lo
```
## nginx
### nginx反代后跳转变成原始ip+端口号
加上如下配置
```shellscript
server_name_in_redirect off;
port_in_redirect off;
```

View File

@@ -17,7 +17,7 @@ import FileTree from '@/components/starlight/FileTree.astro'
| 2 | Windows10/11 | [dnSpy](https://github.com/dnSpy/dnSpy/releases) |
## 1. SSH登录DSM, 切换到root用户
```shellscript
```shellscript title="shell"
ssh {user}@{dsm_ip} // 输入密码
sudo -i // 输入密码
```
@@ -44,18 +44,18 @@ sudo -i // 输入密码
![emby_versions](assets/emby_versions.png)
找到与DSM对应的系统版本与系统架构, 我的系统是DSM7.2 x86_64
复制下载链接, 使用wget下载到NAS临时目录/tmp/emby
```shellscript
```shellscript title="shell"
mkdir -p /tmp/emby
cd /tmp/emby
wget --no-check-certificate https://github.com/MediaBrowser/Emby.Releases/releases/download/4.8.11.0/emby-server-synology72_4.8.11.0_x86_64.spk
```
## 3. 解包emby套件
```shellscript
```shellscript title="shell"
mkdir output
/usr/syno/sbin/synoarchive -xf 'emby-server-synology72_4.8.11.0_x86_64.spk' -C output
```
查看文件树
```shellscript
```shellscript title="shell"
find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
```
现在文件结构应该是这样
@@ -81,7 +81,7 @@ find . -maxdepth 2 | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
</FileTree>
## 4. 解压缩package.tgz
```shellscript
```shellscript title="shell"
mkdir pkg
tar -xf output/package.tgz -C pkg
```
@@ -227,19 +227,19 @@ rm /var/packages/EmbyServer/target/system/Emby.Server.Implementations.dll \
- embypremiere.js
</FileTree>
在dsm终端执行
```shellscript
```shellscript title="shell"
rm pkg/system/Emby.Server.Implementations.dll pkg/system/Emby.Web.dll pkg/system/MediaBrowser.Model.dll pkg/system/dashboard-ui/embypremiere/embypremiere.js
```
### 7.2 替换原文件
先把修改好的文件上传回DSM, 再使用mv或cp命令移动到pkg/system下
### 7.3 打包package.tgz
```shellscript
```shellscript title="shell"
cd pkg
tar -zcvf package.tgz *
```
### 7.4 打包套件
```shellscript
```shellscript title="shell"
cd ..
rm -f output/package.tgz
mv pkg/package.tgz output/
@@ -250,13 +250,13 @@ mv emby.tar emby-server-synology72_4.8.11.0_x86_64_**unlock**.spk
## 8. 自动破解脚本
### 8.1 ssh登录群晖, 切换到root用户
```shellscript
```shellscript title="shell"
curl -LOk https://crackemby.2ha.me/sh && chmod +x emby.sh && bash emby.sh
```
## 9. EmbyLisenceServer
### 9.1 使用Nginx模拟
```nginx
```nginx title="nginx.conf"
server {
listen 80;
server_name {你的域名};

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@@ -0,0 +1,404 @@
---
title: 'Astro集成Shikijs 和RehypePrettyCode踩坑'
description: '1. shikijs重复依赖打包失败; 2. 使用Rehype Pretty Code/Copy Button 给markdown代码块添加复制按钮时发生错误Cannot read properties of undefined (reading "type")'
date: 2025-05-12
tags: ['typescript', 'astro', 'shiki']
image: 'assets/shikicodecopybutton.png'
authors: ['jimlee']
---
import FileTree from '@/components/starlight/FileTree.astro'
# Shikijs重复依赖导致代码报错打包失败
## 执行 npm run build 后发生以下错误
```log title="npm run build log"
src/lib/transformerNotationSkip.ts - error ts(2322): Type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/transformers/node_modules/@shikijs/types/dist/index").ShikiTransformer' is not assignable to type 'import("/var/jenkins_home/workspace/dev.2ha.me/node_modules/@shikijs/types/dist/index").ShikiTransformer'.
Types of property 'preprocess' are incompatible.
return createCommentNotationTransformer(
~~~~~~
Result (78 files):
- 1 error
- 0 warnings
- 0 hints
```
## 检查src/lib/transformerNotationSkip.ts代码
这里引用的ShikiTransformer是/node_modules/@shikijs/types 中的 ShikiTransformer, 与 node_modules/@shikijs/transformers/node_modules/@shikijs/types 中的 ShikiTransformer 代码相同,但是引用不同
```typescript title="TransformerNotationSkipOptions.ts"
import { type ShikiTransformer } from '@shikijs/types';
import { createCommentNotationTransformer } from '@shikijs/transformers'
export interface TransformerNotationSkipOptions {
/**
* Class for skipped lines
*/
classActiveSkip?: string
/**
* Class added to the root element when the code has skipped lines
*/
classActivePre?: string
}
export function transformerNotationSkip(
options: TransformerNotationSkipOptions = {},
): ShikiTransformer {
const { classActiveSkip = 'skip', classActivePre = undefined } = options
return createCommentNotationTransformer(
'skip-lines',
// comment-start | marker | range | comment-end
/^\s*(?:\/\/|\/\*|<!--|#)\s+\[!code skip:(\d+):(\d+)\]\s*(?:\*\/|-->)?/,
function ([_, start, end], _line) {
_line.children = [{ type: 'text', value: `${start}-${end}` }]
_line.properties = { style: `counter-set:line ${end}` }
if (classActiveSkip) this.addClassToHast(_line, classActiveSkip)
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
return false
},
undefined, // remove empty lines
)
}
```
## 检查安装好依赖后的@shikijs目录
<FileTree>
- node_modules
- @shikijs
- engine-javascript
- dist
- README.md
- package.json
- LICENSE
- types
- dist
- README.md
- package.json
- LICENSE
- langs
- dist
- README.md
- package.json
- LICENSE
- engine-oniguruma
- dist
- README.md
- package.json
- LICENSE
- transformers
- dist
- README.md
- package.json
- **node_modules** // 重复依赖最外层 node_modules 下的 @shikijs shiki
- **shiki** // 与 node_modules/shiki 重复
- dist
- README.md
- package.json
- LICENSE
- oniguruma-to-es
- dist
- README.md
- package.json
- types
- LICENSE
- **@shikijs** // 与 node_modules/@shikijs 重复
- engine-javascript
- types
- engine-oniguruma
- vscode-textmate
- core
- LICENSE
- themes
- dist
- README.md
- package.json
- LICENSE
- vscode-textmate
- dist
- README.md
- package.json
- LICENSE.md
- core
- dist
- README.md
- package.json
- LICENSE
</FileTree>
## 删除node_modules中的重复依赖
```shellscript title="shell"
rm -rf node_modules/@shikijs/transformers/node_modules/
```
# 给Astro博客Markdown代码块添加复制按钮
通过查找[Rehype Pretty Code文档](https://rehype-pretty.pages.dev/)找到 [Rehype Pretty Code/Copy Button](https://rehype-pretty.pages.dev/plugins/copy-button/)这个实验性功能
## 1. 安装依赖
```shellscript title="shell"
npm install @rehype-pretty/transformers
```
## 2. 添加transformerCopyButton.ts
这个ts文件是基于node_modules\@rehype-pretty\transformers\dist\copy-button.js修改而来
```typescript title="transformerCopyButton.ts"
import type { ShikiTransformer } from "shiki";
import { h } from "hastscript";
export interface CopyButtonOptions {
duration?: number;
copyIcon?: string;
successIcon?: string
}
export const transformerCopyButton = (
options: CopyButtonOptions = {
duration: 1000
}
): ShikiTransformer => {
return {
name: 'shiki-transformer-copy-button',
code(node) {
const button = h('button', {
class: 'shiki-transformer-button-copy',
'data-code': this.source,
onclick: `
navigator.clipboard.writeText(this.dataset.code);
this.classList.add('shiki-transformer-button-copied');
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
`
}, [
h('span', { class: 'ready' }),
h('span', { class: 'success' })
]);
node.children.push(button)
node.children.push({
type: 'element',
tagName: 'style',
properties: {},
children: [
{
type: 'text',
value: buttonStyles({
successIcon: options.successIcon,
copyIcon: options.copyIcon
})
}
]
})
}
}
}
function buttonStyles({
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
}: {
successIcon?: string,
copyIcon?: string
}) {
let buttonStyle =
`
:root {
--border-color: #e2e2e3;
--background-color: #f6f6f7;
--hover-background-color: #ffff
}
pre:has(code) {
position: relative;
}
pre button.shiki-transformer-button-copy {
position: absolute;
top: 12px;
right: 12px;
z-index: 3;
border: 1px solid var(--border-color);
border-radius: 4px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
place-items: center;
background-color: var(--background-color);
cursor: pointer;
background-repeat: no-repeat;
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
&:hover {
background-color: var(--hover-background-color);
}
& span {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
& .ready {
width: 20px;
height: 20px;
background-image: url("${copyIcon}");
}
& .success {
display: none;
width: 20px;
height: 20px;
background-image: url("${successIcon}");
}
&.shiki-transformer-button-copied {
& .success {
display: block;
}
& .ready {
display: none;
}
}
}`
return buttonStyle
}
```
## 3. 添加插件
```typescript title="astro.config.ts" {1,45,46,47,48,49}#a
+ import { transformerCopyButton } from './src/lib/transformerCopyButton'
export default defineConfig({
site: 'https://dev.2ha.me',
integrations: [
tailwind({
applyBaseStyles: false,
}),
sitemap(),
mdx(),
react(),
icon(),
],
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypeExternalLinks,
{
target: '_blank',
rel: ['nofollow', 'noreferrer', 'noopener'],
},
],
rehypeHeadingIds,
[
rehypeKatex,
{
strict: false,
},
],
sectionize as any,
[
rehypePrettyCode,
{
theme: {
light: 'everforest-dark',
dark: 'everforest-dark',
},
transformers: [
transformerNotationDiff(),
transformerMetaHighlight(),
transformerRenderWhitespace(),
transformerNotationSkip(),
transformerDiffHighlight(),
+ transformerCopyButton({
+ duration: 1000,
+ successIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E",
+ copyIcon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E",
+ })
],
},
],
],
remarkPlugins: [remarkToc, remarkMath, remarkEmoji],
},
server: {
port: 1234,
host: true,
},
devToolbar: {
enabled: false,
},
})
```
## 3. 启动项目后访问blog后报错
```log title="npm run build log"
TypeError: Cannot read properties of undefined (reading 'type')
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:516:22
at Array.flatMap (<anonymous>)
at /vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:507:41
at Array.forEach (<anonymous>)
at Object.root (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/transformers/dist/index.mjs:501:21)
at tokensToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1313:33)
at codeToHast (/vscodeProjects/dev.2ha.me/node_modules/@shikijs/core/dist/index.mjs:1188:10… …
```
## 5. 修改node_modules/@shikijs/transformers依赖中的transformerRenderWhitespace方法
```javascript title="node_modules/@shikijs/transformers/dist/index.mjs" {28,29,30}#a
function transformerRenderWhitespace(options = {}) {
const classMap = {
" ": options.classSpace ?? "space",
" ": options.classTab ?? "tab"
};
const position = options.position ?? "all";
const keys = Object.keys(classMap);
return {
name: "@shikijs/transformers:render-whitespace",
// We use `root` hook here to ensure it runs after all other transformers
root(root) {
const pre = root.children[0];
const code = pre.children[0];
code.children.forEach(
(line) => {
if (line.type !== "element")
return;
const elements = line.children.filter((token) => token.type === "element");
const last = elements.length - 1;
line.children = line.children.flatMap((token) => {
if (token.type !== "element")
return token;
const index = elements.indexOf(token);
if (position === "boundary" && index !== 0 && index !== last)
return token;
if (position === "trailing" && index !== last)
return token;
+ if (token.children.length === 0) {
+ return token;
+ }
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;
const parts = splitSpaces(
node.value.split(/([ \t])/).filter((i) => i.length),
position === "boundary" && index === last && last !== 0 ? "trailing" : position,
position !== "trailing"
);
if (parts.length <= 1)
return token;
return parts.map((part) => {
const clone = {
...token,
properties: { ...token.properties }
};
clone.children = [{ type: "text", value: part }];
if (keys.includes(part)) {
this.addClassToHast(clone, classMap[part]);
delete clone.properties.style;
}
return clone;
});
});
}
);
}
};
}
```

9
src/content/project.mdx vendored Normal file
View File

@@ -0,0 +1,9 @@
---
---
<h3 class="not-prose text-lg font-medium mb-1">
Some work I&rsquo;ve done <span class="text-muted-foreground">ヽ(o^ ^o)ノ</span>
</h3>
<span class="not-prose text-muted-foreground text-xs">
Last updated: 2025-08-11
</span>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

View File

@@ -0,0 +1,8 @@
---
name: '妙妙屋(个人clash订阅管理工具)'
description: '妙妙屋是一个功能强大的Clash订阅管理平台帮助您轻松管理订阅、节点和用户。'
tags: ['open-source', 'personal', 'clash', 'substore']
image: 'assets/mmw.png'
link: 'https://mmwdemo.2ha.me'
startDate: '2025-10-10'
---

View File

@@ -14,13 +14,14 @@ const { title, description, image } = Astro.props
---
<!doctype html>
<html lang="zh">
<html lang="zh" class="dark">
<head>
<Head
title={`${title} | ${SITE.TITLE}`}
description={description}
image={image}
/>
<script is:inline defer src="https://in.2ha.me/script.js" data-website-id="34634aec-34a9-4ef4-9a8f-08ee96699a84"></script>
</head>
<body>
<div

325
src/lib/data-utils.ts Normal file
View File

@@ -0,0 +1,325 @@
import { getCollection, render, type CollectionEntry } from 'astro:content'
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
return await getCollection('authors')
}
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllPostsAndSubposts(): Promise<
CollectionEntry<'blog'>[]
> {
const posts = await getCollection('blog')
return posts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
export async function getAllTags(): Promise<Map<string, number>> {
const posts = await getAllPosts()
return posts.reduce((acc, post) => {
post.data.tags?.forEach((tag) => {
acc.set(tag, (acc.get(tag) || 0) + 1)
})
return acc
}, new Map<string, number>())
}
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects')
return projects.sort((a, b) => {
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
if (orderA !== orderB) {
return orderA - orderB
}
const dateA = a.data.startDate?.getTime() || 0
const dateB = b.data.startDate?.getTime() || 0
return dateB - dateA
})
}
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
parent: CollectionEntry<'blog'> | null
}> {
const allPosts = await getAllPosts()
if (isSubpost(currentId)) {
const parentId = getParentId(currentId)
const allPosts = await getAllPosts()
const parent = allPosts.find((post) => post.id === parentId) || null
const posts = await getCollection('blog')
const subposts = posts
.filter(
(post) =>
isSubpost(post.id) &&
getParentId(post.id) === parentId &&
!post.data.draft,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
const currentIndex = subposts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent }
}
return {
newer:
currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null,
older: currentIndex > 0 ? subposts[currentIndex - 1] : null,
parent,
}
}
const parentPosts = allPosts.filter((post) => !isSubpost(post.id))
const currentIndex = parentPosts.findIndex((post) => post.id === currentId)
if (currentIndex === -1) {
return { newer: null, older: null, parent: null }
}
return {
newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null,
older:
currentIndex < parentPosts.length - 1
? parentPosts[currentIndex + 1]
: null,
parent: null,
}
}
export async function getPostsByAuthor(
authorId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.authors?.includes(authorId))
}
export async function getPostsByTag(
tag: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.filter((post) => post.data.tags?.includes(tag))
}
export async function getRecentPosts(
count: number,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getAllPosts()
return posts.slice(0, count)
}
export async function getSortedTags(): Promise<
{ tag: string; count: number }[]
> {
const tagCounts = await getAllTags()
return [...tagCounts.entries()]
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => {
const countDiff = b.count - a.count
return countDiff !== 0 ? countDiff : a.tag.localeCompare(b.tag)
})
}
export function getParentId(subpostId: string): string {
return subpostId.split('/')[0]
}
export async function getSubpostsForParent(
parentId: string,
): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog')
return posts
.filter(
(post) =>
!post.data.draft &&
isSubpost(post.id) &&
getParentId(post.id) === parentId,
)
.sort((a, b) => {
const dateDiff = a.data.date.valueOf() - b.data.date.valueOf()
if (dateDiff !== 0) return dateDiff
const orderA = a.data.order ?? 0
const orderB = b.data.order ?? 0
return orderA - orderB
})
}
export function groupPostsByYear(
posts: CollectionEntry<'blog'>[],
): Record<string, CollectionEntry<'blog'>[]> {
return posts.reduce(
(acc: Record<string, CollectionEntry<'blog'>[]>, post) => {
const year = post.data.date.getFullYear().toString()
;(acc[year] ??= []).push(post)
return acc
},
{},
)
}
export function groupProjectsByYear(
projects: CollectionEntry<'projects'>[],
): Record<string, CollectionEntry<'projects'>[]> {
return projects.reduce(
(acc: Record<string, CollectionEntry<'projects'>[]>, project) => {
// Use startDate for grouping, fallback to current year if no date
const year = project.data.startDate
? project.data.startDate.getFullYear().toString()
: new Date().getFullYear().toString()
;(acc[year] ??= []).push(project)
return acc
},
{},
)
}
export async function hasSubposts(postId: string): Promise<boolean> {
const subposts = await getSubpostsForParent(postId)
return subposts.length > 0
}
export function isSubpost(postId: string): boolean {
return postId.includes('/')
}
export async function getParentPost(
subpostId: string,
): Promise<CollectionEntry<'blog'> | null> {
if (!isSubpost(subpostId)) {
return null
}
const parentId = getParentId(subpostId)
const allPosts = await getAllPosts()
return allPosts.find((post) => post.id === parentId) || null
}
export async function parseAuthors(authorIds: string[] = []) {
if (!authorIds.length) return []
const allAuthors = await getAllAuthors()
const authorMap = new Map(allAuthors.map((author) => [author.id, author]))
return authorIds.map((id) => {
const author = authorMap.get(id)
return {
id,
name: author?.data?.name || id,
avatar: author?.data?.avatar || '/static/logo.png',
isRegistered: !!author,
}
})
}
export async function getPostById(
postId: string,
): Promise<CollectionEntry<'blog'> | null> {
const allPosts = await getAllPostsAndSubposts()
return allPosts.find((post) => post.id === postId) || null
}
export async function getSubpostCount(parentId: string): Promise<number> {
const subposts = await getSubpostsForParent(parentId)
return subposts.length
}
export async function getCombinedReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
let totalWords = calculateWordCountFromHtml(post.body)
if (!isSubpost(postId)) {
const subposts = await getSubpostsForParent(postId)
for (const subpost of subposts) {
totalWords += calculateWordCountFromHtml(subpost.body)
}
}
return readingTime(totalWords)
}
export async function getPostReadingTime(postId: string): Promise<string> {
const post = await getPostById(postId)
if (!post) return readingTime(0)
const wordCount = calculateWordCountFromHtml(post.body)
return readingTime(wordCount)
}
export type TOCHeading = {
slug: string
text: string
depth: number
isSubpostTitle?: boolean
}
export type TOCSection = {
type: 'parent' | 'subpost'
title: string
headings: TOCHeading[]
subpostId?: string
}
export async function getTOCSections(postId: string): Promise<TOCSection[]> {
const post = await getPostById(postId)
if (!post) return []
const parentId = isSubpost(postId) ? getParentId(postId) : postId
const parentPost = isSubpost(postId) ? await getPostById(parentId) : post
if (!parentPost) return []
const sections: TOCSection[] = []
const { headings: parentHeadings } = await render(parentPost)
if (parentHeadings.length > 0) {
sections.push({
type: 'parent',
title: 'Overview',
headings: parentHeadings.map((heading) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
})),
})
}
const subposts = await getSubpostsForParent(parentId)
for (const subpost of subposts) {
const { headings: subpostHeadings } = await render(subpost)
if (subpostHeadings.length > 0) {
sections.push({
type: 'subpost',
title: subpost.data.title,
headings: subpostHeadings.map((heading, index) => ({
slug: heading.slug,
text: heading.text,
depth: heading.depth,
isSubpostTitle: index === 0,
})),
subpostId: subpost.id,
})
}
}
return sections
}

View File

@@ -0,0 +1,133 @@
import type { ShikiTransformer } from "shiki";
import { h } from "hastscript";
/**
* shikijs transformerRenderWhitespace 会报错 Cannot read properties of undefined (reading 'type'), 解决办法是
* @shikijs/transformers/dist/index.mjs:516:22 加一个判断 提前返回
* 如下
* if (token.children.length === 0) {
return token;
}
在这行代码之前加上面这段代码
const node = token.children[0];
if (node.type !== "text" || !node.value)
return token;
*/
export interface CopyButtonOptions {
duration?: number;
copyIcon?: string;
successIcon?: string
}
export const transformerCopyButton = (
options: CopyButtonOptions = {
duration: 1000
}
): ShikiTransformer => {
return {
name: 'shiki-transformer-copy-button',
code(node) {
const button = h('button', {
class: 'shiki-transformer-button-copy',
'data-code': this.source,
onclick: `
navigator.clipboard.writeText(this.dataset.code);
this.classList.add('shiki-transformer-button-copied');
setTimeout(() => this.classList.remove('shiki-transformer-button-copied'), ${options.duration})
`
}, [
h('span', { class: 'ready' }),
h('span', { class: 'success' })
]);
node.children.push(button)
node.children.push({
type: 'element',
tagName: 'style',
properties: {},
children: [
{
type: 'text',
value: buttonStyles({
successIcon: options.successIcon,
copyIcon: options.copyIcon
})
}
]
})
}
}
}
function buttonStyles({
successIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='rgba(5,223,114,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M16 3h2.6A2.4 2.4 0 0 1 21 5.4v15.2a2.4 2.4 0 0 1-2.4 2.4H5.4A2.4 2.4 0 0 1 3 20.6V5.4A2.4 2.4 0 0 1 5.4 3H8m0 11l3 3l5-7M8.8 1h6.4a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-.8.8H8.8a.8.8 0 0 1-.8-.8V1.8a.8.8 0 0 1 .8-.8'/%3E%3C/svg%3E",
copyIcon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='rgba(128,128,128,1)'%20stroke-linecap='round'%20stroke-linejoin='round'%20stroke-width='2'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='8'%20height='4'%20x='8'%20y='2'%20rx='1'%20ry='1'/%3E%3Cpath%20d='M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2'/%3E%3C/svg%3E",
}: {
successIcon?: string,
copyIcon?: string
}) {
let buttonStyle =
`
:root {
--border-color: #e2e2e3;
--background-color: #f6f6f7;
--hover-background-color: #ffff
}
pre:has(code) {
position: relative;
}
pre button.shiki-transformer-button-copy {
position: absolute;
top: 12px;
right: 12px;
z-index: 3;
border: 1px solid var(--border-color);
border-radius: 4px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
place-items: center;
background-color: var(--background-color);
cursor: pointer;
background-repeat: no-repeat;
transition: var(--border-color) .25s, var(--background-color) .25s, opacity .25s;
&:hover {
background-color: var(--hover-background-color);
}
& span {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
& .ready {
width: 20px;
height: 20px;
background-image: url("${copyIcon}");
}
& .success {
display: none;
width: 20px;
height: 20px;
background-image: url("${successIcon}");
}
&.shiki-transformer-button-copied {
& .success {
display: block;
}
& .ready {
display: none;
}
}
}`
return buttonStyle
}

View File

@@ -29,7 +29,7 @@ export function transformerNotationSkip(
if (classActivePre) this.addClassToHast(this.pre, classActivePre)
return false
},
false, // remove empty lines
)
undefined, // remove empty lines
) as ShikiTransformer
}

View File

@@ -13,13 +13,36 @@ export function formatDate(date: Date) {
}).format(date)
}
export function readingTime(html: string) {
export function formatMonthYear(date: Date) {
return Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
}).format(date)
}
export function calculateWordCountFromHtml(
html: string | null | undefined,
): number {
if (!html) return 0
const textOnly = html.replace(/<[^>]+>/g, '')
const wordCount = textOnly.split(/\s+/).length
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
return textOnly.split(/\s+/).filter(Boolean).length
}
export function readingTime(wordCount: number): string {
const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
return `${readingTimeMinutes} min read`
}
export function getHeadingMargin(depth: number): string {
const margins: Record<number, string> = {
3: 'ml-4',
4: 'ml-8',
5: 'ml-12',
6: 'ml-16',
}
return margins[depth] || ''
}
export function getElapsedTime(unixTimestamp: number): string {
const createdAt = new Date(unixTimestamp)
const now = new Date()
@@ -33,3 +56,12 @@ export function getElapsedTime(unixTimestamp: number): string {
.toString()
.padStart(2, '0')}:${seconds.toString().padStart(2, '0')} elapsed`
}
export function extractDomain(url: string): string {
try {
const domain = new URL(url).hostname
return domain.startsWith('www.') ? domain.slice(4) : domain
} catch {
return url
}
}

View File

@@ -40,7 +40,7 @@ const authors = await getCollection('authors')
<TableHeader>
<TableRow>
<TableHead className="max-w-[80px]">Path</TableHead>
<TableHead className="max-w-[80px] flex items-center">Illustrator</TableHead>
<TableHead className="max-w-[80px] flex items-center">Author</TableHead>
<TableHead className="text-right">Resources</TableHead>
</TableRow>
</TableHeader>
@@ -134,6 +134,19 @@ const authors = await getCollection('authors')
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Background</TableCell>
<TableCell className="flex items-center">
<Link class="contents" href="https://mall.bilibili.com/neul-next/index.html?page=mall-up_itemDetail&noTitleBar=1&itemsId=1107984035&from=items_share&msource=items_share" target="_blank">
@小猫女仆降临 <svg class="size-8" style="filter: invert(100%);" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
</Link>
</TableCell>
<TableCell className="text-right">
<Link href="https://www.bilibili.com/video/BV1462uY8Eo4" target="_blank">
<img class="h-12 w-25" src="/static/images/maoliang.gif" />
</Link>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>

View File

@@ -61,7 +61,8 @@ const subposts = allPosts.filter((p) => p.data.parentTitle === post.data.title)
const totalBody = [post.body!, ...subposts.map((p) => p.body!)]
.map(stripCodeBlocks)
.join('')
const readTime = readingTime(totalBody)
const wordCount = totalBody.split(/\s+/).filter(Boolean).length
const readTime = readingTime(wordCount)
---
<Layout

View File

@@ -57,35 +57,24 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
---
<Layout title="Blog" description="Blog">
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div
class="sm:none hidden h-full max-h-screen min-w-[280px] max-w-[280px] flex-wrap overflow-auto rounded-sm bg-gray-50 pt-5 shadow-md lg:contents dark:bg-gray-900/70 dark:shadow-gray-800/40"
>
<div
class="absolute left-4 top-[50%] px-4 py-2"
style="transform: translateY(-50%);"
<!-- Fixed Sidebar -->
<aside
class="hidden xl:block fixed top-1/2 -translate-y-1/2 w-[240px] px-4 py-3 rounded-sm bg-background/80 backdrop-blur-sm border-2 border-[color:rgba(241,140,110,0.22)] shadow-[4px_4px_0_rgba(0,0,0,0.22)] dark:shadow-[4px_4px_0_rgba(0,0,0,0.65)] z-10"
>
<Link
href={`/blog`}
class="hover:text-primary-500 dark:hover:text-primary-500 font-bold uppercase"
class="block mb-2 font-bold uppercase text-primary hover:text-primary/80 transition-colors"
>
All Posts
</Link>
<ul>
<ul class="space-y-0.5">
{
sortedTags.map((t) => {
return (
<li value={t} class="my-0">
<li value={t}>
<Link
href={`/tags/${slug(t)}`}
class="hover:text-primary-500 dark:hover:text-primary-500 px-3 py-2 text-sm font-medium uppercase text-gray-400 dark:text-gray-300"
class="block px-2 py-1.5 text-sm font-medium uppercase text-foreground/60 hover:text-primary hover:bg-primary/10 rounded transition-colors"
aria-label={`View posts tagged ${t}`}
>
{`${t} (${tagCounts[t]})`}
@@ -95,21 +84,24 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
})
}
</ul>
<span
class="ms-auto inline-flex h-6 items-center text-base sm:text-end"
>
<div class="mt-2 pt-2 border-t border-[color:rgba(241,140,110,0.15)]">
<a
aria-label="View all blog categories"
class="sm:hover:text-accent-two font-medium text-accent"
style="color: #e9d3b6; font-size: 12px;"
class="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
href="/tags/"
>
View all →
</a>
</span>
</div>
</div>
</aside>
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: 'Blog', href: '/blog', icon: 'lucide:archive' },
{ label: `Page ${page.currentPage}`, icon: 'lucide:folder-open' },
]}
/>
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
@@ -135,3 +127,21 @@ const sortedTags = tagKeys.sort((a, b) => tagCounts[b] - tagCounts[a])
/>
</Container>
</Layout>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -1,17 +1,19 @@
---
import AuthorPresence from '@/components/bento/AuthorPresence'
import WakatimeGraph from '@/components/bento/WakatimeGraph.tsx'
import WakatimeGraph from '@/components/bento/WakatimeGraph'
import Link from '@/components/Link.astro'
import FuzzyText from '@/components/magicui/fuzzy-text'
// import FuzzyText from '@/components/magicui/fuzzy-text'
import GradientText from '@/components/magicui/gradint-text'
import LetterGlitch from '@/components/magicui/letter-glitch'
import ShortCuts from '@/components/ShortCuts.astro'
import { SITE, SOCIAL_LINKS } from '@/consts'
import Layout from '@/layouts/Layout.astro'
import { Icon } from 'astro-icon/components'
import { getCollection } from 'astro:content'
import GiteaCalendar from '@/components/bento/GiteaCalendar'
import Music163Player from '@/components/bento/Music163Player'
import GiteaCalendar from '@/components/custom/GiteaCalendar'
import Music163Player from '@/components/custom/Music163Player'
import RandomAnimeBackground from '@/components/custom/RandomAnimeBackgrounds'
// import DevShortCuts from '@/components/DevShortCuts.astro'
import DevShortcutsHexagon from '@/components/DevShortcutsHexagon.astro'
const latestPost = await getCollection('blog').then((posts: any[]) =>
posts
@@ -22,86 +24,48 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
.filter((post) => !post.data.hidden && !post.data.draft)
.at(0),
)
---
<Layout title="主页" description={SITE.DESCRIPTION}>
<section
class="mx-auto grid max-w-[375px] grid-cols-2 gap-4 px-4 [grid-template-areas:'a_a'_'a_a'_'b_b'_'b_b'_'e_e'_'h_i'_'h_c'_'k_c'_'d_d'_'d_d'_'g_g'_'g_g'_'f_f'_'j_j'_'j_j'] *:rounded-3xl *:border *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat sm:max-w-screen-sm sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'h_i'_'h_c'_'k_c'_'f_f'] xl:max-w-screen-xl xl:grid-cols-4 xl:[grid-template-areas:'a_a_b_c'_'d_e_e_c'_'h_f_f_g'_'h_i_j_k'] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&:hover>*:not(.first):hover_.overlay]:opacity-100"
class="mx-auto grid w-full grid-cols-1 gap-4 px-4 [grid-template-areas:'a'_'d'_'b'_'i'_'e'_'g'_'f'_'j'_'k'] [grid-template-rows:repeat(9,auto)] *:border-2 *:border-[color-mix(in_srgb,hsl(var(--primary))_22%,hsl(var(--border)))] *:bg-secondary/25 *:bg-cover *:bg-center *:bg-no-repeat *:overflow-hidden *:w-full *:[box-shadow:4px_4px_0_rgba(0,0,0,0.22)] *:transition-all *:duration-200 sm:max-w-screen-sm sm:grid-cols-2 sm:[grid-template-areas:'a_a'_'b_d'_'e_e'_'j_g'_'f_f'_'i_k'] sm:[grid-template-rows:repeat(6,300px)] sm:*:max-w-[615px] xl:max-w-screen-xl xl:grid-cols-4 xl:[grid-template-areas:'a_a_b_g'_'d_e_e_i'_'k_j_f_f'] xl:[grid-template-rows:repeat(3,300px)] xl:*:max-w-none dark:*:[box-shadow:4px_4px_0_rgba(0,0,0,0.65)] xl:[&:hover:has(>.has-overlay:hover)>.first>.overlay]:opacity-100 xl:[&>*:not(.first):hover_.overlay]:opacity-100"
aria-label="Personal information and activity grid"
>
<div
class="first flex flex-row justify-center aspect-square rounded-3xl border bg-[url('/static/images/tou.gif')] bg-cover bg-color bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] sm:bg-[url('/static/images/tou.gif')] xl:aspect-auto"
class="first flex flex-row xl:max-h-[298px] xl:min-w-[615px] grow-[0] justify-center aspect-square bg-[url('/static/loading.gif')] border bg-cover bg-center bg-position-inherit bg-no-repeat [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
role="img"
aria-label="Introduction"
style="filter: contrast(2) saturate(0) invert(100) sepia(100); border-color: #afafaf;"
>
<!-- <div
class="overlay size-full rounded-3xl bg-[url('/static/404.webp')] bg-cover bg-center bg-no-repeat opacity-0 transition-opacity duration-200"
>
</div> -->
<!-- class="first relative flex aspect-square justify-center rounded-3xl border [grid-area:a] sm:aspect-[2.1/1] xl:aspect-auto" -->
<!-- <img
class="no-repeat relative w-full max-w-fit justify-center rounded-3xl object-cover"
src="/static/404_white_mask.webp"
/> -->
<!-- <div class="absolute self-end p-2 justify-center">
<FuzzyText
client:load
baseIntensity={0.04}
hoverIntensity={0.24}
fontSize={96}
color="#fbb229"
,>4O4</FuzzyText
>
<div class="mb-3 mt-3"></div>
<FuzzyText
client:load
baseIntensity={0.04}
hoverIntensity={0.24}
fontSize={36}
color="#f95038">Not Found</FuzzyText
>
</div> -->
aria-label="Introduction"
>
<RandomAnimeBackground client:load/>
</div>
<div
class="has-overlay relative grid aspect-square grid-cols-4 grid-rows-3 items-center
justify-center bg-[url('/static/honeycomb.webp')] [grid-area:b]"
justify-center [grid-area:b] short-cuts-template hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
style="grid-template-columns: 2.85fr 2.95fr 2.9fr 1.3fr;grid-template-rows: 4.7fr 3.4fr 2.8fr;"
aria-label="Developer Stack Shortcuts"
>
<img
class="overlay absolute no-repeat w-full max-w-fit justify-center rounded-3xl object-cover z-9"
src="/static/images/shortcuts-bg.png"
class="overlay absolute bottom-0 right-0 no-repeat max-w-[28%] object-contain z-[100]"
src="/static/images/shortcuts-bg-mini.png"
/>
<!-- <DevStackIconsCloud client:load/> -->
<ShortCuts />
<!-- <DevShortCuts /> -->
<DevShortcutsHexagon />
</div>
<div class="aspect-[1/2.1] [grid-area:c] bg-[url('/static/images/ump45.png')] xl:aspect-auto bg-color" aria-hidden="true">
<!-- <MagnetLines
client:load
rows={9}
columns={4}
containerSize="100%"
lineColor="#e9d3b6"
lineWidth="0.8vmin"
lineHeight="4vmin"
baseAngle={0}
/> -->
</div>
<div class="relative overflow-hidden [grid-area:d] sm:aspect-square min-h-[300px]">
<div class="relative overflow-hidden [grid-area:d] aspect-square hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<AuthorPresence client:load />
</div>
<div
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto"
class="has-overlay min-h-[300px] relative flex grid aspect-[6/5] grid-rows-2 items-start overflow-hidden p-1 [grid-area:e] sm:aspect-[2.1/1] sm:items-center xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
style="grid-template-rows: 9fr 1fr; "
>
<div
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/lastblogbg-sm.webp')] bg-cover bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/lastblogbg.webp')] xl:opacity-100"
>
</div>
{
@@ -114,7 +78,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
alt={`Featured image for the latest post: ${latestPost.data.title}`}
width={477}
height={251}
class="w-full rounded-2xl border border-border sm:ml-1 sm:w-[82%] "
class="w-full border-2 border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] sm:w-[82%]"
/>
</Link>
@@ -134,7 +98,7 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
aria-label={`Read latest blog post: ${latestPost.data.title}`}
title={`Read latest blog post: ${latestPost.data.title}`}
>
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary">
<div class="absolute top-0 right-0 m-3 flex w-fit items-end rounded-full border bg-secondary/50 p-3 text-primary transition-all duration-300 hover:rotate-12 hover:ring-1 hover:ring-primary z-10">
<Icon name="lucide:move-up-right" size={16} />
</div>
</Link>
@@ -144,36 +108,30 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto"
class="has-overlay xl:min-w-[615px] relative flex aspect-square items-center justify-center overflow-hidden [grid-area:f] sm:aspect-[2.1/1] xl:aspect-auto hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 z-[1] size-full rounded-3xl bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
class="overlay absolute inset-0 z-[1] size-full bg-[url('/static/images/contributions-square.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 sm:bg-[url('/static/images/contributions.png')] xl:opacity-100"
>
</div>
<GiteaCalendar client:load />
</div>
<div
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none"
class="has-overlay relative aspect-square [grid-area:g] hover:bg-none hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 z-0 size-full rounded-3xl bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 z-0 size-full bg-[url('/static/images/music163.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Music163Player client:load />
</div>
<div
class="aspect-[1/2.1] bg-[url('/static/images/ump9.png')] [grid-area:h] xl:aspect-auto bg-color"
aria-hidden="true"
>
</div>
<div
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] "
class="has-overlay relative flex aspect-square items-center justify-center [grid-area:i] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]"
>
<div
class="overlay absolute inset-0 size-full rounded-3xl bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
class="overlay absolute inset-0 size-full bg-[url('/static/images/github.png')] bg-cover bg-center bg-no-repeat transition-opacity duration-200 xl:opacity-100"
>
</div>
<Icon
@@ -195,12 +153,12 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</Link>
</div>
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat ">
<div class="has-overlay aspect-square [grid-area:j] bg-[url('/static/images/waketime.png')] bg-cover bg-center bg-no-repeat hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<WakatimeGraph omitLanguages={['Markdown', 'JSON']} client:load />
</div>
<!-- 字符滚动 -->
<div class="aspect-square [grid-area:k]">
<div class="aspect-square [grid-area:k] hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.26)] hover:-translate-y-1 hover:border-[color-mix(in_srgb,hsl(var(--primary))_40%,hsl(var(--border)))] dark:hover:[box-shadow:6px_6px_0_rgba(0,0,0,0.75)]">
<LetterGlitch
client:load
glitchColors={['#de17a5', '#5617de', '#e9d3b6']}
@@ -213,3 +171,21 @@ const latestPost = await getCollection('blog').then((posts: any[]) =>
</div>
</section>
</Layout>
<style>
section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark section > div:hover {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

57
src/pages/projects.astro Normal file
View File

@@ -0,0 +1,57 @@
---
import Breadcrumbs from '@/components/Breadcrumbs.astro'
import Container from '@/components/Container.astro'
import PageHead from '@/components/PageHead.astro'
import ProjectCard from '@/components/ProjectCard.astro'
import Layout from '@/layouts/Layout.astro'
import { getAllProjects, groupProjectsByYear } from '@/lib/data-utils'
const projects = await getAllProjects()
const projectsByYear = groupProjectsByYear(projects)
const years = Object.keys(projectsByYear).sort(
(a, b) => parseInt(b) - parseInt(a),
)
---
<Layout title="项目" description="dev.2ha.me的项目">
<PageHead slot="head" title="Project" />
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs items={[{ label: 'Project', icon: 'lucide:briefcase' }]} />
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
{
years.map((year) => (
<section class="flex flex-col gap-y-4">
<div class="font-semibold">{year}</div>
<ul class="not-prose flex flex-col gap-4">
{projectsByYear[year].map((project) => (
<li>
<ProjectCard project={project} />
</li>
))}
</ul>
</section>
))
}
</div>
</Container>
</Layout>
<style>
ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.26) !important;
}
.dark ul li > :global(div:hover) {
box-shadow:
0 0 16px rgba(241, 140, 110, 0.5),
0 0 32px rgba(241, 140, 110, 0.3),
0 0 48px rgba(241, 140, 110, 0.15),
6px 6px 0 rgba(0, 0, 0, 0.75) !important;
}
</style>

View File

@@ -1,3 +1,5 @@
@import './pixel-components.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -10,6 +12,16 @@
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff2') format('woff2'),
url('/fonts/OPlusSans3-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono[wght].woff2') format('woff2-variations');
@@ -32,100 +44,117 @@
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
/* MiaoMiaoWu Brand Colors */
--brand-50: #fef5f2;
--brand-100: #fde8e2;
--brand-200: #fbd4c9;
--brand-300: #f7b5a3;
--brand-400: #f18c6e;
--brand-500: #d97757;
--brand-600: #c55438;
--brand-700: #a4432d;
--brand-800: #873829;
--brand-900: #713128;
/* Semantic Colors - Light Mode Default */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%; /* #d97757 橙色 */
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%; /* #f7b5a3 */
--accent-foreground: 16 73% 11%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0; /* 无圆角! */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
}
.light {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
/* Same as :root for light mode */
--background: 60 20% 99%;
--foreground: 16 62% 15%;
--primary: 15 66% 59%;
--primary-foreground: 33 100% 99%;
--secondary: 22 64% 93%;
--secondary-foreground: 15 60% 27%;
--muted: 30 43% 94%;
--muted-foreground: 15 25% 51%;
--accent: 15 62% 78%;
--accent-foreground: 16 73% 11%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--border: 15 30% 55% / 0.38;
--ring: 15 66% 59% / 0.6;
--input: 15 30% 55% / 0.45;
--radius: 0;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-1: 15 66% 59%;
--chart-2: 15 71% 63%;
--chart-3: 45 97% 63%;
--chart-4: 203 92% 70%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
--card: 60 20% 99%;
--card-foreground: 16 68% 13%;
--popover: 60 20% 99%;
--popover-foreground: 16 62% 14%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
/* Dark Mode - MiaoMiaoWu Style */
--background: 222 29% 8%; /* #10131c 深蓝灰 */
--foreground: 33 83% 97%; /* #f9f4f1 暖白 */
--primary: 15 71% 69%; /* #f18c6e 亮橙色 */
--primary-foreground: 16 65% 13%; /* #30160f 深色 */
--secondary: 225 23% 13%; /* #1d2232 深蓝灰 */
--secondary-foreground: 22 64% 93%;
--muted: 224 24% 12%; /* #1c2131 深蓝 */
--muted-foreground: 15 25% 75%; /* #cfb8af 浅灰棕 */
--accent: 15 71% 69% / 0.22; /* 半透明橙 */
--accent-foreground: 22 100% 93%; /* #ffe5da */
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive: 0 70% 68%; /* #f87171 亮红 */
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--border: 0 0% 100% / 0.08; /* 半透明白 */
--ring: 15 71% 69% / 0.45;
--input: 0 0% 100% / 0.12;
--radius: 0;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-1: 15 71% 69%;
--chart-2: 199 89% 61%;
--chart-3: 271 70% 67%;
--chart-4: 45 98% 54%;
--chart-5: 186 78% 54%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
--card: 222 29% 8%;
--card-foreground: 22 75% 94%;
--popover: 222 29% 8%;
--popover-foreground: 33 83% 97%;
}
*,
@@ -136,6 +165,7 @@
html {
color-scheme: dark;
overflow-y: scroll;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@@ -455,8 +485,105 @@
* {
@apply border-border;
}
/* Prevent text selection cursor on all elements by default */
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-tap-highlight-color: transparent;
cursor: default;
}
/* Allow text selection in content areas */
article,
article *,
.prose,
.prose *,
input,
textarea,
[contenteditable="true"] {
user-select: text;
-webkit-user-select: text;
}
/* Only inputs and textareas should have text cursor */
input,
textarea,
[contenteditable="true"] {
cursor: text;
}
/* Ensure proper cursor on interactive elements */
button,
a,
div,
img,
svg,
.pixel-button,
[role="button"],
[role="link"] {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
cursor: pointer;
}
/* Children of interactive elements should not have text cursor */
button *,
a *,
.pixel-button *,
[role="button"] *,
[role="link"] * {
cursor: pointer;
user-select: none;
}
/* Images should never show text cursor */
img,
svg,
video,
canvas {
user-select: none;
-webkit-user-select: none;
cursor: default;
}
/* Allow text selection for content elements but keep default cursor */
p,
span,
h1,
h2,
h3,
h4,
h5,
h6,
li {
user-select: text;
-webkit-user-select: text;
cursor: default;
}
body {
@apply bg-background text-foreground;
@apply text-foreground;
/* MiaoMiaoWu Background Gradients */
background-image:
radial-gradient(1100px circle at 5% -10%, rgba(217, 119, 87, 0.2), transparent 60%),
radial-gradient(900px circle at 90% 0%, rgba(96, 165, 250, 0.12), transparent 65%),
linear-gradient(180deg, rgba(255, 247, 242, 0.98), rgba(255, 247, 242, 1));
background-attachment: fixed;
background-size: cover;
background-color: hsl(var(--background));
}
.dark body {
background-image:
radial-gradient(900px circle at 15% -5%, rgba(241, 140, 110, 0.32), transparent 65%),
radial-gradient(800px circle at 82% 0%, rgba(56, 189, 248, 0.18), transparent 70%),
linear-gradient(180deg, rgba(16, 19, 28, 1), rgba(10, 12, 20, 1));
background-attachment: fixed;
background-size: cover;
}
}
@@ -518,3 +645,8 @@
.bg-color {
background-color: #e9d3b6 !important;
}
.shiki-transformer-button-copy {
border: none !important;
background-color: hsl(0deg 0% 0% / 0%) !important;
}

View File

@@ -0,0 +1,533 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'haipaiqiangdiaosenxiyuan';
src: url('/fonts/haipaiqiangdiaosenxiyuan.woff');
font-weight: 100 800;
font-style: normal;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'OPlusSans3-Medium';
src: url('/fonts/OPlusSans3-Medium.woff');
font-weight: 100 800;
font-style: normal;
/* unicode-range: U+2E80-2EFF,U+3400-4DBF,U+4E00-9FFF; */
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono-Italic[wght].woff2') format('woff2-variations');
font-weight: 100 800;
font-style: italic;
font-display: swap;
}
.styles-module_calendar__sT1ND text {
fill: currentColor;
}
@layer base {
:root {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 34.12deg 53.68% 81.37%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
.light {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--input: 240 5.9% 90%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--additive: 112 50% 36%;
--additive-foreground: 0 0% 9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 33 12% 33%;
--chart-7: 32 12% 25%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--input: 240 3.7% 15.9%;
}
*,
*::before,
*::after {
@apply border-border;
}
html {
color-scheme: dark;
@apply bg-background text-foreground forced-color-adjust-none;
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
}
.disable-transitions,
.disable-transitions * {
@apply !transition-none;
}
.theme {
--animate-gradient: gradient 8s linear infinite;
}
}
.svg-bg-color {
background-color: hsl(var(--primary));
}
:root,
::backdrop {
--sl-color-white: hsl(0, 0%, 100%);
--sl-color-gray-1: hsl(224, 20%, 94%);
--sl-color-gray-2: hsl(224, 6%, 77%);
--sl-color-gray-3: hsl(224, 6%, 56%);
--sl-color-gray-4: hsl(224, 7%, 36%);
--sl-color-gray-5: hsl(224, 10%, 23%);
--sl-color-gray-6: hsl(224, 14%, 16%);
--sl-color-black: hsl(224, 10%, 10%);
--sl-hue-orange: 41;
--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
--sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);
--sl-hue-green: 101;
--sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);
--sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);
--sl-hue-blue: 234;
--sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);
--sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);
--sl-hue-purple: 281;
--sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);
--sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);
--sl-hue-red: 339;
--sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);
--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
--sl-color-accent-low: hsl(224, 54%, 20%);
--sl-color-accent: hsl(224, 100%, 60%);
--sl-color-accent-high: hsl(224, 100%, 85%);
--sl-color-text: var(--sl-color-gray-2);
--sl-color-text-accent: var(--sl-color-accent-high);
--sl-color-text-invert: var(--sl-color-accent-low);
--sl-color-bg: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-6);
--sl-color-bg-sidebar: var(--sl-color-gray-6);
--sl-color-bg-inline-code: var(--sl-color-gray-5);
--sl-color-bg-accent: var(--sl-color-accent-high);
--sl-color-hairline-light: var(--sl-color-gray-5);
--sl-color-hairline: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-black);
--sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12),
0px 2px 1px hsla(0, 0%, 0%, 0.24);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08),
0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12),
0px 1px 1px hsla(0, 0%, 0%, 0.15);
--sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03),
0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33),
0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);
--sl-text-2xs: 0.75rem;
--sl-text-xs: 0.8125rem;
--sl-text-sm: 0.875rem;
--sl-text-base: 1rem;
--sl-text-lg: 1.125rem;
--sl-text-xl: 1.25rem;
--sl-text-2xl: 1.5rem;
--sl-text-3xl: 1.8125rem;
--sl-text-4xl: 2.1875rem;
--sl-text-5xl: 2.625rem;
--sl-text-6xl: 4rem;
--sl-text-body: var(--sl-text-base);
--sl-text-body-sm: var(--sl-text-xs);
--sl-text-code: var(--sl-text-sm);
--sl-text-code-sm: var(--sl-text-xs);
--sl-text-h1: var(--sl-text-4xl);
--sl-text-h2: var(--sl-text-3xl);
--sl-text-h3: var(--sl-text-2xl);
--sl-text-h4: var(--sl-text-xl);
--sl-text-h5: var(--sl-text-lg);
--sl-line-height: 1.75;
--sl-line-height-headings: 1.2;
--sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);
--__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)),
var(--sl-font-system-mono);
--sl-nav-height: 3.5rem;
--sl-nav-pad-x: 1rem;
--sl-nav-pad-y: 0.75rem;
--sl-mobile-toc-height: 3rem;
--sl-sidebar-width: 18.75rem;
--sl-sidebar-pad-x: 1rem;
--sl-content-width: 45rem;
--sl-content-pad-x: 1rem;
--sl-menu-button-size: 2rem;
--sl-nav-gap: var(--sl-content-pad-x);
--sl-outline-offset-inside: -0.1875rem;
--sl-z-index-toc: 4;
--sl-z-index-menu: 5;
--sl-z-index-navbar: 10;
--sl-z-index-skiplink: 20;
}
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
--sl-color-white: hsl(224, 10%, 10%);
--sl-color-gray-1: hsl(224, 14%, 16%);
--sl-color-gray-2: hsl(224, 10%, 23%);
--sl-color-gray-3: hsl(224, 7%, 36%);
--sl-color-gray-4: hsl(224, 6%, 56%);
--sl-color-gray-5: hsl(224, 6%, 77%);
--sl-color-gray-6: hsl(224, 20%, 94%);
--sl-color-gray-7: hsl(224, 19%, 97%);
--sl-color-black: hsl(0, 0%, 100%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);
--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);
--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);
--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);
--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);
--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);
--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);
--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);
--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);
--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);
--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);
--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);
--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
--sl-color-accent-high: hsl(234, 80%, 30%);
--sl-color-accent: hsl(234, 90%, 60%);
--sl-color-accent-low: hsl(234, 88%, 90%);
--sl-color-text-accent: var(--sl-color-accent);
--sl-color-text-invert: var(--sl-color-black);
--sl-color-bg-nav: var(--sl-color-gray-7);
--sl-color-bg-sidebar: var(--sl-color-bg);
--sl-color-bg-inline-code: var(--sl-color-gray-6);
--sl-color-bg-accent: var(--sl-color-accent);
--sl-color-hairline-light: var(--sl-color-gray-6);
--sl-color-hairline-shade: var(--sl-color-gray-6);
--sl-color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
--sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.06),
0px 2px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.03),
0px 5px 2px hsla(0, 0%, 0%, 0.03), 0px 3px 2px hsla(0, 0%, 0%, 0.06),
0px 1px 1px hsla(0, 0%, 0%, 0.06);
--sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, 0.01),
0px 16px 6px hsla(0, 0%, 0%, 0.03), 0px 9px 5px hsla(223, 13%, 10%, 0.08),
0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
}
@layer components {
article {
@apply prose-headings:scroll-mt-20 prose-headings:break-words;
@apply prose-p:break-words;
@apply prose-a:!text-primary prose-a:!decoration-primary/50 prose-a:!underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-inherit;
@apply prose-blockquote:!not-italic prose-blockquote:!text-muted-foreground;
@apply prose-pre:!px-0;
@apply prose-img:mx-auto prose-img:rounded-xl prose-img:border;
@apply prose-table:mx-auto prose-table:block prose-table:max-w-fit prose-table:overflow-x-auto prose-td:break-words;
@apply sm:prose-table:mx-0 sm:prose-table:table sm:prose-table:max-w-none;
/* Fixing Katex display */
.katex-display {
@apply overflow-x-auto overflow-y-hidden py-4;
}
/* Fixing Katex fractions */
.frac-line {
@apply border-foreground;
}
/* Removes background from <mark> elements */
mark {
@apply bg-transparent;
}
/* Blanket syntax highlighting */
code[data-theme*=' '] {
span {
color: var(--shiki-dark);
}
.dark & span {
color: var(--shiki-dark);
}
}
/* Inline code */
:not(pre) > code {
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium before:!content-none after:!content-none;
}
/* Code blocks */
figure[data-rehype-pretty-code-figure] {
@apply relative;
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {
@apply mt-0 rounded-t-none;
}
}
/* Shadcn-like scrollbar */
pre::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
pre::-webkit-scrollbar-track {
@apply bg-transparent;
}
pre::-webkit-scrollbar-thumb {
@apply rounded-full bg-border bg-clip-padding p-px;
}
/* Code block styles */
pre {
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
/* Code block content */
> code {
@apply whitespace-pre-wrap;
counter-reset: line;
/* For code blocks with line numbers */
&[data-line-numbers] {
> [data-line]::before {
counter-increment: line;
content: counter(line);
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
}
}
/* For each line in the code block */
> [data-line] {
@apply px-4;
}
/* Highlighted lines */
[data-highlighted-line] {
@apply bg-foreground/10;
}
/* Highlighted characters */
[data-highlighted-chars] > span {
@apply bg-muted-foreground/40 py-[7px];
}
.tab {
@apply relative;
}
.tab::before {
@apply absolute opacity-30;
content: '⇥';
}
/* Skip lines */
.skip {
@apply my-2 bg-foreground/5 text-center text-foreground;
&::before {
content: '' !important;
}
}
/* Diff lines */
.diff {
&.add {
@apply bg-additive/15;
}
&.remove {
@apply bg-destructive/15;
&::before {
content: '-';
counter-increment: none;
}
}
&.highlight {
@apply bg-foreground/10;
}
}
/* Copy button */
> button:has(> span) {
@apply right-1 top-1 m-0 size-8 rounded-lg bg-secondary p-1 backdrop-blur-none transition-all duration-200;
}
}
}
}
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
@theme inline {
@keyframes gradient {
to {
backgroundposition: var(--bg-size, 300%) 0;
}
}
}
/* patch */
.recharts-cartesian-grid-vertical {
display: none;
}
/*
@media screen and (min-width: 1280px) {
html {
zoom: 0.95;
}
} */
.bg-color {
background-color: #e9d3b6 !important;
}
.shiki-transformer-button-copy {
border: none !important;
background-color: hsl(0deg 0% 0% / 0%) !important;
}

View File

@@ -0,0 +1,252 @@
/* MiaoMiaoWu Pixel Style Components */
/* Pixel Border - 像素边框 */
.pixel-border {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 60%, transparent);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.pixel-border:hover {
border-color: color-mix(in srgb, hsl(var(--primary)) 75%, transparent);
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.24),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-border {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.dark .pixel-border:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Button - 像素按钮 */
.pixel-button {
position: relative;
border-width: 2px;
border-style: solid;
border-color: transparent;
border-radius: 0;
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.18);
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
background-color 0.18s ease,
border-color 0.18s ease;
}
.pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22);
transform: translateY(-1px);
}
.pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.16);
transform: translate(0);
}
.dark .pixel-button {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45);
}
.dark .pixel-button:hover {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
}
.dark .pixel-button:active {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}
/* Pixel Card - 像素卡片 */
.pixel-card {
position: relative;
border-width: 2px;
border-style: solid;
border-color: color-mix(in srgb, hsl(var(--primary)) 22%, hsl(var(--border)));
background: hsl(var(--card));
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(255, 255, 255, 0.04);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.pixel-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, hsl(var(--primary)) 40%, hsl(var(--border)));
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.26),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* 悬停时显示斜纹背景 */
.pixel-card::before {
content: '';
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
45deg,
transparent 0,
transparent 6px,
rgba(255, 255, 255, 0.04) 6px,
rgba(255, 255, 255, 0.04) 12px
);
opacity: 0;
transition: opacity 0.2s ease;
z-index: 0;
}
.pixel-card:hover::before {
opacity: 1;
}
.pixel-card > * {
position: relative;
z-index: 1;
}
.dark .pixel-card {
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.65),
0 0 0 1px rgba(255, 255, 255, 0.04);
}
.dark .pixel-card:hover {
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.75),
0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* Pixel Badge - 像素徽章 */
.pixel-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.65rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(241, 140, 110, 0.3);
background: rgba(241, 140, 110, 0.12);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
font-weight: 600;
}
/* Pixel Pill - 药丸按钮 */
.pixel-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 0;
border-width: 2px;
border-style: solid;
border-color: rgba(217, 119, 87, 0.45);
padding: 0.35rem 0.85rem;
background: color-mix(in srgb, hsl(var(--secondary)) 40%, transparent);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.15),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
transition: all 0.2s ease;
}
.pixel-pill:hover {
background: rgba(217, 119, 87, 0.16);
border-color: rgba(217, 119, 87, 0.65);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.2),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.dark .pixel-pill {
border-color: rgba(241, 140, 110, 0.55);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.dark .pixel-pill:hover {
background: rgba(241, 140, 110, 0.22);
border-color: rgba(241, 140, 110, 0.75);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
/* Pixel Text - 像素文字效果 */
.pixel-text {
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25),
-1px -1px 0 rgba(255, 241, 232, 0.18);
}
.dark .pixel-text {
text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55),
-1px -1px 0 rgba(255, 241, 232, 0.08);
}
/* Grid Pattern - 网格图案 */
.grid-pattern {
position: relative;
}
.grid-pattern::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.05) 1px, transparent 1px);
background-size: 32px 32px;
opacity: 0.6;
pointer-events: none;
z-index: -1;
}
.dark .grid-pattern::after {
background-image:
linear-gradient(to right, rgba(241, 140, 110, 0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(241, 140, 110, 0.08) 1px, transparent 1px);
opacity: 0.4;
}
/* Scanlines - 扫描线效果 */
.scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.05) 0,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px,
transparent 6px
);
pointer-events: none;
}
.dark .scanlines {
background-image: repeating-linear-gradient(
-45deg,
rgba(255, 255, 255, 0.03) 0,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 6px
);
}
/* Remove rounded corners from common elements */
button:not(.short-cuts-template *, [class*="rounded-full"]),
input,
textarea,
select,
.card,
.badge {
border-radius: 0 !important;
}
/* Keep rounded corners for shortcut buttons and other rounded-full elements */
.short-cuts-template div[class*="rounded-full"],
div[class*="rounded-full"] {
border-radius: 9999px !important;
}

View File

@@ -8,6 +8,7 @@ const config: Config = {
extend: {
fontFamily: {
sans: [
'OPlusSans3-Medium',
'haipaiqiangdiaosenxiyuan',
...defaultTheme.fontFamily.sans
],
@@ -19,6 +20,18 @@ const config: Config = {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
brand: {
50: 'var(--brand-50)',
100: 'var(--brand-100)',
200: 'var(--brand-200)',
300: 'var(--brand-300)',
400: 'var(--brand-400)',
500: 'var(--brand-500)',
600: 'var(--brand-600)',
700: 'var(--brand-700)',
800: 'var(--brand-800)',
900: 'var(--brand-900)'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
@@ -63,9 +76,10 @@ const config: Config = {
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
lg: '0',
md: '0',
sm: '0',
DEFAULT: '0'
},
animation: {
orbit: 'orbit calc(var(--duration)*1s) linear infinite',