feat: 改进页面切换器样式和响应式布局

This commit is contained in:
2026-05-24 00:03:56 +08:00
parent cb8c9c6764
commit 88da4ca96e
7 changed files with 3779 additions and 4784 deletions

96
AGENTS.md Normal file
View File

@@ -0,0 +1,96 @@
# AGENTS.md
本文件用于指导 AI Coding Agents 在本仓库内高效、安全、一致地工作。优先遵循现有实现与仓库约定;除非必要,不要引入新架构。
## 项目概览
- 这是一个 Nuxt.js + TypeScript + Tailwind CSS + Bun 项目。
- 默认使用 Bun 作为包管理器与运行时。
- 优先采用 SSR 与 Nuxt conventions。
- 优先使用 Composition API。
- TypeScript 必须保持 strict mode。
## 环境与工具链
- 优先通过 `nix develop``nix shell` 补全缺失工具,不要假设系统已全局安装 node、npm、pnpm。
- 如果缺少开发工具,优先补充 `flake.nix`,或补充 `shell.nix` / devShell。
- 默认使用 Bun
- 安装依赖:`bun install`
- 运行脚本:`bun run <script>`
- 避免使用 npm、pnpm、yarn
- 优先保证开发环境可复现,避免依赖本机隐式状态。
- 常用命令:
```bash
nix develop
bun install
bun run dev
bun run build
bun run lint
```
## DevOps
- Docker 已支持,构建入口见 [Dockerfile](Dockerfile),本地联动服务见 [docker-compose.yml](docker-compose.yml)。默认使用 `docker compose up --build` 启动整套环境。
- Nix flake 已支持,开发环境以 [flake.nix](flake.nix) 为准;优先通过 `nix develop` 进入可复现 shell再执行 Bun 命令。
- GitHub Actions 已配置 CI检查顺序与本地一致安装依赖、`bun run lint``bun run typecheck``bun run build`
- 自动格式化使用 `bun run format`,修改代码后优先执行,保证 Prettier 与 Tailwind 排版一致。
- 自动 lint 与自动 typecheck 依赖仓库脚本和 CI本地修改后优先跑 `bun run lint``bun run typecheck`,避免把问题留到流水线。
## TypeScript 规范
- 参考规范:[TypeScript 风格指南](https://siiway.org/zh/dev/ts-style.html)
- 默认使用 `const`,禁止 `var`
- 避免 `any`;必要时先收窄类型,再使用显式断言。
- 优先使用 `type`,仅在需要扩展或声明合并时使用 `interface`
- 不要使用 `enum`,优先使用 literal union。
- 避免大型类,优先函数式与组合式设计。
- 公共 API 必须显式声明返回类型。
- 文件名使用 kebab-case。
- composables 使用 `useXxx` 命名。
- Vue 组件使用 PascalCase 命名。
- 避免默认导出Nuxt 特殊约定除外。
- 保持 import 顺序稳定,依赖 ESLint + Prettier 自动格式化。
## Nuxt / Vue 约定
- composables 放在 `/composables`
- server routes 放在 `/server/api`
- shared types 放在 `/types`
- 通用工具函数放在 `/utils`
- 页面逻辑保持轻量,复杂业务下沉到 composables / server / utils。
- 避免在 components 中写复杂业务逻辑。
- 优先使用 `useFetch``useAsyncData` 与 Nuxt auto imports。
- Tailwind class 要保持可读性,避免过长 class chain。
- 优先使用语义化 wrapper components而不是把样式逻辑散落在页面里。
## AI Agent 行为规则
- 修改代码前先阅读现有实现与相邻调用点。
- 修改前优先搜索已有 utility / composable / server helper避免重复造轮子。
- 优先遵循现有代码风格,不要强行引入新的架构风格。
- 最小化修改范围,优先做局部且可验证的改动。
- 不要随意增加依赖;新增依赖必须说明原因。
- 不要破坏 SSR、hydration 或 Nuxt 自动导入约定。
- 不要绕过 TypeScript 类型系统。
- 不要通过关闭 lint、typecheck 或 build 来“修复”问题。
## 测试与质量
- 所有改动应尽量通过 typecheck、lint、build。
- 修改后优先运行:
```bash
bun run lint
bun run typecheck
bun run build
```
- 如果某个检查失败,先修复根因,再继续扩大修改。
## 维护原则
- 保持本文件短小、可执行、面向 AI Agent。
- 只记录仓库中不容易通过扫描直接发现、但会影响正确性的约定。
- 细节说明优先链接到其他文档,不在此处重复展开。

View File

@@ -1,44 +1,47 @@
<template> <template>
<div <header
class="my-4 mx-auto max-w-3xl w-full px-4 py-3 grid grid-cols-[auto_1fr_auto] gap-3 items-center" class="page-switcher fixed left-1/2 z-50 mx-auto grid w-fit max-w-[calc(100%-2rem)] -translate-x-1/2 grid-cols-1 items-center justify-items-center rounded-2xl border border-white/10 bg-linear-to-br from-white/8 to-white/3 px-4 py-2 shadow-md-dark backdrop-blur-xl transition-[top,box-shadow,transform,background-color] duration-200 ease-out"
:style="headerStyle"
> >
<button <nav class="flex flex-nowrap gap-2 justify-center" aria-label="主导航">
:disabled="currentIndex <= 0"
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
@click="goPrev"
>
上一页
</button>
<div class="flex gap-2 flex-wrap justify-center">
<button <button
v-for="item in pages" v-for="item in pages"
:key="item.name" :key="item.name"
:class="{ :class="{
'bg-primary/30 border-primary/60 text-primary shadow-lg shadow-primary/25': 'border-primary/40 bg-primary/10 text-primary shadow-lg shadow-primary/15':
item.name === route.name, item.name === route.name,
}" }"
class="px-2.5 py-2 bg-white/10 text-text-primary border border-white/15 rounded-2xl cursor-pointer transition-all duration-200 hover:bg-white/15 hover:border-primary/40" class="rounded-2xl border border-white/10 bg-linear-to-br from-white/5 to-white/2 px-2.5 py-1.5 cursor-pointer text-text-primary shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark hover:bg-linear-to-br hover:from-primary/6"
@click="router.push({ name: item.name })" @click="router.push({ name: item.name })"
> >
{{ item.label }} {{ item.label }}
</button> </button>
</div> </nav>
<button </header>
:disabled="currentIndex >= pages.length - 1"
class="bg-white/10 text-text-primary border border-white/15 rounded-2xl px-3 py-2 cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
@click="goNext"
>
下一页
</button>
</div>
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const scrollY = ref(0);
const updateScrollY = () => {
scrollY.value = window.scrollY || window.pageYOffset || 0;
};
onMounted(() => {
updateScrollY();
window.addEventListener("scroll", updateScrollY, { passive: true });
window.addEventListener("resize", updateScrollY, { passive: true });
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", updateScrollY);
window.removeEventListener("resize", updateScrollY);
});
const pages = [ const pages = [
{ name: "index", label: "首页" }, { name: "index", label: "首页" },
@@ -49,17 +52,15 @@ const pages = [
// { name: "comments", label: "留言" }, // { name: "comments", label: "留言" },
]; ];
const currentIndex = computed(() => pages.findIndex((item) => item.name === route.name)); const headerStyle = computed(() => {
const maxOffset = 16;
const minOffset = 4;
const settleDistance = 320;
const progress = Math.min(scrollY.value / settleDistance, 1);
const top = maxOffset - (maxOffset - minOffset) * progress;
const goPrev = () => { return {
if (currentIndex.value > 0) { top: `${top}px`,
router.push({ name: pages[currentIndex.value - 1].name });
}
};
const goNext = () => {
if (currentIndex.value < pages.length - 1) {
router.push({ name: pages[currentIndex.value + 1].name });
}
}; };
});
</script> </script>

View File

@@ -82,6 +82,34 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
padding-top: 5.75rem;
}
@media (max-width: 768px) {
.content-stack {
padding-top: 8.75rem;
}
.page-switcher {
width: calc(100% - 1rem);
gap: 0.5rem;
padding: 0.5rem 0.65rem;
border-radius: 1rem;
grid-template-columns: minmax(0, 1fr);
top: 0.5rem;
}
.page-switcher nav {
min-width: 0;
flex-wrap: nowrap;
justify-content: flex-start;
overflow-x: auto;
scrollbar-width: none;
}
.page-switcher nav::-webkit-scrollbar {
display: none;
}
} }
.app-body { .app-body {

3364
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/fa6-brands": "^1.2.6", "@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/fa6-solid": "^1.2.4",
"@iconify-json/simple-icons": "^1.2.66", "@iconify-json/simple-icons": "^1.2.66",
"@nuxt/eslint": "1.12.1", "@nuxt/eslint": "1.12.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",

4998
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,9 @@
allowBuilds:
'@parcel/watcher': true
core-js: true
esbuild: true
sharp: true
unrs-resolver: true
vue-demi: true
ignoredBuiltDependencies: ignoredBuiltDependencies:
- core-js - core-js