| name | animal-island-vue-style |
| description | 使用 animal-island-vue 设计风格创建 Vue 3 UI 界面或组件。当用户需要: (1) 用动物森友会风格创建 UI 页面或组件; (2) 使用 animal-island-vue 组件库开发界面; (3) 构建温馨自然、圆润可爱风格的 Vue 界面; (4) 复现或扩展 animal-island-vue 的视觉语言; (5) 提问"动物森友会风格"、"animal island 风格"、"可爱圆润风格"的 UI 时,务必使用此 skill。
|
Animal Island Vue 设计风格指南
三文档分工(生成代码 / 调样式时按需查阅,避免互相翻查):
AI_USAGE.md — API 手册:每个组件的 props、类型、默认值、合法取值、禁用用法。写代码优先查这里。
skill/SKILL.md(本文档)— 像素级样式:设计 token、每组件精确 CSS(hex/px/keyframe)、Demo 布局、新组件开发模板。要自己实现/扩展样式时查这里。
DESIGN_PROMPT.md — 给外部工具(v0 / Figma AI / Midjourney / DALL-E)的提示词包,含 clip-path、色板速查、禁用清单。只在喂别的 AI 时用。
概述
animal-island-vue 是一套受《集合啦!动物森友会》启发的 Vue 3 + TypeScript UI 组件库。
设计语言核心:温暖大地色系 + 大圆角 pill 形 + 游戏按键立体感 + 柔和动效 + 几何 / 有机形状并存(几何代表:Title 飘带的 swallowtail clip-path;有机代表:Modal 的 SVG blob、WeddingInvitation 的不规则虚线边框)。
- 源码:
src/components/<ComponentName>/(每组件包含 *.vue + index.ts + 可选 types.ts)
- Demo 站:
demo/pages/<ComponentName>Demo.vue
- 构建:Vite (library mode) +
vite.config.ts(库)/ vite.config.docs.ts(Demo)
- 样式系统:scoped
<style lang="less" scoped> + BEM + src/styles/variables.less 设计 token(不使用 CSS Modules)
全量导出清单(29 个 named exports = 28 个组件 + 1 个伴生导出按钮)
从 src/index.ts 导出:
| 组件 | 职责 | 交互 | 装饰 / 纯展示 |
|---|
BackTop | 返回顶部按钮,Nook 袋图标浮窗,支持自定义滚动容器、动画时长和可见高度 | ✓ | |
Button | 按钮,5 种类型 × 3 种尺寸 | ✓ | |
Input | 输入框,3 种尺寸 + clear/prefix/suffix | ✓ | |
Switch | 开关,默认/小号 | ✓ | |
Modal | SVG blob 裁切弹窗 | ✓ | |
Card | 容器,default/dashed,13 种 NookPhone 实色 + 13 种 pattern 波点墙纸(CSS radial-gradient,非图片) | | ✓ |
Title | 章节标题,飘带横幅(swallowtail clip-path 燕尾 + 折角阴影 + 微透视正面),13 种配色(替代旧 Card type="title") | | ✓ |
Collapse | 手风琴(动画用 CSS Grid 0fr↔1fr 实现,无 JS 动画) | ✓ | |
Select | 下拉选择器(受控) | ✓ | |
Skeleton | 加载占位骨架屏(SkeletonButton、SkeletonInput、SkeletonAvatar) | | ✓ |
Checkbox | 多选框组,水平/垂直,3 种尺寸 | ✓ | |
Radio | 单选框组,3 种尺寸,键盘 roving tabindex | ✓ | |
Tooltip | 12 种 placement,hover/focus/click 触发,default/island 形态 | ✓ | |
Icon | SVG 图标库(10 个) | | ✓ |
Time | HUD 实时时钟 | | ✓ |
Phone | NookPhone 3×3 应用网格 | | ✓ |
Footer | 底部装饰图(sea/tree) | | ✓ |
Divider | 装饰分割线,5 种风格 | | ✓ |
|
类型导出:BackTopProps、ButtonProps/ButtonType/ButtonSize/ButtonHTMLType、InputProps/InputSize、SwitchProps/SwitchSize、ModalProps、CardProps/CardType/CardColor、TitleProps/TitleSize/TitleColor、FooterProps/FooterType、CollapseProps、CursorProps、TimeProps、PhoneProps、DividerProps/DividerType、TypewriterProps、SelectProps/SelectOption、SkeletonProps/SkeletonVariant、IconProps/IconName、TabsProps/TabItem、CheckboxProps/CheckboxOption/CheckboxSize/CheckboxValue、RadioProps/RadioOption/RadioSize/RadioValue、TooltipProps/TooltipPlacement/TooltipTrigger/TooltipVariant、CodeBlockProps、LoadingProps、TableProps/TableColumn/TableRecord、WeddingInvitationProps/WeddingInvitationExpose/WeddingInvitationExportButtonProps。运行时值:ICON_LIST。
Vue 端约定:
- 受控值统一通过
v-model / v-model:open / v-model:expanded(即 modelValue + update:modelValue 等事件)
- React 中的
ReactNode props 在 Vue 端改为命名插槽(#icon、#prefix、#suffix、#footer、#checked、#unchecked、#question、#empty、Table 的 #cell-{dataIndex} / #header-{dataIndex}、Tabs 按 item.key 命名的动态插槽等);其余可结构化的内容统一通过默认插槽承载(如 Card、Collapse 答案区、Modal 主体、Typewriter 等)
1. Design Tokens
色彩系统
@primary-color: #19c8b9;
@primary-color-hover: #3dd4c6;
@primary-color-active: #11a89b;
@primary-color-bg: #e6f9f6;
@text-color: #794f27;
@text-color-body: #725d42;
@text-color-secondary: #9f927d;
@text-color-muted: #8a7b66;
@text-color-disabled: #c4b89e;
@border-color: #9f927d;
@border-color-light: #c4b89e;
@border-color-hover: #a89878;
@bg-color: #f8f8f0;
@bg-color-content: rgb(247, 243, 223);
@bg-color-secondary: #f0e8d8;
@bg-color-disabled: #f0ece2;
@bg-color-input: rgb(247, 243, );
;
;
;
;
;
;
;
;
;
;
;
;
;
;
NookPhone 应用调色板(Card color prop 可选值):
| color 值 | 背景色 | 文字色 |
|---|
| default | rgb(247, 243, 223) | #725d42 |
| app-pink | #f8a6b2 | #fff |
| purple | #b77dee | #fff |
| app-blue | #889df0 | #fff |
| app-yellow | #f7cd67 | #725d42 |
| app-orange | #e59266 | #fff |
| app-teal | #82d5bb | #fff |
| app-green | #8ac68a | #fff |
| app-red | #fc736d | #fff |
| lime-green | #d1da49 | #3d5a1a |
| yellow-green | #ecdf52 | #725d42 |
| brown | #9a835a | #fff |
| warm-peach-pink | #e18c6f | #fff |
字体
项目使用两款 Google Fonts 圆体字,必须按以下方式引入,本地未安装时通过在线地址加载:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
或在 CSS / Less 入口文件顶部:
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap');
font-family:
Nunito,
'Noto Sans SC',
-apple-system,
'PingFang SC',
'Hiragino Sans GB',
'Microsoft YaHei',
sans-serif;
| 字体 | 用途 | Google Fonts key |
|---|
| Nunito | 主字体,拉丁字符 | family=Nunito |
| Noto Sans SC | 中文字体,简体覆盖 | family=Noto+Sans+SC |
Vue 版本同时通过 @fontsource/* 在 src/index.ts 直接 import 字体子集(nunito / noto-sans-sc / zen-maru-gothic),库消费者无需手动 <link>;如果你脱离组件库自实现,按上文 <link> 引入即可。如需扩展日文字符,自行 @import Zen Maru Gothic 或类似字体并追加到 font-family 末尾。
字重分级:
- 正文内容:500
- 按钮文字、标题、菜单项:600–700
- 数字强调(时间数字、时钟):900
- placeholder / 说明文字:400
字间距:letter-spacing: 0.01em(正文)/ 0.02em(按钮/标题)/ 1.5px(星期大写)
禁止使用细体(weight < 400)或等宽字体。
间距 / 圆角 / 边框
间距:xs=4px sm=8px md=12px lg=16px xl=24px
圆角:sm=12px base=18px lg=24px pill=50px(按钮/输入框)
边框:默认 2px solid,输入框 2.5px,大尺寸输入框 3px
阴影
box-shadow: 0 3px 10px 0 rgba(61, 52, 40, 0.1);
box-shadow: 0 8px 24px 0 rgba(61, 52, 40, 0.14);
box-shadow: 0 2px 4px 0 rgba(61, 52, 40, 0.06);
box-shadow: 0 3px 10px 0 rgba(61, 52, 40, 0.1);
box-shadow: 0 5px 0 0 ;
: ;
: ;
: ;
: ;
: ;
: ;
重要:只有 primary 风格按钮(含 danger primary)才使用 0 5px 0 0 这种像素级 3D 厚阴影;default / dashed / text / link 用上面的柔和 elevation 阴影。把 3D 阴影套到所有按钮上会让界面变得过重过游戏化。
动效
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.15s;
transition: all 0.3s ease;
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateY(-1px);
transform: translateY(-2px);
transform: translateY(2px);
@keyframes animal-zoom-in {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes animal-fade-in {
{
: ;
}
{
: ;
}
}
ac-fade-up {
{
: ;
: ();
}
{
: ;
: ();
}
}
2. 组件精确样式规范
BackTop
返回顶部按钮,Nook 袋图标浮窗,固定在右下角(position: fixed),滚动超过 visibilityHeight px 后出现。
| 属性 | 说明 | 默认值 |
|---|
visibilityHeight | 滚动多少 px 后显示 | 400 |
duration | 滚动动画时长(ms) | 300 |
target | 滚动容器函数 | () => window |
onClick | 点击回调 | - |
className | 自定义类名 | - |
style | 自定义样式 | - |
<BackTop :visibilityHeight="400" />
<BackTop :duration="800" />
<BackTop :target="() => containerRef" :visibilityHeight="200" />
Button
| 属性 | small | middle | large |
|---|
| height | 32px | 45px | 48px |
| padding | 0 16px | 0 20px | 0 32px |
| font-size | 12px | 14px | 16px |
| border-radius | 12px | 50px | 24px |
| border-width | 2px | 2px | 2px |
primary 按钮精确值(仅 primary / danger-primary 用 3D 厚阴影):
color: #794f27;
background: #f8f8f0;
border-color: #f8f8f0;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
box-shadow: 0 5px 0 0 #bdaea0;
transform: translateY(-1px);
box-shadow: 0 6px 0 0 #bdaea0;
transform: translateY(2px);
box-shadow: 0 1px 0 0 #bdaea0;
outline: 2px solid #19c8b9;
outline-offset: 2px;
opacity: 0.5;
default / dashed / text / link 按钮(柔和 elevation):
box-shadow: var(--animal-shadow-sm);
color: #19c8b9;
border-color: #19c8b9;
box-shadow: var(--animal-shadow-base);
transform: translateY(-1px);
color: #11a89b;
border-color: #11a89b;
transform: translateY(0);
box-shadow: var(--animal-shadow-sm);
不要把 primary 那套 0 5px / 6px / 1px #bdaea0 套到 default / dashed 上 —— 整体会显得过重过 cartoon。
loading 斜纹动画(精确值):
background: #0ec4b6;
border: 4px solid #4de2da;
color: #fff;
background-image: repeating-linear-gradient(-45deg, #0ec4b6, #0ec4b6 10px, #01b0a7 10px, #01b0a7 20px);
background-size: 28.28px 28.28px;
animation: animal-btn-loading 1s linear infinite;
@keyframes animal-btn-loading {
0% {
background-position: 0 0;
}
100% {
background-position: -28.28px 0;
}
}
danger primary 按钮:
color: #fff;
box-shadow: 0 5px 0 0 #c94444;
Input
⚠️ shadow prop 默认 false:默认无阴影,下表的 box-shadow 仅在 <Input shadow /> 显式开启时生效。status (error/warning) 阴影与 focus 黄色光晕不受此 prop 控制。
| 属性 | small | middle | large |
|---|
| height | 32px | 40px | 48px |
| padding | 0 14px | 0 18px | 0 22px |
| font-size | 12px | 14px | 16px |
| border-radius | 40px | 50px | 50px |
| border-width | 2.5px | 2.5px | 3px |
box-shadow(仅 shadow={true}) | 0 2px 0 0 #d4c9b4 | 0 3px 0 0 #d4c9b4 | 0 4px 0 0 #d4c9b4 |
精确颜色值:
background: rgb(247, 243, 223);
border: 2.5px solid #c4b89e;
color: #725d42;
font-weight: 500;
letter-spacing: 0.01em;
color: #c4b89e;
font-weight: 400;
color: #a0936e;
margin-right: 6px;
margin-left: 6px;
border-color: #a89878;
box-shadow: 0 3px 0 0 #c4b89e;
border-color: #ffcc00;
box-shadow:
0 3px 0 0 #e0b800,
0 0 0 3px rgba(255, 204, , );
: ;
: ;
: none;
: ;
: ;
: ;
: ;
clear 按钮:
width: 20px;
height: 20px;
margin-left: 4px;
color: #c4b89e;
font-size: 13px;
font-weight: 700;
border-radius: 50%;
transition: all 0.15s;
color: #725d42;
background: rgba(114, 93, 66, 0.1);
Switch
默认尺寸:
min-width: 52px;
height: 28px;
border: 2.5px solid #c4b89e;
border-radius: 50px;
background: #d4c9b4;
box-shadow: inset 0 2px 4px rgba(114, 93, 66, 0.15);
width: 21px;
height: 21px;
top: 50%;
left: 2px;
transform: translateY(-50%);
background: rgb(247, 243, 223);
border: 2.5px solid #bdaea0;
border-radius: 50%;
background: #86d67a;
border-color: #6fba2c;
box-shadow: inset 0 2px 4px rgba(90, , , );
: ( - );
: ;
: solid ;
: ;
: ;
small 尺寸:
min-width: 38px;
height: 20px;
border-width: 2px;
width: 14px;
height: 14px;
top: 1px;
left: 1px;
box-shadow: 0 2px 0 0 #bdaea0;
left: calc(100% - 16px);
box-shadow: 0 2px 0 0 #5a9e1e;
inner 文字(#checked / #unchecked 插槽):
font-size: 11px;
font-weight: 700;
color: #fff;
line-height: 1;
letter-spacing: 0.02em;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
padding: 0 8px 0 28px;
padding: 0 28px 0 8px;
padding: 0 6px 0 20px;
font-size: 9px;
loading spinner:
width: 11px;
height: 11px;
border: 2px solid #6fba2c;
border-right-color: transparent;
border-radius: 50%;
animation: animal-spin 0.6s linear infinite;
border-color: #a89878;
@keyframes animal-spin {
to {
transform: rotate(360deg);
}
}
Card
border-radius: 20px;
background: rgb(247, 243, 223);
padding: 16px 24px;
color: #725d42;
font-weight: 500;
transition: all 0.3s ease;
transform: translateY(-2px);
border: 2px dashed #e8dcc8;
background: rgb(250, 248, 242);
box-shadow: none;
background:
radial-gradient(circle, rgba(248, 166, 178, 0.18) 1.5px, transparent 1.5px) 0 0/28px 28px,
radial-gradient(circle, (, , , ) , transparent ) / ,
;
: solid ;
: ;
旧版 Card type="title" 在 v0.9.x 移除,章节标题请使用独立的 <Title> 组件(见下文)。
Title(飘带 Ribbon 章节标题)
替代旧 Card type="title",渲染游戏风飘带横幅:燕尾两端 + 折角阴影 + 微透视正面主体。
源码:src/components/Title/Title.vue(scoped Less,BEM 类名 animal-title__*)。
--rf: #27d039;
--rb: #20992a;
--rk: #115017;
--rt: #fff;
font-family: Nunito, 'Noto Sans SC', sans-serif;
font-weight: 800;
display: inline-flex;
height: 2em;
padding: 0 1.6em;
letter-spacing: 0.04em;
filter: drop-shadow(0 0.08em 0.12em rgba(0, 0, 0, 0.05));
.animal-title__back--left {
clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 30% 50%, 0% 0%);
}
.animal-title__back--right {
: ( , , , , );
}
: ;
: ;
: -;
{
: ;
: transparent (--rk) transparent transparent;
}
{
: ;
: transparent transparent transparent (--rk);
}
{
: ;
: ;
: () ();
}
尺寸(SIZE_MAP 通过 inline font-size 注入;所有内部 em 自动缩放):
| size | font-size |
|---|
| small | 14px |
| middle | 20px |
| large | 28px |
13 种颜色覆盖:在 wrapper 上叠加 .animal-title--color-app-pink / .animal-title--color-purple / .animal-title--color-app-blue / .animal-title--color-app-yellow / .animal-title--color-app-orange / .animal-title--color-app-teal / .animal-title--color-app-green / .animal-title--color-app-red / .animal-title--color-lime-green / .animal-title--color-yellow-green / .animal-title--color-brown / .animal-title--color-warm-peach-pink 之一;每个类同时覆盖 --rf / --rb / --rk / --rt 四个变量。详见 Title.vue <style scoped> 末尾的 13 行 .animal-title--color-* 定义。
例:
.animal-title--color-app-yellow {
--rf: #f7cd67;
--rb: #d4a030;
--rk: #8a6010;
--rt: #725d42;
}
.animal-title--color-purple {
--rf: #b77dee;
--rb: #9050d0;
--rk: #5a1a9a;
--rt: #fff;
}
Collapse
border-radius: 18px;
border: 2px solid #9f927d;
margin-bottom: 12px;
opacity: 0.6;
padding: 16px 24px;
gap: 12px;
width: 28px; height: 28px;
background: #19c8b9;
color: #fff;
border-radius: 50%;
font-size: 18px; font-weight: 700;
box-shadow: 0 2px 4px rgba(25, 200, 185, 0.3);
transform: rotate(180deg);
opacity: 0.5;
opacity: 1; transform: rotate(45deg);
font-size: ; : ; : ;
: grid;
: fr;
: grid-template-rows (, , , );
: fr;
: hidden;
: ;
: ; : ;
;
Tabs
scoped Less + BEM;类名根 .animal-tabs,内部使用 __list / __item / __icon / __leaf / __content 等子元素。
.animal-tabs {
background: rgb(247, 243, 223);
border-radius: 20px;
border: 2px solid #9f927d;
overflow: hidden;
}
.animal-tabs__list {
display: flex;
gap: 4px;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-bottom: 2px solid #c4b89e;
}
.animal-tabs__item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: transparent;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #8a7b66;
transition: all 0.2s ease;
}
{
: (, , , );
: ;
}
{
: ;
: ;
: ;
}
{
: ;
}
{
: ;
}
{
: ();
}
{
: absolute;
: -;
: -;
: ;
: leafWiggle ease-in-out infinite;
}
leafWiggle {
,
{
: ();
}
{
: (-);
}
{
: ();
}
}
{
: ;
: fadeIn ease;
}
fadeIn {
{
: ;
: ();
}
{
: ;
: ();
}
}
Modal
SVG clip-path 完整 path d 值(精确还原 blob 轮廓):
<template>
<svg style="position: absolute; width: 0; height: 0" aria-hidden="true">
<defs>
<clipPath id="animal-modal-clip" clipPathUnits="objectBoundingBox">
<path
d="M0.501,0.005 L0.501,0.005 L0.523,0.005 L0.549,0.006
C0.704,0.01,0.796,0.017,0.825,0.027
L0.827,0.028
C0.872,0.045,0.939,0.044,0.978,0.17
C1,0.254,1,0.365,0.99,0.505
L0.988,0.513
C0.979,0.558,0.971,0.598,0.965,0.633
C0.956,0.689,0.979,0.77,0.964,0.865
C0.953,0.928,0.921,0.966,0.869,0.979
C0.821,0.986,0.773,0.992,0.726,0.995
L0.712,0.996 L0.694,0.997
C0.648,1,0.586,1,0.507,1
L0.501,1 L0.464,1
C0.385,1,0.325,0.998,0.283,0.995
C0.234,0.992,0.184,0.987,0.133,0.979
C0.081,0.966,0.05,0.928,0.039,0.865
C0.023,0.77,0.047,0.689,0.037,0.633
C0.031,0.595,0.023,0.552,0.013,0.505
C-0.006,0.365,-0.002,0.254,0.024,0.17
C0.064,0.045,0.13,0.045,0.174,0.028
L0.175,0.028
C0.204,0.017,0.303,0.009,0.474,0.005
L0.501,0.005"
/>
</clipPath>
</defs>
</svg>
</template>
Modal 精确样式:
background: rgba(0, 0, 0, 0.35);
animation: animal-fade-in 0.25s ease;
z-index: 1000;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 64px);
animation: animal-zoom-in 0.3s ease;
clip-path: url(#animal-modal-clip);
background: rgb(247, 243, 223);
color: rgb(128, 115, 89);
padding: 48px 48px 32px 48px;
font-size: 28px;
font-weight: 700;
color: rgba(114, 93, 66, 1);
padding-bottom: 15px;
width: ;
: ;
: ;
: (, , , );
: ;
: all ;
: (, , , );
: (, , , );
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: solid (, , , );
: ;
: all ;
: ;
: (, , , );
: (, , , );
: (, , , );
: (, , , );
: (, , , );
: (, , , );
: (, , , );
Time
display: flex; align-items: center;
gap: 20px;
padding: 16px 36px;
background: linear-gradient(180deg, #fff 0%, #f8f8f0 100%);
border: 3px solid #d4cfc3;
border-radius: 18px;
animation: ac-fade-up 0.5s ease-out;
padding-right: 24px;
border-right: 3px solid rgba(159, 146, 125, 0.35);
color: #6fba2c;
font-weight: 900; font-size: 14px;
letter-spacing: 1.5px;
color: #8b7355;
font-weight: 800; font-size: 22px;
color: #8b7355;
font-weight: 900; font-size: 48px;
: ;
: ; : ;
: relative; : -;
: ;
: blink step-end infinite;
blink { { : ; } }
: ; : ;
→ : ;
→ : ;
/ → : ;
Phone(NookPhone)
外壳(固定尺寸,不响应式):
.animal-phone {
width: 527px;
height: 788px;
background: #f8f4e8;
border-radius: 136px;
overflow: hidden;
}
.animal-phone__home-screen {
height: 100%;
padding-top: 40px;
background: #f8f4e8;
background-size: 100% 200%;
animation: grasswave 8s ease-in-out infinite;
display: flex;
flex-direction: column;
align-items: center;
}
@keyframes grasswave {
0%,
100% {
background-position: 0% 0%;
}
50% {
background-position: 0% 100%;
}
}
顶部时间栏:
.animal-phone__date {
padding: 0 70px 31px 70px;
text-align: center;
}
.animal-phone__date-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 32px;
font-weight: 800;
letter-spacing: 2px;
color: #dddbcc;
}
.animal-phone__blink {
font-size: 32px;
font-weight: 800;
color: #dddbcc;
animation: blink 1s steps(1) infinite;
vertical-align: text-bottom;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
.animal-phone__day-text {
font-size: 48px;
font-weight: 800;
color: #725c4e;
letter-spacing: 2px;
height: 56px;
: ;
}
3×3 应用网格:
.animal-phone__apps-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
padding: 8px;
flex: 1;
align-content: center;
justify-content: center;
}
.animal-phone__app-item {
width: 123px;
height: 123px;
border-radius: 45px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.animal-phone__app-item:hover .animal-phone__app-icon {
animation: iconBounce 0.3s ease-in-out forwards;
}
.animal-phone__app-icon {
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: 70% auto;
}
.animal-phone__app-item--offset {
overflow: hidden;
}
.animal-phone__app-icon--offset {
transform: translateY(10px);
}
@keyframes iconBounce {
{
: () ();
}
{
: () (-);
}
{
: () (-);
}
}
应用数据结构(src/components/Phone/Phone.vue):
| id | iconClass | 背景色 | offset | hasNewMessage |
|---|
| camera | iconCamera | #B77DEE | | ✓ |
| app | iconApp | #889DF0 | ✓ | |
| critterpedia | iconCritterpedia | #F7CD67 | | |
| diy | iconDiy | #E59266 | | |
| shopping | iconDesign | #F8A6B2 | | |
| variant | iconMap | #82D5BB | | ✓ |
| design | iconVariant | #8AC68A | | |
| map | iconHelicopter | #FC736D | | |
| chat | iconChat | #D1DA49 | | |
每个 iconClass 都绑定一个 background-image: url('./img/icon-*.svg'),iconApp 特殊使用 background-size: 100% auto(其他是 70% auto)。可用图标资源:icon-miles/camera/chat/critterpedia/design/diy/helicopter/map/shopping/variant.svg,以及状态图标 wifi.svg / location.svg / page.svg。
小红点(新消息):
.animal-phone__badge {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #ff544a;
border: 5px solid #f8f4e8;
}
底部状态图标:
.animal-phone__icon-wifi {
width: 79px;
height: 29px;
background: url('./img/wifi.svg') center/contain no-repeat;
}
.animal-phone__icon-location {
width: 36px;
height: 36px;
background: url('./img/location.svg') center/contain no-repeat;
}
.animal-phone__icon-page {
width: 65px;
height: 32px;
background: url('./img/page.svg') center/contain no-repeat;
}
.animal-phone__page-indicator {
display: flex;
justify-content: center;
align-items: center;
margin-top: 74px;
}
行为: 内部 onMounted + setInterval(1000) 更新时间(onUnmounted 清理),12 小时制 + AM/PM + 零填充分钟,冒号闪烁 1s 一个周期。组件无业务回调,纯展示。
Footer
<template>
<Footer />
<!-- 默认:森林(tree,高 60px) -->
<Footer type="sea" />
<!-- 海浪(高 80px) -->
</template>
.animal-footer {
width: 100%;
height: 80px;
background: url('./img/footer-sea.svg') center/contain no-repeat;
}
.animal-footer--tree {
background-image: url('./img/footer-tree.webp');
height: 60px;
background-size: cover;
background-position: bottom center;
}
sea:SVG 海浪插画,viewBox="0 0 1440 186",多色(珊瑚 #EC7175、海蓝 #327A93、浅蓝 #98D2E3、深青 #008077 等)。
tree:webp 森林剪影,置于页面最底部。
Divider
<template>
<Divider type="line-brown" />
<!-- 默认 -->
<Divider type="line-teal" />
<Divider type="line-white" />
<Divider type="line-yellow" />
<Divider type="wave-yellow" />
</template>
.animal-divider {
width: 100%;
height: 12px;
background: url('./img/divider-line-brown.svg') center/contain no-repeat;
}
.animal-divider--line-teal {
background-image: url('./img/divider-line-teal.svg');
}
.animal-divider--line-white {
background-image: url('./img/divider-line-white.png');
}
.animal-divider--line-yellow {
background-image: url('./img/divider-line-yellow.svg');
}
.animal-divider--wave-yellow {
background-image: url('./img/wave-yellow.svg');
}
默认 SVG 色值参考:#D8D0C3(米褐),viewBox="0 0 297 14"。
Cursor
<template>
<Cursor>
<App />
<!-- 此范围内所有元素变为游戏手指光标 -->
</Cursor>
</template>
样式文件为 普通全局 CSS(非 scoped;类名固定为 animal-cursor,挂在根 <div> 上):
.animal-cursor,
.animal-cursor * {
cursor:
url('./cursor-icon.png') 4 0,
auto !important;
}
cursor-icon.png 热点坐标 (4, 0)
- 使用
!important 覆盖默认光标
- ⚠️ 此组件不能用
scoped:scoped 选择器无法穿透 slot 内容;必须以全局 CSS 形式注册(<style> 不带 scoped,或全局样式入口引入)
Typewriter
<template>
<Typewriter :speed="90" :trigger="openCount" auto-play @done="handleDone">
<p>第一行 <strong>加粗</strong></p>
<p>第二行</p>
</Typewriter>
</template>
Props:
| name | type | default | 说明 |
|---|
| 默认插槽 | Slot | — | 要逐字打出的内容,保留原有元素结构 / 换行 / 样式 |
speed | number (ms) | 90 | 每字间隔 |
trigger | unknown | — | 值变化即重新播放(通常传递弹窗 open 次数或递增 key) |
autoPlay | boolean | true | false 直接全量显示 |
@done | () => void | — | 播放完成事件 |
实现要点:
countText(vnode):递归统计 VNode 树(含 children / el?.textContent / 字符串子节点)的纯文本长度
renderTruncated(vnode, state):按剩余字符数递归裁剪,使用 cloneVNode 保留原节点与样式
watch([() => total.value, () => props.speed, () => props.trigger, () => props.autoPlay]),内部 setInterval 按步递增 count
- 无样式文件,不包裹任何额外 DOM(默认插槽直接 render),对布局零影响
Checkbox
Props:
| name | type | default | 说明 |
|---|
options | CheckboxOption[] | — | 必填;每项 { label, value, disabled? } |
modelValue | Array<string | number> | — | 受控选中值(配合 v-model) |
defaultValue | Array<string | number> | [] | 非受控默认值 |
size | 'small' | 'middle' | 'large' | 'middle' | 尺寸 |
disabled | boolean | false | 禁用全部项 |
direction | 'horizontal' | 'vertical' | 'horizontal' | 排列方向 |
@update:modelValue | (values) => void | — | 选中值变化(v-model) |
@change | (values) => void | — | 同上,业务回调 |
尺寸表(box 方框):
| 属性 | small | middle | large |
|---|
| 宽高 | 18×18px | 22×22px | 28×28px |
| border-width | 2px | 2.5px | 3px |
| 标签 font-size | 12px | 14px | 16px |
| 对勾 font-size | 11px | 13px | 16px |
精确样式:
display: flex; flex-wrap: wrap;
gap: 12px;
flex-direction: column; gap: 8px;
display: inline-flex; align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4,0,0.2,1);
background: rgb(247, 243, 223);
border: 2.5px solid #c4b89e;
border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;
border-color: #19c8b9;
transform: translateY(-1px);
outline: 2px solid #ffcc00; outline-offset: 2px;
background: #19c8b9;
border-color: ;
: ; : ;
: ; : ; : ;
: animal-checkbox-pop (,,,);
animal-checkbox-pop {
{ : (); : ; }
{ : (); }
{ : (); : ; }
}
: ; : ;
: ;
: ;
: not-allowed;
: ;
: ; : ; : none ;
: ;
CodeBlock
Props:
| name | type | default | 说明 |
|---|
code | string | — | 必填;原始源码字符串,内部自动按 JSX/TS 分词高亮 |
style | CSSProperties | — | 会合并覆盖默认深色主题 |
class | string | — | 自定义类名 |
默认主题(写死在组件,不走 Less):
padding: 20px 24px;
background: #2b2118;
border: 1px solid #3d3028;
border-radius: 20px;
font-size: 14px;
line-height: 1.7;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
font-weight: 600;
color: #e8d5bc;
white-space: pre;
overflow: auto;
tab-size: 4;
Token 调色板(COLORS 常量):
| token | 颜色 | 覆盖 |
|---|
| comment | #6b5e50 | /* */、// |
| string | #a8d4a0 | 反引号 / 单双引号、数字 |
| keyword | #d4a0e0 | import/export/const/return/async/...、true/false/null/undefined |
| react | #e06c75 | Vue/ref/computed/onMounted/defineProps/defineEmits/PropType/...(保留 React 名字以兼容跨框架代码片段) |
| component | #80c0e0 | 大写驼峰标识符(组件名、类型名) |
| func | #61afef | 小写标识符后跟 ( |
| prop | #e8c87a | 标识符后跟 =(template props / 赋值) |
| jsx | #f0a870 | <Tag、</Tag、/> |
| operator | #d4b896 | {}[]();, 和 `+-*/=<>& |
| default | #e8d5bc | 其余文本 |
不支持 language prop;非 JS/TS 代码(Python/Shell/SQL)会按通用规则着色,显示可能不准确。不带 copy 按钮、行号或折行。
Radio
源码:src/components/Radio/Radio.vue(scoped Less + BEM 类名 animal-radio__*)。
| 属性 | small | middle | large |
|---|
| 外盒尺寸 | 18×18px | 22×22px | 28×28px |
| 圆角 | 12px | 14px | 16px(重圆方形,非正圆) |
| 边框宽 | 2px | 2px | 2px |
| 内勾尺寸 | 10×10px | 12×12px | 16px font-size |
| label font-size | 12px | 14px | 16px |
background: rgb(247, 243, 223);
border: 2px solid #c4b89e;
border-color: #19c8b9;
transform: translateY(-1px);
background: #19c8b9;
border-color: #11a89b;
@keyframes radio-pop {
0% {
transform: scale(0.4);
opacity: 0;
}
60% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
color: #725d42;
font-weight: 500;
letter-spacing: 0.01em;
color: #794f27;
: solid ;
: ;
: ;
: not-allowed;
: ;
: ;
: ;
: flex;
: ;
: flex;
: column;
: ;
Tooltip
源码:src/components/Tooltip/Tooltip.vue(scoped Less + BEM)。default 与 island 是两套完全不同的视觉,不要混淆。
default 变体(标准温色 bubble):
background: rgb(247, 243, 223);
border: 2px solid #c4b89e;
border-radius: 16px;
padding: 6px 12px;
max-width: 240px;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
letter-spacing: 0.01em;
color: #725d42;
box-shadow: 0 3px 10px rgba(61, 52, 40, 0.1);
z-index: 100;
gap: 10px;
size: 8px;
border-radius: 2px;
island 变体(透明有机气泡):
background: transparent;
border: none;
box-shadow: none;
padding: 12px 20px;
max-width: 280px;
font-weight: 600;
line-height: 1.55;
text-align: center;
.animal-tooltip__island-arrow {
width: 14px;
height: 14px;
border-radius: 50%;
filter: drop-shadow(0 4px 14px rgba(121, 79, 39, 0.14));
}
placement 12 种:top / top_start / top_end / bottom / bottom_start / bottom_end / left / left_start / left_end / right / right_start / right_end。
Loading(全屏遮罩)
源码:src/components/Loading/Loading.vue(scoped Less + BEM)。项目无 GSAP / MotionPath,全部用原生 CSS + SVG stroke-dasharray 实现。
position: absolute;
inset: 0;
background: black;
overflow: hidden;
--mask-r: 0;
mask: radial-gradient(circle at center, transparent var(--mask-r), black calc(var(--mask-r) + 1px));
color: #19c8b9;
animation: spin 1s linear infinite;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
animation: dash 1.5s ease-in-out infinite;
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
: -;
}
}
按钮 inline 的 loading 斜纹(-45deg mint 条纹 28.28px)属于 Button 组件,不要与 <Loading> 全屏遮罩混为一谈。
Table
源码:src/components/Table/Table.vue(scoped Less + BEM)。外壳无实线 border;行分隔靠 ::after 的 dashed 横线实现;hover 行是对角青色条纹。
background: rgb(247, 243, 223);
border-radius: 20px;
padding: 6px;
box-sizing: border-box;
padding: 16px 20px;
font-size: 14px;
font-weight: 700;
color: #725d42;
letter-spacing: 0.02em;
border-image: none;
&::after {
content: '';
border-bottom: 1px dashed rgb(240, 232, 216);
}
padding: 14px 20px;
font-size: 14px;
font-weight: 500;
color: #725d42;
line-height: 1.6;
: (, , , );
: (-, (, , , ) , (, , , ) );
: ;
: ( round );
: ;
: ;
: center;
: ;
: ;
: (, , , );
: ();
: ;
WeddingInvitation
源码:src/components/WeddingInvitation/WeddingInvitation.vue + weddingInvitation.less(外置 Less,便于共享给导出按钮)。这是特种卡:信封外壳 + 底部可撕抽奖券 + 浮动叶子动画,整体通过 defineExpose({ exportAsImage(filename?) }) 暴露给父组件,导出走 modern-screenshot。
max-width: 420px;
padding: 56px 36px var(--lottery-h, 160px);
border-radius: 16px;
background:
radial-gradient(...) ,
url(envelope-texture.png);
filter: drop-shadow(0 10px 24px rgba(61, 52, 40, 0.18));
box-shadow: inset 0 0 0 2px rgba(114, 93, 66, 0.12);
&::before {
background: radial-gradient(circle, ... 14px 14px);
opacity: 0.55;
}
&::after {
border: 1.5px dashed rgba(, , , );
: / ;
}
{
: ;
: ;
: (, , );
: (circle, (, , , ) , transparent );
: ;
: inset - (, , , );
: ;
}
{
: ( (, , , ));
}
float {
,
{
: () ();
}
{
: (-) ();
}
}
: float ease-in-out infinite;
{
: ;
: ;
: (to right, , transparent);
: (to right, transparent, , transparent);
}
导出 PNG 时 Chromium 不读 document.fonts,需要把 @font-face 作为 <style> 子节点塞进截图根节点 —— 见 src/components/WeddingInvitation/WeddingInvitation.vue 中 exportAsImage 的 embedFontStyles 逻辑(通过 defineExpose 暴露给父组件 ref 调用)。
Select
源码:src/components/Select/Select.vue(scoped Less + BEM)。受控选择器,触发区非 Input 样式。
.animal-select__trigger {
background: #fff;
border: 2px solid #e8dcc8;
border-radius: 12px;
padding: 8px 13px;
color: #725d42;
font-weight: 600;
font-size: 14px;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
}
.animal-select__trigger:hover {
border-color: #d4c4a8;
background: #fffdf7;
}
.animal-select__trigger--open {
border-color: #19c8b9;
color: #19c8b9;
}
.animal-select__trigger--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.animal-select__dropdown {
background: #ffeea0;
border-radius: 28px;
padding: 12px 0;
z-index: 100;
opacity: ;
: opacity ease;
}
{
: ;
}
{
: flex;
: center;
: ;
: ;
: ;
: ;
: pointer;
: background ease;
}
{
: (, , , );
}
{
: ;
}
Props: modelValue(受控,必填)、options({ key, label }[],必填)、placeholder(默认 '请选择')、disabled、ariaLabel、ariaLabelledBy。Options 迭代用 v-for + :key。
Skeleton
源码:src/components/Skeleton/Skeleton.vue(scoped Less + BEM)。加载占位骨架屏,4 个子组件:Skeleton、SkeletonButton、SkeletonInput、SkeletonAvatar。
.animal-skeleton {
display: block;
width: 100%;
}
.animal-skeleton__block {
background: #e8e2d6;
border-radius: 8px;
animation: animal-skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes animal-skeleton-pulse {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
.animal-skeleton__block--text {
height: 16px;
border-radius: 8px;
}
.animal-skeleton__block--circle {
border-radius: 50%;
}
.animal-skeleton__block--rect {
border-radius: 8px;
}
.animal-skeleton__block--paragraph {
margin-bottom: 12px;
}
.animal-skeleton-btn {
height: 45px;
border-radius: 50px;
}
.animal-skeleton-input {
height: 40px;
border-radius: 50px;
}
{
: ;
: ;
: ;
}
Props: loading(boolean,默认 true)、variant('text' | 'circle' | 'rect' | 'paragraph',默认 'text')、active(boolean,默认 true)、rows(number,默认 3)、width(number|string)、rowWidths((number|string)[])、widthValue(number|string)、heightValue(number|string)。默认插槽:loading=false 时渲染的内容。
Icon
源码:src/components/Icon/Icon.vue(scoped Less + BEM)。10 个内置 SVG 图标,通过 background-image 渲染。
.animal-icon {
display: inline-block;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.animal-icon--bounce:hover {
animation: animal-icon-bounce 0.3s ease-in-out forwards;
}
@keyframes animal-icon-bounce {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.2) rotate(-5deg);
}
100% {
transform: scale(1.1) rotate(-4deg);
}
}
10 个图标名:icon-miles, icon-camera, icon-chat, icon-critterpedia, icon-design, icon-diy, icon-helicopter, icon-map, icon-shopping, icon-variant。运行时导出 ICON_LIST({ name, label }[])。Props: name(IconName,必填)、size(number|string,默认 24)、bounce(boolean,默认 false)、src(string — 自定义图片 URL,与 name 互斥)。
Tag
源码:src/components/Tag/Tag.vue(scoped Less + BEM)。Pill 形标签,支持 3 种变体 × 13 色。
.animal-tag {
display: inline-flex;
align-items: center;
gap: 4px;
line-height: 1;
font-weight: 600;
border-radius: 999px;
border: 1.5px solid transparent;
transition: all 0.2s ease;
user-select: none;
white-space: nowrap;
}
.animal-tag--small {
height: 24px;
padding: 0 10px;
font-size: 12px;
}
.animal-tag--medium {
height: 29px;
padding: 0 12px;
font-size: 13px;
}
.animal-tag--large {
height: 34px;
padding: 0 16px;
font-size: 15px;
}
.animal-tag--solid {
background: rgb(247, 243, 223);
color: #8f734f;
: ;
}
{
: transparent;
: ;
: ;
}
{
: transparent;
: ;
: dashed;
: ;
}
{
: (--animal-tag-color);
: (--animal-tag-color);
: ;
}
,
{
: (--animal-tag-color);
: (--animal-tag-color);
: transparent;
}
{
: ;
: ;
: none;
: ;
: (, , , );
: inherit;
: ;
: pointer;
: background ease;
}
{
: (, , , );
}
{
: pointer;
}
{
: (-);
: (, , , );
}
{
: solid ;
: ;
}
{
: ;
: not-allowed;
: none;
}
Props: size('small' | 'medium' | 'large',默认 'medium')、variant('solid' | 'outlined' | 'dashed',默认 'solid')、color(13 色,默认 'default')、closable、disabled。Emits: close。默认插槽标签内容。
Progress
源码:src/components/Progress/Progress.vue(scoped Less + BEM)。线性进度条,条纹动画 fill。
.animal-progress {
display: flex;
align-items: center;
width: 100%;
line-height: 1;
user-select: none;
}
.animal-progress__row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.animal-progress__info {
font-weight: 700;
color: #725d42;
white-space: nowrap;
flex-shrink: 0;
letter-spacing: 0.02em;
}
.animal-progress__track {
flex: 1;
min-width: 80px;
background: #f8f8f0;
border: 2px solid #e8dcc8;
box-shadow: inset 0 2px 4px rgba(114, 93, 66, 0.08);
overflow: hidden;
border-radius: 999px;
}
.animal-progress__track--small {
height: 12px;
border-width: ;
}
{
: ;
}
{
: ;
}
{
: absolute;
: ;
: ;
: ;
: ;
: ;
: (-, , , , );
: ;
: animal-progress-stripe linear infinite;
: width (, , , );
}
{
: none;
}
animal-progress-stripe {
{
: ;
}
{
: - ;
}
}
{
: absolute;
: ;
: ;
: (-);
: ;
: ;
: ;
: ;
: (, , , );
: nowrap;
: ;
}
Props: percent(0–100,必填)、size('small' | 'middle' | 'large',默认 'middle')、showInfo(默认 true)、infoPosition('inside' | 'right' | 'top',默认 'inside')、infoFormat(fn)、duration(秒,默认 0.6,0 为不动画)。受 prefers-reduced-motion: reduce 影响。
Notification
命令式 API 通知系统。源码:src/components/Notification/(Notification.ts + NotificationContainer.vue + NotificationView.vue)。
.animal-notification-root {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2000;
}
.animal-notification__position {
position: fixed;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
max-width: calc(100vw - 32px);
}
.animal-notification__position--top {
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.animal-notification__position--topLeft {
top: 20px;
left: 20px;
}
.animal-notification__position--topRight {
top: 20px;
right: 20px;
}
.animal-notification__position--bottom {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.animal-notification__position--bottomLeft {
bottom: 20px;
left: ;
}
{
: ;
: ;
}
{
: auto;
: flex;
: flex-start;
: ;
: ;
: (, , );
: ;
: solid ;
: (, , , );
: ;
: animal-notification-slide-in ease;
}
{
: animal-notification-slide-out ease forwards;
}
animal-notification-slide-in {
{
: ;
: (-);
}
{
: ;
: ();
}
}
animal-notification-slide-out {
{
: ;
: ();
}
{
: ;
: (-);
}
}
{
: ;
}
{
: ;
}
{
: ;
}
{
: ;
}
{
: ;
: ;
: ;
: ;
}
{
: ;
}
{
: ;
}
{
: ;
}
{
: ;
}
{
: ;
: ;
: ;
: ;
}
{
: ;
: ;
: ;
: ;
: ;
}
{
: ;
: ;
: ;
: none;
: transparent;
: ;
: ;
: pointer;
: ;
: flex;
: center;
: center;
: all ease;
}
{
: (, , , );
: ;
}
API:Notification.open(config)、.success()、.info()、.warning()、.error()、.destroy(key?)。Config 字段:message(必填)、description、duration(默认 4.5s,0=不自动关闭)、position(6 种)、icon(VNode)、btn(VNode)、key、onClose、onClick、closeIcon、className、style。需在 App 根节点放置 <NotificationContainer />。
Drawer
源码:src/components/Drawer/Drawer.vue(scoped Less + BEM)。侧边/顶部/底部滑出面板,带焦点陷阱、背景景深效果。
.animal-drawer__mask {
position: fixed;
inset: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.35);
animation: animal-fade-in 0.25s ease;
}
.animal-drawer__panel {
position: fixed;
z-index: 1000;
background: rgb(247, 243, 223);
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(61, 52, 40, 0.18);
transition: transform 0.3s cubic-bezier(0.2, 0, 0.2, 1);
}
.animal-drawer__panel--right {
top: 0;
right: 0;
bottom: 0;
transform: translateX(0);
}
.animal-drawer__panel--left {
: ;
: ;
: ;
: ();
}
{
: ;
: ;
: ;
: ();
}
{
: ;
: ;
: ;
: ();
}
{
: () ();
}
{
: ();
}
{
: flex;
: center;
: space-between;
: ;
: ;
}
{
: ;
: ;
: ;
}
{
: ;
: ;
: none;
: transparent;
: ;
: ;
: pointer;
: ;
: all ease;
}
{
: (, , , );
: ;
}
{
: ;
: auto;
: ;
}
{
: ;
: ;
: solid ;
}
Props: open(必填)、title、placement('left' | 'right' | 'top' | 'bottom',默认 'right')、width(默认 378)、height(默认 300)、maskClosable(默认 true)、pushBackground(默认 true)、footer、maskStyle。Emits: close。焦点陷阱:Tab 循环、ESC 关闭、关闭后恢复焦点。打开时锁定 body 滚动。pushBackground 对背景元素施加 scale(0.94) + blur(1px) + borderRadius(14px)。
Wallet
源码:src/components/Wallet/Wallet.vue(scoped Less + BEM)。钱袋 + 金额 pill 的 NookPhone 风格货币展示。
.animal-wallet {
--wallet-pill-w: 132px;
--wallet-pill-h: 42px;
--wallet-bag: 50px;
--wallet-text-size: 17px;
--wallet-halo: 4px;
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
width: var(--wallet-pill-w);
padding-top: calc(var(--wallet-bag) * 0.7);
user-select: none;
line-height: 1;
}
.animal-wallet--small {
--wallet-pill-w: 96px;
--wallet-pill-h: 32px;
--wallet-bag: 38px;
--wallet-text-size: 12px;
--wallet-halo: 3px;
}
.animal-wallet--large {
--wallet-pill-w: 168px;
--wallet-pill-h: 52px;
--wallet-bag: 62px;
--wallet-text-size: 22px;
--wallet-halo: 5px;
}
.animal-wallet__bag-slot {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: var(--wallet-bag);
height: var(--wallet-bag);
z-index: 1;
}
.animal-wallet__bag-img {
: ;
: ;
: contain;
}
{
: (--wallet-pill-w);
: (--wallet-pill-h);
: ;
: ;
: flex;
: center;
: center;
: (--wallet-halo) ;
}
{
: ;
: (--wallet-text-size);
: ;
: ;
: (, , , );
}
Props: value(number|string,默认 '00,000',数字自动千分位格式化)、icon(string — 自定义图片 URL)、size('small' | 'medium' | 'large',默认 'medium')、thousandSeparator(string,默认 ',')。插槽 #icon 替换默认钱袋图标。
Form(表单系统)
源码:src/components/Form/(Form.vue + FormItem.vue + FormProvider.vue + context.ts + useForm.ts + validators.ts + types.ts)。声明式表单 + 校验,支持嵌套字段。
Form 组件:
.animal-form {
font-family: @font-family;
&--horizontal {
.animal-form-item {
display: flex;
align-items: center;
gap: 12px;
}
}
&--vertical {
.animal-form-item {
display: flex;
flex-direction: column;
gap: 4px;
}
}
&--inline {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
}
FormItem 样式:
.animal-form-item {
width: 100%;
&__label {
font-size: 14px;
font-weight: 600;
color: #725d42;
white-space: nowrap;
}
&__required {
color: #e05a5a;
margin-left: 2px;
}
&__control {
flex: 1;
}
&__help {
font-size: 12px;
font-weight: 500;
margin-top: 4px;
line-height: 1.4;
}
&__help--error {
color: #e05a5a;
}
&__help--warning {
color: #dba90e;
}
&--has-error .animal-form-item__control :deep(input) {
border-color: #e05a5a;
}
}
Form Props: form(FormInstance)、initialValues、layout('horizontal' | 'vertical' | 'inline',默认 'horizontal')、labelAlign、labelCol/wrapperCol({ span, offset })、size('small' | 'middle' | 'large')、disabled、colon(默认 true)、requiredMark(boolean | 'optional')、onFinish、onFinishFailed、onValuesChange、onReset。
FormItem Props: name(NamePath — 支持 'user.name' 嵌套)、label、rules(RuleObject[])、required、dependencies、valuePropName(默认 'modelValue')、trigger(默认 'onUpdate:modelValue')、getValueFromEvent、normalize、hidden、hasFeedback、validateStatus、help、noStyle、labelCol、wrapperCol、colon、requiredMark、layout、initialValue。
RuleObject: required、message、min、max、len、pattern(RegExp)、whitespace、type('string' | 'number' | 'boolean' | 'integer' | 'float' | 'array' | 'object' | 'email' | 'url' | 'date')、validator(async fn → void | string)。
FormInstance 方法(useForm() 产出):getFieldValue、getFieldsValue、setFieldValue、setFieldsValue、resetFields、validateFields(返回 Promise)、submit、setFields、isFieldTouched、isFieldValidating、getFieldError、scrollToField。
Image
源码:src/components/Image/Image.vue(非 scoped Less + BEM,全局样式与 Drawer 一致以便 Teleport 预览弹层命中)。白色衬板相框图片组件,支持懒加载、加载失败占位、点击预览大图。
.animal-image {
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #fff;
padding: 12px;
border-radius: 8px;
box-shadow: 0 8px 14px 0 rgba(0, 0, 0, 0.08);
line-height: 0;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.animal-image__img {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.25s ease;
}
.animal-image--loaded .animal-image__img {
opacity: 1;
}
.animal-image--error {
flex-direction: column;
gap: 8px;
color: #c4b89e;
font-size: ;
: ;
}
{
: zoom-in;
: none;
}
{
: (, , );
: ;
}
{
: ;
: ;
}
{
: fixed;
: ;
: ;
: flex;
: center;
: center;
: (, , , );
: animal-image-fade-in ease;
}
{
: (, );
: ;
: ;
: contain;
: (, , , );
}
{
: absolute;
: ;
: ;
: ;
: ;
: ;
: (, , , );
: solid (, , , );
}
Props:src(string,必填)、alt(string,默认 '')、width/height(number|string,数字自动加 px)、color(ImageColor,默认 'white',14 种 Card 底色)、lazy(boolean,默认 false → 原生 loading="lazy")、preview(boolean,默认 true)。Emits:load(Event)、error(Event)。preview=true 时相框渲染为 <button>(原生 Enter/Space 激活),预览弹层经 <Teleport to="body"> 挂载,支持 ESC / 遮罩点击 / 关闭按钮关闭,关闭后焦点归还触发元素。
3. Demo 布局精确规范
这是 Demo 站(demo/App.vue)的实际布局数值,用于还原完整页面效果:
整体布局
background:
url(home_bg.svg) center/cover no-repeat,
#7dc395;
background: url(content_bg_pc.jpg) center fixed;
width: 220px;
min-width: 220px;
background: url(menu_bg.svg) center/cover no-repeat;
Sidebar 精确值
padding: 20px 16px 12px;
border-bottom: 1px solid #e8e2d6;
font-weight: 700;
font-size: 15px;
color: #725d42;
letter-spacing: -0.3px;
width: 24px;
height: 24px;
margin-right: 8px;
padding: 8px 0;
padding: 12px 16px 4px;
font-size: 11px;
color: #a0936e;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
margin: 1px 5px;
height: 40px;
padding: 0 16px;
padding-left: 26px;
font-weight: 600;
font-size: ;
: ;
: all ;
: ;
: transparent;
: ;
: ;
: ;
主内容区
padding: 32px 40px;
left: 220px;
width: calc(100% - 220px);
z-index: 0;
pointer-events: none;
移动端适配
height: 52px; padding: 0 12px;
background: rgba(255, 252, 244, 0.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid #e8e2d6;
z-index: 50;
font-size: 20px; color: #725d42; padding: 4px 8px; border-radius: 8px;
68px;
width: 240px; z-index: 99;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
background: rgba(0, 0, 0, 0.35); z-index: 98;
4. HomePage 精确规范
padding: 60px 40px 40px;
min-height: 80vh;
font-size: 50px;
font-weight: 700;
color: #fff9e6;
text-shadow: 0px 4px 1px rgba(0, 0, 0, 0.4);
margin: 0 0 12px;
font-size: 12px;
font-weight: 600;
padding: 2px 10px;
border-radius: 10px;
background: #e6f9f6;
color: #19c8b9;
margin-left: 8px;
font-size: 17px;
color: #7c5734;
line-height: 1.7;
margin: 0 0 28px;
max-width: 520px;
: ;
: ;
: ;
: ;
: auto;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: (auto-fit, (, fr));
: ;
: (-);
: (, , , );
: () (-);
: ;
: auto;
: ;
: ;
: solid ;
: ;
: ;
: ;
: ;
: ;
代码高亮配色:
| Token 类型 | 颜色 |
|---|
| 注释 | #6b5e50(italic, weight 400) |
| 字符串 | #a8d4a0 |
| Vue 模板标签 | #f0a870 |
| 关键字 / npm/pnpm | #f0a870 |
| 命令动词(install/add) | #a8d4a0 |
括号 {} | #d4b896 |
箭头 => | #d4a0e0 |
| CSS 变量名 | #e8c87a |
:root | #f0a870 |
| 十六进制色值 | #8ab8e0 |
5. 自实现 CSS 变量完整模板
不依赖组件库时,在 :root 中声明以下变量:
:root {
--animal-font: Nunito, 'Noto Sans SC', -apple-system, 'PingFang SC', 'Hiragino Sans GB', sans-serif;
--animal-primary: #19c8b9;
--animal-primary-hover: #3dd4c6;
--animal-primary-active: #11a89b;
--animal-primary-bg: #e6f9f6;
--animal-text: #794f27;
--animal-text-body: #725d42;
--animal-text-secondary: #9f927d;
--animal-text-muted: #8a7b66;
--animal-text-disabled: #c4b89e;
--animal-bg: #f8f8f0;
--animal-bg-content: rgb(247, 243, 223);
--animal-bg-disabled: #f0ece2;
--animal-border: #c4b89e;
--animal-border-hover: #a89878;
--animal-radius-sm: 12px;
--animal-radius: 18px;
--animal-radius-lg: 24px;
--animal-radius-pill: 50px;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: (, , , );
: ;
: ;
: ;
}
6. 7 条设计铁律
- 颜色:大地棕色系文字 + 薄荷青绿主色 + 奶油米白背景,禁止纯黑 / 冷灰
- 圆角:最小 12px;按钮、输入框必须 50px pill 形
- 立体感:3D 厚阴影(
0 Npx 0 0 [暗色] + hover 上浮 / active 下压)仅用于 primary 按钮 / danger-primary 按钮 / Input / Switch;default / dashed / text / link 按钮用柔和 elevation 阴影(0 2px 4px / 0 3px 10px rgba(61,52,40,...))即可
- 字体:Nunito(Google Fonts)圆体,按钮/标题 weight 600+,从不使用细体
- 动效:过渡 0.15~0.35s,缓动
cubic-bezier(0.4, 0, 0.2, 1),平滑不生硬
- 焦点:输入框用黄色
#ffcc00,按钮用青绿 #19c8b9,绝不用蓝色
- 禁止:直角矩形交互元素、纯黑文字
#000、冷蓝色调、扁平无阴影设计
7. 新组件文件结构模板
src/components/MyComponent/
├── MyComponent.vue # SFC:<script setup lang="ts"> + <template> + <style lang="less" scoped>
├── types.ts # (可选)独立类型文件,跨文件复用泛型时必需
└── index.ts # 统一导出
src/components/MyComponent/index.ts:
export { default as MyComponent } from './MyComponent.vue';
export type { MyComponentProps } from './types';
src/index.ts 追加:
export { MyComponent } from './components/MyComponent';
export type { MyComponentProps } from './components/MyComponent';
仓库内所有组件均使用 <script setup lang="ts"> + scoped Less + BEM;禁止使用 CSS Modules(*.module.less)。
Less / BEM 模板(直接使用设计 token):
@import '@/styles/variables.less';
.animal-mycomp {
background: @bg-color-content;
color: @text-color-body;
border: @border-width solid @border-color-light;
border-radius: @border-radius-base;
font-family: @font-family;
font-weight: 500;
letter-spacing: 0.01em;
transition: all @motion-duration-base @motion-ease;
box-shadow: 0 3px 0 0 @shadow-input;
&:hover:not(.animal-mycomp--disabled) {
border-color: @border-color-hover;
transform: translateY(-1px);
box-shadow: 0 4px 0 0 @shadow-input;
}
&:focus-within {
border-color: ;
:
,
(, , , );
}
{
: ;
: -allowed;
: ;
: ;
: ;
: none;
}
{
: inline-flex;
: center;
}
}
Vue SFC 模板:
<script setup lang="ts">
import type { MyComponentSize } from './types';
interface Props {
/** 尺寸 */
size?: MyComponentSize;
/** 禁用 */
disabled?: boolean;
}
withDefaults(defineProps<Props>(), {
size: 'middle',
disabled: false,
});
defineEmits<{ (e: 'click', event: MouseEvent): void }>();
defineSlots<{ default?: () => unknown; icon?: () => unknown }>();
</script>
<template>
<div
class="animal-mycomp"
:class="[`animal-mycomp--${size}`, { 'animal-mycomp--disabled': disabled }]"
@click="$emit('click', $event)"
>
<span v-if="$slots.icon" class="animal-mycomp__icon">
<slot name="icon" />
</span>
<slot />
</div>
</template>
<style lang="less" scoped>
@import '@/styles/variables.less';
.animal-mycomp {
font-family: @font-family;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
background: @bg-color-input;
color: @warm-color-soft;
border: 2px solid @border-color-light;
border-radius: 50px;
box-shadow: 0 3px 0 0 @shadow-soft;
transition: all @motion-duration-base @motion-ease;
&:hover:not(.animal-mycomp--disabled) {
border-color: @border-color-hover;
transform: translateY(-1px);
box-shadow: 0 4px 0 0 @shadow-soft-hover;
}
&:focus-within {
border-color: #ffcc00;
box-shadow:
0 3px 0 0 #e0b800,
0 0 0 3px rgba(255, 204, 0, 0.15);
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
background: @bg-color-disabled;
color: @text-color-disabled;
box-shadow: none;
}
&--small {
height: 32px;
padding: 0 16px;
font-size: @font-size-sm;
}
&--middle {
height: 45px;
padding: 0 20px;
font-size: @font-size-base;
}
&--large {
height: 48px;
padding: 0 32px;
font-size: @font-size-lg;
}
&__icon {
display: inline-flex;
align-items: center;
}
}
</style>
Less / BEM 提示:
- 顶部
@import '@/styles/variables.less'; 引入全局 token,组件内统一用 @xxx 语法。
- 状态样式用 BEM 修饰符(
&--disabled、&--checked)而非堆叠工具类,便于阅读和覆盖。
- 子元素一律用
&__name,避免出现孤立的 .foo .bar 选择器,scoped 编译后 BEM 命名足以隔离。
- 时长 token:
@motion-duration-fast (0.15s) / @motion-duration-base (0.25s) / @motion-duration-slow (0.35s),缓动统一用 @motion-ease。
8. Demo 页面规范
每个组件在 demo/pages/<ComponentName>Demo.vue 创建演示页:
<script setup lang="ts">
import { MyComponent } from '../../src';
import { CodeBlock, ApiTable } from '../tools';
const props = [{ name: 'size', type: "'small' | 'middle' | 'large'", default: "'middle'", description: '尺寸' }];
</script>
<template>
<div>
<h2>MyComponent</h2>
<MyComponent size="large">内容</MyComponent>
<CodeBlock :code="`<MyComponent size="large">内容</MyComponent>`" />
<ApiTable :data="props" />
</div>
</template>
并在 demo/ComponentPage.vue(或 demo/router.ts)注册路由,同时把 title / desc 写入 demo/pageInfo.ts:
export const PAGE_INFO: Record<string, { title: string; desc: string }> = {
button: { title: 'Button 按钮', desc: '...' },
input: { title: 'Input 输入框', desc: '...' },
switch: { title: 'Switch 开关', desc: '...' },
card: { title: 'Card 卡片', desc: '...' },
collapse: { title: 'Collapse 折叠面板', desc: '...' },
cursor: { title: 'Cursor 光标', desc: '...' },
time: { title: 'Time 时间', desc: '...' },
phone: { title: 'Phone 手机', desc: '...' },
footer: { title: 'Footer 底部装饰', desc: '...' },
modal: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
: { : , : },
};
新增组件务必追加对应条目,否则 Demo 侧栏不会展示。