This commit is contained in:
Ricocc
2025-11-06 23:01:29 +08:00
parent d24e03ce6a
commit 1434ce3517
28 changed files with 236 additions and 157 deletions

111
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,111 @@
# 贡献指南
感谢你对这个项目的关注!我们欢迎所有形式的贡献。
## 如何贡献
### 报告 Bug
如果你发现了 bug
1. 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues) 确认该 bug 尚未被报告
2. 如果是一个新的 bug请创建一个新的 Issue包含
- 清晰的问题描述
- 复现步骤
- 预期行为
- 实际行为
- 截图(如果有)
- 环境信息(浏览器、操作系统等)
### 提出新功能
如果你有改进建议或新功能想法:
1. 查看现有 Issues 确认没有类似提议
2. 创建一个新的 Feature Request Issue描述
- 功能描述
- 使用场景
- 可能的实现方案(可选)
### 提交代码
1. **Fork 项目**
```bash
git clone https://github.com/your-username/ricoui-portfolio.git
cd ricoui-portfolio
```
2. **创建分支**
```bash
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
3. **安装依赖**
```bash
pnpm install
```
4. **进行修改**
- 编写代码
- 确保代码通过检查:`pnpm check`
- 测试你的修改
5. **提交更改**
```bash
git add .
git commit -m "feat: 添加新功能描述"
# 或
git commit -m "fix: 修复bug描述"
```
提交信息请遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
- `feat:` 新功能
- `fix:` Bug 修复
- `docs:` 文档更新
- `style:` 代码格式调整(不影响功能)
- `refactor:` 代码重构
- `perf:` 性能优化
- `test:` 测试相关
- `chore:` 构建/工具相关
6. **推送并创建 Pull Request**
```bash
git push origin feature/your-feature-name
```
然后在 GitHub 上创建 Pull Request。
## 代码规范
- 使用 Biome 进行代码检查和格式化
- 运行 `pnpm check` 确保代码符合规范
- 保持代码简洁、可读
- 添加必要的注释
## 开发环境
- Node.js >= 18
- pnpm (推荐) 或 npm/yarn
- 现代浏览器Chrome、Firefox、Safari、Edge
## 测试
在提交 PR 前,请确保:
- [ ] 代码通过 `pnpm check`
- [ ] 在本地运行 `pnpm dev` 测试正常
- [ ] 构建通过 `pnpm build`
- [ ] 测试了不同的浏览器和设备
## 问题?
如果遇到问题,可以:
- 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues)
- 创建新的 Issue 提问
- 联系维护者hello@ricoui.com
再次感谢你的贡献!🎉

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -18,11 +18,11 @@
}, },
{ {
"name": "Luon Models", "name": "Luon Models",
"description": "Company Website", "description": "Agency Company Website",
"tags": ["Company","Website","Branding"], "tags": ["Agency","Website"],
"image": "/assets/works/luonmodels.jpg", "image": "/assets/works/luonmodels.jpg",
"video": "/assets/works/luonmodels.mp4", "video": "/assets/works/luonmodels.mp4",
"url": "https://luonmodels.netlify.app/" "url": "/work/luonmodels"
}, },
{ {
"name": "Ricoui", "name": "Ricoui",

View File

@@ -79,9 +79,9 @@ const {title, description
<h3>Writing</h3> <h3>Writing</h3>
<p>Style guides, design notes, and quick reads.</p> <p>Style guides, design notes, and quick reads.</p>
<div class="mt-2 btn opacity-0 transition duration-300 ease-in-out"> <div class="mt-2 btn opacity-0 transition duration-300 ease-in-out">
<Button url="/blog" size="sm"> <!-- <Button url="/blog" size="sm">
Blog Blog
</Button> </Button> -->
</div> </div>
</div> </div>
<div class="explore-figure computer absolute right-[5%] sm:right-[-10%] md:right-[-7%] bottom-[14%] w-[120px] sm:w-[130px] md:w-[148px] h-auto"> <div class="explore-figure computer absolute right-[5%] sm:right-[-10%] md:right-[-7%] bottom-[14%] w-[120px] sm:w-[130px] md:w-[148px] h-auto">

View File

@@ -46,7 +46,7 @@ const currentSizeClasses = sizeClasses[size];
target={type === "disabled" ? undefined : target} target={type === "disabled" ? undefined : target}
class={`${ class={`${
type === "solid" type === "solid"
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-[#f3f5ff] dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]` ? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-primary/20 dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]`
: type === "fill" : type === "fill"
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-btn-primary dark:bg-btn-primary-dark border-[0] border-[rgba(138,127,255,0.2)] shadow-[0px_2px_3px_0_rgba(0,0,0,0.1)] max-w-60 transition duration-400 ease-in-out hover:bg-btn-primary-hover dark:hover:bg-btn-primary-dark-hover hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:shadow-[0px_6px_9px_0_rgba(0,0,0,0.2)]` ? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-btn-primary dark:bg-btn-primary-dark border-[0] border-[rgba(138,127,255,0.2)] shadow-[0px_2px_3px_0_rgba(0,0,0,0.1)] max-w-60 transition duration-400 ease-in-out hover:bg-btn-primary-hover dark:hover:bg-btn-primary-dark-hover hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:shadow-[0px_6px_9px_0_rgba(0,0,0,0.2)]`
: type === "disabled" : type === "disabled"
@@ -58,11 +58,11 @@ const currentSizeClasses = sizeClasses[size];
onclick={type === "disabled" ? "return false;" : undefined} onclick={type === "disabled" ? "return false;" : undefined}
> >
{type === "solid" ? ( {type === "solid" ? (
<div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-[#6f6c8f] dark:text-[#c9d7f2]`}> <div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-neutral-700 dark:text-neutral-300`}>
<slot /> <slot />
</div> </div>
) : type === "fill" ? ( ) : type === "fill" ? (
<span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-white`}> <span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-neutral-50`}>
<slot /> <slot />
</span> </span>
) : type === "disabled" ? ( ) : type === "disabled" ? (

View File

@@ -5,15 +5,3 @@
<img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full" /> <img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full" />
</a> </a>
{/* <a
href="/"
class="h-5 text-base group relative z-30 flex items-center space-x-1.5 text-black dark:text-white font-semibold"
>
<span
class="text-xl -translate-y-0.5 group-hover:-rotate-12 group-hover:scale-[1.2] ease-in-out duration-300"
>✦</span
>
<!-- Logo Text -->
<span class="-translate-y-0.5"> aria</span>
</a> */}

View File

@@ -1,91 +0,0 @@
---
interface Props {
class?: string; // Class of the marquee
marqueeElements: number; // Number of elements
marqueeElementWidth: string; // Width of the marquee
marqueeElementWidthAuto?: boolean; // Is the width auto
marqueeElementWidthResponsive: string; // Width of the marquee
marqueePauseOnHover?: boolean; // Duration of the marquee
marqueeReverse?: "reverse" | "" | undefined; // Duration of the marquee
marqueeDuration?: string; // Duration of the marquee
}
let {
marqueeElements,
marqueeElementWidth,
marqueeElementWidthAuto = true,
marqueeElementWidthResponsive,
marqueePauseOnHover = false,
marqueeDuration = "50s",
marqueeReverse = "",
} = Astro.props;
// Remove value of if `marqueeElementWidth`, `marqueeElementWidthResponsive` marqueeElementWidthAuto true
if (marqueeElementWidthAuto) {
marqueeElementWidth = "";
marqueeElementWidthResponsive = "";
}
---
<div
class:list={[
"marquee",
{
"marquee-revers": marqueeReverse,
"marquee-pause-on-hover": marqueePauseOnHover,
},
]}
style={`--marquee-elements:${marqueeElements};--marquee-element-width-responsive:calc(${marqueeElementWidthResponsive} / var(--marquee-elements) * ${marqueeElements});--marquee-element-width:calc(${marqueeElementWidth} / var(--marquee-elements) * ${marqueeElements});${"--marquee-duration:" + marqueeDuration};--marquee-reverse:${marqueeReverse};`}>
<div
class:list={[
"marquee-content flex items-center",
{ "marquee-element-width-auto": marqueeElementWidthAuto },
Astro.props.class,
]}>
<slot />
</div>
</div>
<script>
function updateMarquee() {
const marqueeContents = document.querySelectorAll(
".marquee-element-width-auto",
) as NodeListOf<HTMLElement>;
marqueeContents.forEach((marqueeContent) => {
const images = marqueeContent.querySelectorAll("img");
images.forEach((img) => {
if (!img.complete) {
// wait for images to load
img.addEventListener("load", () => updateImageSize(img));
} else {
updateImageSize(img);
}
});
// update total width after images resized
const totalWidth = marqueeContent.scrollWidth;
marqueeContent.style.setProperty("--total-width", `${totalWidth / 2}px`);
});
}
function updateImageSize(img: HTMLImageElement) {
const computed = getComputedStyle(img);
const height = parseFloat(computed.height); // Tailwind-applied height
const aspectRatio = img.naturalWidth / img.naturalHeight;
const width = height * aspectRatio;
img.setAttribute("height", height.toString());
img.setAttribute("width", width.toString());
}
// Debounced resize handler
let resizeTimeout: number;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(updateMarquee, 100);
});
// Initial run
updateMarquee();
</script>

View File

@@ -12,10 +12,10 @@
</div> </div>
<script> <script>
import Matter from "matter-js"; // 修改为默认导入 import Matter from "matter-js";
(() => { (() => {
// 懒加载:当 .tricks-view 进入视口时初始化
const target = document.querySelector(".tricks-view"); const target = document.querySelector(".tricks-view");
if (!target) { if (!target) {
console.error("[Tricks] .tricks-view not found"); console.error("[Tricks] .tricks-view not found");
@@ -39,7 +39,7 @@ import Matter from "matter-js"; // 修改为默认导入
async function initTricksAnimation() { async function initTricksAnimation() {
const container = document.querySelector(".tool-stack-box") || document.querySelector(".matter-wrapper"); const container = document.querySelector(".tool-stack-box") || document.querySelector(".matter-wrapper");
const canvasHost = document.querySelector(".tricks-canvas") as HTMLElement; const canvasHost = document.querySelector(".tricks-canvas") as HTMLElement;
const domLayer = document.querySelector(".tricks-elements") as HTMLElement; // 添加类型断言 const domLayer = document.querySelector(".tricks-elements") as HTMLElement;
if (!container || !canvasHost || !domLayer) { if (!container || !canvasHost || !domLayer) {
console.error( console.error(
@@ -48,36 +48,36 @@ import Matter from "matter-js"; // 修改为默认导入
return; return;
} }
// 配置(调大元素尺寸 & 缩小边缘间隙) // Config (enlarge element size & reduce edge gaps)
const pad = 2; // 内边距,越小越贴边 const pad = 2; // Inner padding; smaller value sticks closer to edges
const margin = 1; // 静态边界与容器边缘距离,越小越贴边 const margin = 1; // Distance between static boundaries and container edges; smaller value sticks closer
const count = 15; // 元素数量 const count = 15; // Number of elements
const noRotate = false; // 不旋转可设为 true const noRotate = false; // Set to true to disable rotation
const scaleFactor = 0.75; // 全局放大因子:>1 放大,<1 缩小 const scaleFactor = 0.75; // Global scaling factor: >1 enlarge, <1 shrink
// 尺寸 // Dimensions
let { width: cw, height: ch } = container.getBoundingClientRect(); let { width: cw, height: ch } = container.getBoundingClientRect();
let W = Math.max(cw - pad, 100); let W = Math.max(cw - pad, 100);
let H = Math.max(ch - pad, 100); let H = Math.max(ch - pad, 100);
// 物理引擎 // Physics engine
const engine = Matter.Engine.create(); const engine = Matter.Engine.create();
const world = engine.world; const world = engine.world;
// 鼠标拖拽(事件落在 canvasHost 上) // Mouse drag (events fall on canvasHost)
const mouseConstraint = Matter.MouseConstraint.create(engine, { const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: Matter.Mouse.create(canvasHost), mouse: Matter.Mouse.create(canvasHost),
constraint: { render: { visible: false }, stiffness: 1 }, constraint: { render: { visible: false }, stiffness: 1 },
}); });
Matter.World.add(world, mouseConstraint); Matter.World.add(world, mouseConstraint);
// 边界(地面与左右墙) // Boundaries (ground and side walls)
let ground = Matter.Bodies.rectangle(W / 2, H - margin, W, 14, { isStatic: true }); let ground = Matter.Bodies.rectangle(W / 2, H - margin, W, 14, { isStatic: true });
let wallL = Matter.Bodies.rectangle(margin, H / 2, 14, H, { isStatic: true }); let wallL = Matter.Bodies.rectangle(margin, H / 2, 14, H, { isStatic: true });
let wallR = Matter.Bodies.rectangle(W - margin, H / 2, 14, H, { isStatic: true }); let wallR = Matter.Bodies.rectangle(W - margin, H / 2, 14, H, { isStatic: true });
Matter.World.add(world, [ground, wallL, wallR]); Matter.World.add(world, [ground, wallL, wallR]);
// 图标路径(按需调整) // Icon paths (adjust as needed)
const iconUrls = [ const iconUrls = [
"/assets/stack/astro.png", "/assets/stack/astro.png",
"/assets/stack/css.png", "/assets/stack/css.png",
@@ -95,12 +95,12 @@ import Matter from "matter-js"; // 修改为默认导入
"/assets/stack/vscode.png", "/assets/stack/vscode.png",
]; ];
// 预加载图片,获取 naturalWidth/Height // Preload images to obtain naturalWidth/Height
function loadImage(url: string) { function loadImage(url: string) {
return new Promise<HTMLImageElement>((resolve, reject) => { return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image(); const img = new Image();
img.decoding = "async"; img.decoding = "async";
img.loading = "eager"; // 懒加载触发后尽快加载 img.loading = "eager"; // Load as soon as lazy-load is triggered
img.src = url; img.src = url;
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image load failed: " + url)); img.onerror = () => reject(new Error("Image load failed: " + url));
@@ -114,10 +114,10 @@ import Matter from "matter-js"; // 修改为默认导入
console.error("[Tricks] image load failed:", e); console.error("[Tricks] image load failed:", e);
} }
// 尺寸策略:显著增大目标面积区间(适配 720x480 // Sizing strategy: significantly increase target area range (adapted for 720x480)
function computeAreaRange() { function computeAreaRange() {
// 以容器面积为基准,提高图标面积比例 // Use container area as the base; increase icon area proportion
// 720*480 ≈ 345600;这里让单个图标面积大致在 1/20 ~ 1/10 容器面积之间 // 720*480 ≈ 345600; here a single icon area is roughly between 1/20 ~ 1/10 of the container area
const baseArea = W * H; const baseArea = W * H;
const areaMin = clamp(baseArea / 20, 12000, 30000); const areaMin = clamp(baseArea / 20, 12000, 30000);
const areaMax = clamp(baseArea / 10, 20000, 50000); const areaMax = clamp(baseArea / 10, 20000, 50000);
@@ -125,7 +125,7 @@ import Matter from "matter-js"; // 修改为默认导入
} }
let { areaMin, areaMax } = computeAreaRange(); let { areaMin, areaMax } = computeAreaRange();
// 图标刚体类(矩形,尺寸基于 naturalWidth/Height // Icon rigid body class (rectangle, size based on naturalWidth/Height)
class IconBody { class IconBody {
w: number; w: number;
h: number; h: number;
@@ -141,29 +141,29 @@ import Matter from "matter-js"; // 修改为默认导入
? img.naturalWidth / img.naturalHeight ? img.naturalWidth / img.naturalHeight
: 1; : 1;
// 目标面积(随机落在区间内) // Target area (random within range)
const A = randRange(areaMin, areaMax); const A = randRange(areaMin, areaMax);
// 推导宽高:w = sqrt(A*r), h = w/r // Derive width/height: w = sqrt(A*r), h = w/r
let w = Math.sqrt(A * r); let w = Math.sqrt(A * r);
let h = w / r; let h = w / r;
// 全局放大或缩小 // Global scale up or down
w *= scaleFactor; w *= scaleFactor;
h *= scaleFactor; h *= scaleFactor;
this.w = w; this.w = w;
this.h = h; this.h = h;
// 物理矩形刚体 // Physics rectangle body
this.body = Matter.Bodies.rectangle(x, y, this.w, this.h, { this.body = Matter.Bodies.rectangle(x, y, this.w, this.h, {
restitution: 0.35, restitution: 0.35,
friction: 0.1, friction: 0.1,
frictionAir: 0.02, frictionAir: 0.02,
density: clamp((this.w * this.h) / 40000, 0.001, 0.02), density: clamp((this.w * this.h) / 40000, 0.001, 0.02),
inertia: noRotate ? Infinity : undefined, // 不旋转:设置惯性无穷 inertia: noRotate ? Infinity : undefined, // No rotation: set infinite inertia
}); });
// DOM:容器 + img // DOM: container + img
this.el = document.createElement("div"); this.el = document.createElement("div");
this.el.className = "tricks-circle"; this.el.className = "tricks-circle";
this.el.style.width = `${this.w}px`; this.el.style.width = `${this.w}px`;
@@ -172,11 +172,11 @@ import Matter from "matter-js"; // 修改为默认导入
const node = img.cloneNode(true) as HTMLImageElement; const node = img.cloneNode(true) as HTMLImageElement;
node.style.width = "100%"; node.style.width = "100%";
node.style.height = "100%"; node.style.height = "100%";
node.style.objectFit = "contain"; // "cover" node.style.objectFit = "contain"; // or "cover"
node.alt = node.alt || "icon"; node.alt = node.alt || "icon";
this.el.appendChild(node); this.el.appendChild(node);
domLayer.appendChild(this.el); // 这里 domLayer 已经确保不为 null domLayer.appendChild(this.el); // domLayer is ensured to be non-null here
} }
update() { update() {
@@ -190,7 +190,7 @@ import Matter from "matter-js"; // 修改为默认导入
} }
} }
// 创建图标刚体 // Create icon rigid bodies
const total = Math.min(iconsImgs.length, count); const total = Math.min(iconsImgs.length, count);
const iconsBodies: IconBody[] = []; const iconsBodies: IconBody[] = [];
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
@@ -198,16 +198,16 @@ import Matter from "matter-js"; // 修改为默认导入
} }
Matter.World.add(world, iconsBodies.map((it) => it.body)); Matter.World.add(world, iconsBodies.map((it) => it.body));
// 启动引擎 // Start engine
const runner = Matter.Runner.create(); const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine); Matter.Runner.run(runner, engine);
// 每次物理步进后,同步 DOM // After each physics step, sync DOM
Matter.Events.on(engine, "afterUpdate", () => { Matter.Events.on(engine, "afterUpdate", () => {
iconsBodies.forEach((it) => it.update()); iconsBodies.forEach((it) => it.update());
}); });
// 自适应尺寸:更新边界位置,并重新计算面积区间 // Responsive sizing: update boundary positions and recompute area range
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
W = Math.max(rect.width - pad, 100); W = Math.max(rect.width - pad, 100);
@@ -223,7 +223,7 @@ import Matter from "matter-js"; // 修改为默认导入
}); });
} }
// 工具函数IIFE 外也可用) // Utility functions (also usable outside the IIFE)
function clamp(v: number, min: number, max: number) { function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v)); return Math.max(min, Math.min(max, v));
} }
@@ -319,4 +319,4 @@ import Matter from "matter-js"; // 修改为默认导入
object-fit: contain; object-fit: contain;
} }
</style> </style>

View File

@@ -36,9 +36,9 @@ if (typeof logo === 'string') {
data-aos-duration="500" data-aos-duration="500"
data-aos-once="true" data-aos-once="true"
> >
<div class="flex items-center gap-2 sm:gap-3 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]"> <div class="flex items-center gap-1 sm:gap-2 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]">
<!-- Logo block --> <!-- Logo block -->
<div class="shrink-0 h-10 w-10 sm:h-11 sm:w-11 rounded-xl bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden"> <div class="shrink-0 h-10 w-10 sm:h-10 sm:w-10 rounded-full bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden">
{logoSrc ? ( {logoSrc ? (
<img src={logoSrc} alt="logo" class="h-full w-full object-cover" loading="lazy" /> <img src={logoSrc} alt="logo" class="h-full w-full object-cover" loading="lazy" />
) : ( ) : (
@@ -47,7 +47,7 @@ if (typeof logo === 'string') {
</div> </div>
<!-- Middle pills (tags + optional github) --> <!-- Middle pills (tags + optional github) -->
<div class="flex items-center gap-2sm:gap-3 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1"> <div class="flex items-center gap-1 sm:gap-1 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1">
{tags && tags.map((t) => ( {tags && tags.map((t) => (
<span class=" hidden font-light sm:inline-flex items-center h-10 px-3 sm:px-3 rounded-xl bg-neutral-800/80 ring-1 ring-white/10 text-neutral-300 text-sm whitespace-nowrap"> <span class=" hidden font-light sm:inline-flex items-center h-10 px-3 sm:px-3 rounded-xl bg-neutral-800/80 ring-1 ring-white/10 text-neutral-300 text-sm whitespace-nowrap">
{t} {t}
@@ -72,7 +72,7 @@ if (typeof logo === 'string') {
href={url} href={url}
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener"
class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 font-medium hover:bg-yellow-200 transition-colors whitespace-nowrap" class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 text-sm hover:bg-yellow-200 transition-colors whitespace-nowrap"
> >
{visitLabel} {visitLabel}
</a> </a>

View File

@@ -3,18 +3,19 @@
const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://portfolio.ricoui.com/'; const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://portfolio.ricoui.com/';
export const siteConfig = { export const siteConfig = {
title: "Rico Portfolio", title: "Ricoui Portfolio",
author: "Ricoui", author: "Ricoui",
url: SITE_URL, url: SITE_URL,
mail: "hello@ricoui.com", mail: "hello@ricoui.com",
resume: "#", // resume add your resume file path here: /assets/resume.pdf
resume: "https://ricoui.com/",
utm: { utm: {
source: `${SITE_URL}`, source: `${SITE_URL}`,
medium: "referral", medium: "referral",
campaign: "navigation", campaign: "navigation",
}, },
meta:{ meta:{
title: "Rico Portfolio", title: "Ricoui Portfolio",
description: "I'm Rico, a web designer passionate about both design and code. Currently developing a personal product for the design community.", description: "I'm Rico, a web designer passionate about both design and code. Currently developing a personal product for the design community.",
keywords: "web designer, portfolio, design, code, personal website", keywords: "web designer, portfolio, design, code, personal website",
image: `${SITE_URL}/og.jpg`, image: `${SITE_URL}/og.jpg`,

View File

@@ -329,7 +329,7 @@ const filtered = headings.filter(h => h.depth <= 3);
data-aos-once="true" data-aos-once="true"
>Let's Connect</h2> >Let's Connect</h2>
<p <p
class="text-sm leading-6 text-neutral-600 dark:text-neutral-300" class="text-sm leading-6 text-neutral-700 dark:text-neutral-300"
data-aos="fade-up-xs" data-aos="fade-up-xs"
data-aos-delay="100" data-aos-delay="100"
data-aos-duration="500" data-aos-duration="500"
@@ -339,7 +339,7 @@ const filtered = headings.filter(h => h.depth <= 3);
href={siteConfig.social.twitter} href={siteConfig.social.twitter}
target="_blank" target="_blank"
class="text-indigo-500 underline">follow me on twitter</a class="text-indigo-500 underline">follow me on twitter</a
>, or you can send me an <a href="mailto:hello@ricouic.om" class="text-indigo-500 underline" >, or you can send me an <a href="mailto:hello@ricoui.com" class=" underline"
>email</a >email</a
> and I'll be sure to get back to you. > and I'll be sure to get back to you.
</p> </p>

View File

@@ -4,7 +4,7 @@ import PageHeader from "@/components/elements/PageHeader.astro";
import BlogSection from "@/components/sections/BlogSection.astro"; import BlogSection from "@/components/sections/BlogSection.astro";
// 使用与分页页面相同的每页文章数量 // 使用与分页页面相同的每页文章数量
const POSTS_PER_PAGE = 3; const POSTS_PER_PAGE = 6;
// 博客首页显示第一页内容 // 博客首页显示第一页内容
const currentPage = 1; const currentPage = 1;
--- ---

View File

@@ -10,8 +10,20 @@ export const POSTS_PER_PAGE = 6;
// 生成分页路径 // 生成分页路径
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await getCollection("post"); const allPosts = await getCollection("post");
// 使用顶层定义的POSTS_PER_PAGE常量
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); // 按发布日期排序文章
let posts = allPosts.sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);
// 查找特色文章,在列表页需要从主列表中移除
const featuredPost = posts.find((post) => post.data.featured);
if (featuredPost) {
posts = posts.filter((post) => post.data.featured !== true);
}
// 使用移除特色文章后的文章数量计算总页数
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => { return Array.from({ length: totalPages }, (_, i) => {
const page = i + 1; const page = i + 1;

View File

@@ -0,0 +1,58 @@
---
import { Image } from "astro:assets";
import PostLayout from "@/layouts/PostLayout.astro";
import PageHeader from "@/components/elements/PageHeader.astro";
import ActionBar from "@/components/widgets/ActionBar.astro";
import SeparatorLine from "@/components/elements/SeparatorLine.astro";
//自动导入 Image 组件和图像
const allImages = await Astro.glob('../../assets/work/luonmodels/*.{jpg,png,webp}');
const logoUrl = "https://luonmodels.vercel.app/favicon.png";
---
<PostLayout
title="LUON - Agency Website Template"
description = 'LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios.',
keywords = 'LUONModels, Agency Website Template, Astro, Modeling Agency, Photographer, Studio',
>
<div class="work article">
<main class="work-wrapper">
<PageHeader
title="LUON - Agency Website Template"
tags={["Designer","Portfolio","Open Source"]}
className="mb-6 md:mb-8"
>
</PageHeader>
</section>
<div class="site-container m-auto">
<SeparatorLine />
</div>
<section class="article-content">
<div class="site-container mx-auto my-8 md:my-16 ">
<div>
<p>
LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios. It aims to showcase people, photography, and information clearly and effectively, while also making it convenient to build event pages, content blogs, and various other specialized pages.
</p>
<p>Features</p>
<ul>
<li>✅ Responsive Design: Fully optimized for mobile and desktop viewing, ensuring a consistent experience across devices.</li>
<li>✅ Built with Astro.js: Leverages the powerful features of Astro.js for optimized performance and easy customize.</li>
<li>✅ Intuitive User Interface: Minimalist layout promotes ease of use, allowing users to find what they need without confusion.</li>
<li>✅ Speed Optimization: Engineered for exceptional loading times, ensuring that users dont have to wait on page loads.</li>
<li>✅ Customizable Components: Flexibility to customize elements easily, aligning with your brands identity and needs.</li>
<li>✅ SEO Friendly: Built with search engine optimization in mind, helping improve visibility and rankings.</li>
</ul>
<p>🔗🔗 <a href="https://luonmodels.vercel.app/" target="_blank">Live Demo</a></p>
</div>
{allImages.map((img, index) => (
<picture class="picture mt-8 md:mt-12 mb-8 md:mb-12 rounded-2xl overflow-hidden">
<Image src={img.default} alt={`Image ${index + 1}`} class="w-full h-full object-cover" loading="lazy" decoding="async" quality={100}/>
</picture>
))}
</div>
</section>
</main>
</div>
<ActionBar logo={logoUrl} tags={["Agency", "Website"]} url="https://luonmodels.vercel.app/" github="https://ricoui.gumroad.com/l/luon" visitLabel="Visit Site" />
</PostLayout>