关于 dido-frontend-nuxt
面向 Dido Nuxt 4 前端项目的编码规范技能,供 Claude Code 在实现与维护界面时参考。内容涵盖 Vue、SCSS、组件、国际化和配置规则,并以 Dido 的 Nuxt starter kit 为实现参照。
主要功能
- 01Nuxt 前端编码约定
- 02Vue 与组件组织规则
- 03SCSS 与设计系统规范
- 04国际化和配置指南
- 05Figma 设计稿核对流程
适用场景
- 01基于 Dido starter kit 开发网站
- 02按一致规则编写 Vue 组件
- 03将 Figma 设计转为响应式界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ didomarchet/ dido-skills --skill dido-frontend-nuxt安装后在 Claude Code 中输入/dido-frontend-nuxt确认已加载。
附带文件riferimento/chiamate-al-backend.mdriferimento/skill-di-terzi.md
详细介绍
Dido 的 Nuxt 前端
Dido Nuxt 前端的实现方法。所需技能: dido-code-style,其中包含适用于所有技术栈的原则。这里只介绍前端特有的内容。
参考实现: Dido 的 Nuxt 4 脚手架(GitHub 上的 DidoMarchet/starter-kit-nuxt-webapp)。新网站从这里开始,并在这里查阅 composable、插件和配置的代码。脚手架的相关信息(已接入的内容、模块、渲染方式、SCSS 系统)见其 docs/project-architecture.md 和 docs/scss-system.md。
对于基于脚手架创建的项目,机器可检查的规则都由 lint 检查(eslint.starter.mjs、stylelint.starter.mjs):Claude Code 每写完一个文件后检查一次,每次提交前再检查一次。如果项目(其 CLAUDE.md 或文档,而非代码)中的说明与此技能不同,以项目为准,并在报告中指出差异,因为其中一个来源需要修正。
| 你准备处理的内容 | 阅读 |
|---|---|
| Dido 后端调用:登录、令牌、错误 | riferimento/chiamate-al-backend.md |
| admin(使用 Tailwind 的 SPA) | 文末的“Admin”章节 |
非 dido-* 技能(设计、GSAP、DatoCMS、Vue、FormKit……) | riferimento/skill-di-terzi.md |
原则
- 使用设计系统进行组合。 使用 utility 类控制 margin 和 padding,使用 token 设置 gap,使用行(
.row-*)控制宽度。复用已有的排版样式、按钮和卡片,不要自定义间距。 - 务实使用 token。 Token 是以编号类形式公开的 SCSS 变量,并非正式的设计 token 系统。只使用一次的值直接写在使用处,重复出现的值才作为 token。
- 规范数值。 使用整数或 5 的倍数,包括布局尺寸;字体大小使用 2 或 4 的倍数。如果项目有 Figma,数值以 Figma 为准,不要凭空设定,但要取整:不属于比例尺 token 或 5 的倍数的值,取两者中更接近的值(67 → 65,17 → 16)。此技能中的规则优先于布局,因此由此造成的与设计稿差异不算错误。
- 以 Figma 为准。 如果有 Figma,编写代码前先通过 MCP 查看(结构、数值、正确节点的截图),并测量内容框、gap 和 padding:不要凭记忆估算。
- 文本不要紧贴线条。 文本、标题或 CTA 不得紧贴可见线条(背景网格线、垂直分隔线);即使 Figma 中与线条对齐,也必须与线条之间留出最小 padding,并使用项目中的 token,通过类来应用。图片可以贴着线条。
- 降级处理,不要崩溃。 缺失的数据或配置应产生可控状态(回退值、提示消息、隐藏相应区块),绝不能导致页面报错。但页面的主要内容除外:如果 CMS 无响应,页面应返回 500;如果内容不存在,则返回 404。在 SSR 中,读取可能缺失的对象属性会导致整个页面失败,因此必须在读取处提供安全值(
config?.chiave ?? default)。
JS 和 Vue
- 使用箭头函数、完整名称,不要使用
console.log:参见 dido-code-style 中的“代码形式”。 - 始终使用
<script setup>,不要使用 Options API。Nuxt 根据路径命名组件(components/widget/media-text/index.vue对应WidgetMediaText),因此不要使用defineOptions({ name })。 - 操作 DOM、滚动和动画的副作用放在 composable(
app/composables/、useX)中,由它公开状态和操作,不包含标记。纯逻辑放在app/utils/中,并为其编写测试。查询和调用放在专用目录中,绝不要写在组件里。 - 模板中不要写静态文本:品牌名称从配置(
siteName)读取,标签使用 i18n,内容来自 CMS。
命名
- 文件和文件夹始终使用 kebab-case(
widget/media-text)。模板中的标签保留 PascalCase(<WidgetMediaText>),因为自动导入会拼接路径。 - Composable 使用
useX,工具函数使用动作名称(buildTrack、isLightHex),插件则在需要时添加环境后缀(*.client.js)。 - BEM 类名:块
.componente、__elemento、--modificatore。变体按区块或品牌命名,绝不按颜色命名(--footer,而不是--dark)。 - 只使用一套编号的排版比例(
.title-1…4、.text-1…3),绝不同时使用两套命名。 - 颜色按系列分级(
$black-1、$grey-2),不使用语义化名称。仅在需要时使用 ID(锚点、aria-*、JS),并采用 kebab-case。大写通过text-transform实现,绝不直接写在文本中。
组件
- 每个组件都放在
components/<area>/<nome>/文件夹中,包含index.vue、style.scss及其资源,并按领域分组(article/、navigation/)。 widget/中只放通用且可复用的组件:通过 prop 接收所有内容,并提供 slot。重复出现的模式(如轮播)只作为一个 widget 存在一次。特定区块的组件留在其所属区域。- 同类元素在整个网站中行为一致:重复出现的模式只实现一次,绝不让某个页面拥有行为各异的控件。所有轮播和可滚动的行都以相同方式拖动、滚动并响应方向键,无论其中包含什么内容;所有卡片、下划线、悬停效果、淡入淡出和图像切换都以相同方式动效呈现,使用相同的缓动和按下状态。组件文件只包含属于它自身的内容:重复出现的内容(行为、悬停效果、下划线、手形光标、动效值)只在负责实现它的 widget、composable、mixin 或全局类中定义一次,组件只需使用它。
.vue中的顺序:<template>、<script setup>、<style lang="scss" src="./style.scss"></style>。样式不使用 scoped。- 组件只使用自身块的类和设计系统中的类,并通过 prop、事件、slot 或 store 与其他组件通信,绝不通过查找其他组件的 DOM 来通信。样式与所属组件一同存在:移除组件后,或在没有该组件的页面上,其类也会消失。
- 永久资源放在
app/assets/img/中,使用描述性且利于 SEO 的 kebab-case 名称(<azienda>-logo.svg);占位资源单独放在一个文件夹中。在<img>中,路径放在src里;只有当 URL 以 JS 结尾时才使用import。图标使用 SVG 图片,并通过第二个文件或mask重新着色。
SCSS
- 每个
style.scss和每个 partial 都以工具箱开头,它提供 token 和 mixin,但不会输出 CSS:@use "~/assets/styles/scss/core" as *;。它也会通过additionalData引入,但写在文件顶部能让文件更易读。 core没有@forward的 token,在使用它的文件中就不存在:vars/或mixins/中的新文件必须添加到对应的_index.scss。- 使用
slamp(min, max)实现流体缩放。视口范围在每个项目中配置一次,只有使用不同范围时才显式传入。 - 字体和较大的间距始终使用流体缩放:字号阶梯中的标题、组件中字号从 40px 起的字体,以及从 80px 起的
margin、padding、gap、top和bottom。最大值取自 Figma 桌面稿,最小值取自 Figma 移动稿中的同一元素。如果没有移动稿,估算最小值(约为一半,再取整)。仅通过react("large")改变的值,改为在两个值之间使用slamp。正文当前字号和较小的间距 token 保持固定。标题的负间距使用em,这样会随字号缩小。 - 控件和卡片从移动端到桌面端一起缩放:使用少量共享的流体 token(控件高度和文字、卡片内边距和标题、CTA 高度),取值采用 Figma 中 1920px 画板上的值,并供所有组件使用。不能让某个组件仍保持桌面尺寸,而相邻组件已经缩小。
- 除非 Dido 有特别指示,否则左右边距和内边距绝不使用固定值:页面边距使用
slamp从移动端缩放到桌面端,并使用项目 token(例如$side-margin: slamp($grid-gutter, $grid-margin));Figma 桌面稿中的固定宽度列在桌面端以下要收窄。若固定使用 1920px 的值,在 1024px 时边距和侧边栏会挤占内容空间。 - 媒体查询使用
@include react("chiave")(large、<medium),绝不手写>=。悬停样式始终放在react("pointer-and-hover")内,避免在触屏设备上保持悬停状态。 - 每个可点击元素都要有悬停效果,并采用网站的过渡效果(时长使用 token,缓动使用网站设定):绝不瞬间切换,即使 Figma 中如此设计也不例外。操作文本(“关闭”、“筛选”、链接)都使用项目统一的动画下划线,即使原本就有下划线也一样(这种情况下,下划线会重新绘制);悬停时文本颜色加深一档。图标按钮使用更深的色调或背景,实心按钮使用同色系的更深色调。按下时(
:active)也应用相同状态,这是触屏上的触碰反馈:项目 mixin 同时定义悬停和按下状态,实心按钮轻微缩小(0.97),并在减少动态效果时保持静止。默认情况下:不设置-webkit-tap-highlight-color;链接和按钮设置touch-action: manipulation;按钮文本不可选中。 - 与背景融合(
mix-blend-mode)的图像绝不对自身或其容器应用动画opacity、filter或transform:浏览器为过渡创建的新图层会让图像连同白色背景一起闪烁。要让图像显现,在其上方淡入一层背景色薄纱,只为颜色设置动画。图标通过mask和currentcolor改变颜色,不使用filter。 - 标题旁的操作文本(“关闭”、“确认”)字号比标题小一级,并与标题基线对齐,避免与标题混淆。不同列中同一行的标题(名称和旁边的第一张卡片)共用基线,即使其中一个使用流体缩放也一样。
- 通过按钮执行操作后的确认信息(链接已复制)应为附着在该按钮上的工具提示,尖端指向按钮,而不是远处的提示。
- 面板、抽屉或模态框每次重新打开时都要回到初始状态:滚动到顶部,并重置之前的浏览状态(最简单的做法是每次打开时使用新的
:key重新挂载)。 - 移动端的卡片选项应以全宽垂直列表呈现,而不是横向滚动的一排:在手机上横向滚动会让人看不清自己在选什么。
- 使用类,而非 mixin:通过对比例尺执行
@each生成编号类,并放在正确的 layer 中(layout/、typo/)。mixin 仅用于响应式辅助功能和交互行为,例如链接的动画下划线。 - 使用类设置外边距和内边距,在组件样式中使用 token 设置
gap,使用.row-*设置宽度。全页背景应作为一个带有 row 的 section 实现。 - 动效统一使用网站的缓动效果,时长使用
vars/_timings.scss中的$speed-*token。
国际化
- 文本通过
$t处理,模板中绝不使用按语言判断的三元表达式。 - 词典按区域拆分:每种语言一个目录,每个区域一个文件;语言入口负责组合模块,不包含字符串。导入区域文件时需要使用别名
~~,因为使用相对路径时,@nuxtjs/i18nv10 不会加载这些文件。 - 各语言的内容必须保持一致:新增字符串时,要在所有语言中添加相同的键,并放在相同区域。单个文件应在第一个分区处拆分,而不是更晚。
- 本地化 slug 放在 i18n 配置(
pages)中。每个页面都必须在那里声明,即使两种语言的 slug 相同也不例外,否则页面不会进入站点地图。对于动态 slug,应按语言设置参数,这样 hreflang 和 canonical 才会指向正确的 slug。
配置
nuxt.config.ts只负责展开configuration/中按区域划分的配置片段,修改设置时应在对应的片段中进行。修改配置片段不会导致开发服务器重启。- 环境变量和密钥通过从
process.env读取的runtimeConfig获取,绝不写入组件中。runtimeConfig.public会进入客户端 bundle,因此其中只能包含本质上公开的值(例如只读 CMS token)。 robots和noindex取决于环境:只有生产环境会被编入索引。
数据与第三方服务
- 只能通过
useGraphql()读取 CMS,并将查询和 fragment 放在app/graphql/<cms>/中。内部使用 Nuxt 的useAsyncData和@datocms/cda-client的executeQuery。只有useGraphql()和服务端数据源会导入 CMS 客户端。REST 请求通过useApiFetch()发起。页面和组件绝不直接调用 CMS 客户端或 HTTP 客户端。 - CMS 出错不意味着页面不存在:页面应返回 500,只有内容不存在时才返回 404。
- 在页面中使用
useSeo()设置 SEO 和 hreflang。使用本地化 slug 时,必须使用generateSeoLocaleParams和setI18nParams,否则 alternate 会声明不存在的 URL。 useLenis()在整个应用中只初始化一次,放在布局或app.vue中。每个独立滚动的容器(模态框的内容区、抽屉、下拉菜单、筛选面板)都应使用自己的 Lenis 实现与页面相同的平滑滚动,并接入 GSAP 的 ticker。容器只能有一个子元素,因为 Lenis 会在第一个子元素尺寸变化时重新计算可滚动距离。Lenis 只会在自己的容器内查找data-lenis-prevent,从上方另一个已打开的模态框传来的事件不得使其滚动。- 服务于整个站点的第三方服务(分析、错误跟踪、用户同意)应使用插件。服务于单个页面或组件的第三方服务(表单、预订小组件、播放器)应使用 composable 处理效果,并使用组件处理标记。
- 第三方库只能由其对应的插件或 composable 导入;如果没有对应配置,它就不起作用。这样,只需删除这些文件、依赖项和使用位置,就能移除该第三方服务。如果它需要从站点接收数据,例如错误跟踪所需的 GraphQL 错误,应由它挂接到 hook(
graphql:error,useGraphql()会在发生错误时调用该 hook,或vue:error)。 - 嵌入内容应采用设计系统的样式:不使用服务自带的 CSS,而是使用自己的按钮类。如果服务通过
<iframe>渲染,则无法为其设置样式,因此在承诺支持之前必须先确认。 - 在说明第三方服务的功能之前,应测量它会联系哪些域名、写入哪些 cookie。
- 服务端动态数据源(例如站点地图)应在服务端使用自己的客户端调用服务,不使用
useGraphql();按服务要求分页,并确保失败时不会导致构建失败。包含详情页的内容模型应通过查询每种语言来加入站点地图。 - CMS 架构只能通过经过版本控制并在沙箱环境中测试的 migration 修改:绝不手动修改,沙箱环境也不例外。
动效
- 动画使用 GSAP:进入和退出、模态框、底部面板和抽屉、下拉菜单、标签页、滚动时的显现、序列和微交互。只有按钮和链接的悬停与按压效果(颜色、背景、下划线、按压时的缩放)保留在 CSS 中。
- 按组件制作动画。重复出现的动效只定义一次,放在 composable(
useXMotion)或组件中,其他组件复用它,而不是各自编写时间轴。模态框是一个统一组件,使用 GSAP 实现进入和退出,供底部面板、抽屉和搜索使用。两个内容之间的淡入淡出由一个统一包装器处理:一个使用 GSAP 钩子的<Transition :css="false">。 - 组件的动画在
gsap.context()内创建,并以组件元素为作用域;组件卸载时通过revert()清理。动画只在客户端运行,从onMounted开始。 - 打开和关闭使用同一条时间轴,通过
play()和reverse()控制,键盘触发关闭(Esc)时也一样:如果在打开到一半时关闭,就从当前位置反向播放,而不是从头开始。过渡中的所有部分(清空的页头和显现的搜索框)都放在同一条时间轴中,绝不一半用 CSS、一半用 GSAP。 - 全站只使用一种缓动曲线,时长使用 token。只有与滚动相关的动效和连续循环(跑马灯、加载指示器)使用线性缓动。固定定位使用
position: sticky,不用 JS。 - 缓动曲线、时长和 stagger 只定义在 SCSS token(
vars/_timings.scss中的$ease-site、$speed-*、$stagger)里,JS 通过 SCSS 模块读取它们:绝不在 JS 中重复写数字。一个客户端插件负责注册 GSAP 插件,并通过gsap.defaults一次性设置全站缓动曲线和基础时长。 - 跟随滚动的动效(视差、文字点亮、页头隐藏)使用与 Lenis 连接的 ScrollTrigger,以便在同一帧更新。由程序控制的滚动(锚点、“返回顶部”)通过
lenis.scrollTo实现,绝不用window.scrollTo或第二套滚动系统。惯性使用 GSAP 实现。 - 弹出层或下拉菜单从其触发按钮处展开(
transformOrigin朝向按钮)。首次显示工具提示后,只要指针仍停留在原处,后续工具提示就立即显示。可拖动的元素(底部面板、轮播)根据手势速度关闭或滚动,并在边缘提供阻力:使用 GSAP 的 Draggable 和 InertiaPlugin。 - 每个动画都以屏幕刷新率运行(每秒 60 或 120 帧),不掉帧:
- 只通过
transform和opacity(x、y、scale、rotation、autoAlpha)移动和缩放,绝不用width、top、margin或padding。只有在下方内容需要移动时(手风琴)才动画改变高度,并且只作用于较小的区块。只有在无法使用transform时,才在小面积表面上使用clip-path和filter; - 对于运动过程中不会改变的值(
transformPerspective、transformOrigin),要先用gsap.set固定:在to()中设置这些值时,GSAP 会从零开始对它们做动画,导致形状在最初几帧发生跳变; - GSAP 在主线程上运行,因此动画期间主线程要保持空闲。开始前先挂载并测量内容(先完成所有读取,再进行写入),图片提前加载,Vue 不要在同一时刻重新绘制大型内容;
- 只在移动的表面上使用
will-change,对多个相同元素使用 token 中的stagger,用gsap.quickTo处理跟随指针的值; - 通过减慢 GSAP(
gsap.globalTimeline.timeScale(0.1))并逐帧检查来验证:相邻帧之间形状不会跳变。
- 只通过
- 模态框的遮罩层与表面放在同一条时间轴上,因为 GSAP 无法对
::backdrop做动画,而它会保持透明。底部面板或抽屉滑入时不做淡入淡出,且<dialog>要设置overflow: clip:否则,表面从外部进入时会出现它自己的滚动条。不要使用hidden,因为代码仍可滚动隐藏内容:showModal()将焦点设在内容上,而内容会随着表面一起停留在屏幕外,导致 dialog 滚动到焦点所在位置;表面一边由 GSAP 移动,一边会直接出现在最终位置。 - 横跨屏幕移动的元素(底部面板、抽屉、面板)的时长根据移动距离确定:使用一个参考 token(例如
$speed-medium对应 1000px),按距离的平方根计算,并限制在两个 token 时长之间。手机上更短,桌面上更长。 - 替换其他内容显示的内容(标签页、使用
v-show的面板)通过淡入淡出包装器淡入,绝不突然切换。 - 进入动画中的模态框表面要设置
will-change: transform:如果浏览器在动画结束时移除其合成层,那么第一次重绘(第一次悬停)时文字和图片就会闪烁。 - 图片库中的图片都放在同一个框内,彼此叠放,且只有当前图片可见:切换时,下一张图片已经加载。始终使用
NuxtImg。 - 始终遵循
prefers-reduced-motion:不做缩放或位移动画,最多使用淡入淡出。要在动画开始时读取该设置,因为它可能在浏览过程中发生变化。 - 绝不在悬停时缩放图片:最多显示遮罩层或文字。
无障碍访问与 SEO
- 装饰性图片使用
alt="",信息性图片使用描述图片内容的alt。文件名应考虑 SEO;在适当的地方使用loading="lazy"和decoding="async"。 - 每个页面只使用一个
<h1>,标题按顺序排列,并使用语义化标记和地标。 - 对比度至少达到 WCAG AA(4.5:1,大号文本为 3:1)。未达标的配色组合应明确标出,不要默默选用。
- 使用
:focus-visible显示焦点,并使其区别于:focus。 - 异步操作完成后,将焦点移至消息上(
tabindex="-1"和.focus()),以播报操作结果。只有在内容变化前就已存在于 DOM 中的实时区域才有效。 <noscript>中只能包含文本,只能进行一次插值,CSS 中不得使用display。启用 JavaScript 时,浏览器会将其读作文本;其中的标签会导致水合不匹配。- 图片按宽度调整尺寸,绝不设置固定高度(使用
block-size: auto,或使用经过专门选择的object-fit容器)。
验证
除 dido-code-style 的要求外:
- 修改过的文件通过 lint,JS 和 CSS 均无问题;
- 修改过的页面返回 200,控制台和日志中无错误,包括水合不匹配;
- 修改过的 util 测试通过;
- 将修改部分中的每个元素与网站上的同类元素进行比较(轮播与轮播、卡片与卡片、链接与链接):组件和行为一致,包括拖动、悬停、按压和过渡效果;
- 使用项目的设计技能(impeccable、design-taste-frontend、mobile-native,如有)在 375、768、1024、1440 和 1920 的视口宽度下检查修改过的界面:内容易读、布局均衡、使用顺畅。仅仅没有显示异常还不够;
- 如果修改了第三方服务,则测量域名和 Cookie;
- 新增第三方服务时,即使没有配置其环境变量,页面也应返回 200。
管理后台
由 starterkit-backend-admin 创建的管理后台(SPA,ssr: false)遵循此技能,但技术栈不同的部分除外:
- 样式:使用 Tailwind、
app/assets/styles/main.css中的@theme令牌以及docs/design-system.md中的模式,而不是 SCSS、slamp和react。颜色使用角色名称(--color-ink、--color-surface),字体使用title-1…2、text-1…3的字号阶梯。类中不得写入颜色或尺寸;尺寸问题由 lint 标出。 - 配置:只使用一个
nuxt.config.ts,不使用configuration/中的切片。 - 动效:按照
docs/design-system.md使用带有motion-reduce的 Tailwind 过渡效果。不要使用 GSAP。 - 文本:只有管理后台支持多种语言时才使用 i18n;仅支持一种语言时,文本直接写在模板中。
- 与网站保持一致:代码风格、
<script setup>、代码块顺序、kebab-case 命名的文件和文件夹、组件单独放在一个文件夹中,以及仅使用useApi()调用后端(riferimento/chiamate-al-backend.md)。登录和密码重置除外:它们使用useApiUrl()直接 fetch;此时,401 表示凭据错误,而不是会话过期。