| name | uniapp |
| description | UniApp + Vue3 mobile development standards. Use this skill when developing UniApp mobile apps, using wot-design-uni components, implementing mobile UI/UX, or handling mini-program compatibility. |
UniApp 移动端开发规范
触发条件
- Develop UniApp mobile projects
- Use wot-design-uni components
- Implement mobile UI/UX
- Handle mini-program compatibility issues
- Use UnoCSS atomic classes
Part 0: 强制规则
核心分层原则
BEM 定义语义,UnoCSS 补充微调,SCSS 只做脏活。
| 层级 | 职责 | 示例 |
|---|
| BEM | 组件语义锚点 | profile-card、profile-card__avatar |
| UnoCSS | 简单布局微调(≤3 个属性) | flex items-center gap-16rpx |
| SCSS | 伪类/动画/穿透等原子类做不了的事 | @keyframes、复杂选择器 |
选型决策
| 场景 | 方案 | 示例 |
|---|
| 简单布局(1-2 个属性) | 原子类 | class="flex items-center" |
| 组件样式(3+ 属性) | BEM | class="profile-card" |
| 重复出现的组合 | Shortcuts 收敛 | class="flex-center" |
| 主题相关 | CSS 变量 + BEM | background: var(--color-primary) |
<!-- ✅ 组件样式归 BEM,微调用原子类 -->
<view class="profile-card">
<image class="profile-card__avatar" :src="user.avatar" />
<text class="profile-card__name">{{ user.name }}</text>
</view>
<!-- ✅ 简单间距直接用原子类 -->
<view class="mt-16rpx p-4">
<wd-search v-model="keywords" placeholder="搜索" />
</view>
<!-- ❌ 同一元素混用过多原子类 + BEM -->
<view class="profile-card flex flex-col p-28rpx bg-white rounded-28rpx shadow-sm">
</view>
0.1 组件优先原则
能用 wot-design-uni 官方组件,绝不自定义
| 需求 | 使用 | 避免 |
|---|
| 按钮 | <wd-button> | 自定义 .btn |
| 输入框 | <wd-input> | 自定义 .input |
| 卡片 | <wd-card> / <wd-cell-group> | 自定义 .card |
| 弹窗 | <wd-popup> / <wd-dialog> | 自定义弹窗 |
| 列表 | <wd-cell> / <wd-grid> | 自定义列表 |
| 加载 | <wd-loading> / <wd-skeleton> | 自定义动画 |
| 空状态 | <wd-status> | 自定义空状态 |
0.2 CSS 统一原则
禁止重复定义相同样式,必须复用
.user-card {
padding: 24rpx;
background: var(--color-bg);
}
.role-card {
padding: 24rpx;
background: var(--color-bg);
}
.base-card {
padding: 24rpx;
background: var(--color-bg);
border-radius: 16rpx;
}
0.3 暗黑模式强制要求
所有颜色必须使用 CSS 变量,禁止硬编码
.card {
background: #ffffff;
color: #333333;
}
.card {
background: var(--color-bg);
color: var(--color-text);
}
Part 1: 技术栈
- Vue - 前端框架
- Uni-app - 跨平台框架
- wot-design-uni - UI 组件库
- TypeScript - 类型安全
- UnoCSS - 原子 CSS
- Pinia - 状态管理
Part 2: 目录结构
src/
├── api/ # API 请求
│ ├── system/ # 系统模块
│ │ ├── user.ts
│ │ └── role.ts
│ └── index.ts
├── components/ # 全局组件
│ ├── UserCard/
│ │ └── index.vue
│ └── CustomTree/
│ └── index.vue
├── composables/ # 组合式函数
│ ├── useList.ts # 列表逻辑
│ ├── useForm.ts # 表单逻辑
│ └── useAuth.ts # 认证逻辑
├── enums/ # 枚举定义
│ ├── ResultCode.ts
│ └── Status.ts
├── pages/ # 页面
│ ├── index/ # 首页
│ │ └── index.vue
│ ├── login/ # 登录
│ │ └── index.vue
│ ├── mine/ # 我的
│ │ └── index.vue
│ └── work/ # 工作台
│ ├── menu/
│ │ └── index.vue
│ └── user/
│ └── index.vue
├── static/ # 静态资源
│ ├── images/
│ └── icons/
├── stores/ # Pinia 状态
│ ├── modules/
│ │ ├── user.ts
│ │ └── app.ts
│ └── index.ts
├── styles/ # 全局样式
│ ├── variables.scss # CSS 变量
│ ├── index.scss # 入口
│ └── uno.scss # UnoCSS 配置
├── utils/ # 工具函数
│ ├── request.ts # 请求封装
│ ├── auth.ts # Token 管理
│ ├── permission.ts # 权限判断
│ └── format.ts # 格式化
├── App.vue # 根组件
├── main.ts # 入口
├── manifest.json # 应用配置
├── pages.json # 页面配置
└── uni.scss # UniApp 样式变量
Part 3: CSS 使用边界
UnoCSS 原子类 + BEM 混合方案
| 原子类数量 | 方案 |
|---|
| ≤3 个 | UnoCSS 原子类 |
| >3 个 | BEM CSS 类 |
合并原则(元素已有类名时)
<!-- ❌ 错误:类名 + 原子类混用 -->
<view class="filter-bar flex items-center mt-12rpx">
<!-- ✅ 正确:原子类合并到 CSS 类 -->
<view class="filter-bar">
.filter-bar {
display: flex;
align-items: center;
margin-top: 12rpx;
}
使用预设快捷类
| 原子类组合 | 预设 | 减少 |
|---|
flex items-center | flex-start | 2→1 |
flex justify-between items-center | flex-between | 3→1 |
flex justify-center items-center | flex-center | 3→1 |
Part 4: 命名规范
文件命名
| 类型 | 规范 | 示例 |
|---|
| 组件 | PascalCase | UserCard.vue |
| 页面 | kebab-case 或小写 | user-profile.vue |
| 组合式函数 | camelCase + use | useUserStore.ts |
| 工具函数 | camelCase | formatDate.ts |
| 常量 | UPPER_SNAKE_CASE | API_PREFIX.ts |
变量命名
| 类型 | 规范 | 示例 |
|---|
| 变量 | camelCase | userList |
| 常量 | UPPER_SNAKE_CASE | MAX_COUNT |
| 私有变量 | _ 前缀 | _internalState |
| 布尔值 | is/has/can 前缀 | isLoading, hasToken |
方法命名
| 动作 | 前缀 | 示例 |
|---|
| 获取/加载 | fetch/load | fetchUserList |
| 提交/保存 | submit/save | submitForm |
| 新增 | create | createUser |
| 更新 | update | updateUser |
| 删除 | delete | deleteUser |
| 打开/关闭 | open/close | openDialog |
| 事件处理 | handle | handleSubmit |
CSS 命名(BEM)
block__element--modifier
│ │ │
│ │ └── 状态/变体(双连字符)
│ └── 组成部分(双下划线)
└── 页面前缀 + 功能实体
Block 必须带页面前缀:
首页(index) → home-hero / home-nav / home-stat
我的(mine) → mine-hero / mine-profile / mine-menu
登录(login) → login-form / login-brand / login-nav
工作台(work) → work-menu / work-user / work-card
Element 通用词汇:
| 语义 | 标准词汇 | 禁止 |
|---|
| 头部 | __header | head / top |
| 底部 | __footer | bottom |
| 标题 | __title | name / heading |
| 副标题 | __subtitle | desc(描述时用 desc) |
| 描述 | __desc | text / detail |
| 图标 | __icon | img / pic |
| 图片 | __image | img / bg |
| 头像 | __avatar | photo |
| 标签 | __tag | badge(徽章时用 badge) |
| 徽章 | __badge | dot(圆点时用 dot) |
| 操作按钮 | __action | action-btn |
| 操作区 | __actions | btns |
| 内容 | __body | content |
| 输入框 | __input | field(行容器时用 field) |
| 表单行 | __field | row |
| 列表 | __list | items |
| 列表项 | __item | row |
SCSS 嵌套:
.login {
background: linear-gradient(135deg, var(--color-bg-tertiary), var(--color-primary-light));
&__field {
display: flex;
align-items: center;
&:focus-within {
box-shadow: 0 0 0 2px var(--color-primary);
}
}
&__code-btn {
height: 64rpx;
border-radius: 16rpx;
&--active { color: var(--color-primary); background: var(--color-primary-light); }
&--disabled { color: var(--color-text-placeholder); background: var(--color-bg-tertiary); }
}
}
Part 5: 页面模板
列表页面模板
<template>
<view class="page">
<!-- 搜索栏 -->
<view class="search-bar">
<wd-search v-model="keyword" placeholder="搜索" @search="handleSearch" />
</view>
<!-- 筛选标签 -->
<view class="filter-tabs">
<wd-tabs v-model="activeTab" @change="handleTabChange">
<wd-tab name="all" title="全部" />
<wd-tab name="enabled" title="启用" />
<wd-tab name="disabled" title="禁用" />
</wd-tabs>
</view>
<!-- 列表 -->
<scroll-view
class="list-container"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleLoadMore"
>
<view v-for="item in list" :key="item.id" class="list-item">
<user-card :user="item" @edit="handleEdit" @delete="handleDelete" />
</view>
<wd-loadmore :state="loadState" />
</scroll-view>
<!-- 悬浮按钮 -->
<wd-fab icon="add" @click="handleAdd" />
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import type { UserVO } from "@/api/system/user/types";
const keyword = ref("");
const activeTab = ref("all");
const list = ref<UserVO[]>([]);
const loading = ref(false);
const refreshing = ref(false);
const hasMore = ref(true);
const pageNum = ref(1);
const loadState = computed(() => {
if (loading.value) return "loading";
if (!hasMore.value) return "finished";
return "loading";
});
async function fetchList(isRefresh = false) {
if (loading.value) return;
loading.value = true;
if (isRefresh) {
pageNum.value = 1;
list.value = [];
}
try {
const res = await UserAPI.getPage({
keyword: keyword.value,
status: activeTab.value,
pageNum: pageNum.value,
});
list.value = isRefresh ? res.data.list : [...list.value, ...res.data.list];
hasMore.value = res.data.list.length >= 20;
} finally {
loading.value = false;
refreshing.value = false;
}
}
function handleSearch() {
fetchList(true);
}
function handleTabChange() {
fetchList(true);
}
function handleRefresh() {
refreshing.value = true;
fetchList(true);
}
function handleLoadMore() {
if (hasMore.value) {
pageNum.value++;
fetchList();
}
}
function handleAdd() {
uni.navigateTo({ url: "/pages/work/user/edit" });
}
function handleEdit(id: number) {
uni.navigateTo({ url: `/pages/work/user/edit?id=${id}` });
}
async function handleDelete(id: number) {
const { confirm } = await uni.showModal({
title: "确认删除?",
content: "删除后不可恢复",
});
if (confirm) {
await UserAPI.deleteById(id);
uni.showToast({ title: "删除成功", icon: "success" });
fetchList(true);
}
}
onLoad(() => fetchList());
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background-color: var(--color-bg-secondary);
}
.search-bar {
padding: 24rpx;
background-color: var(--color-bg);
}
.filter-tabs {
background-color: var(--color-bg);
}
.list-container {
height: calc(100vh - 200rpx);
}
</style>
Part 6: 卡片设计规范
信息层级原则
移动端卡片 ≠ Web 表格
| 层级 | 内容 | 视觉权重 | 示例 |
|---|
| 主信息 | 身份识别 | 最突出 | 头像 + 用户名 + 状态 |
| 次信息 | 身份定位 | 中等 | 角色 + 部门 |
| 辅助信息 | 联系方式 | 较弱 | 手机号 + 邮箱 |
| 元信息 | 时间戳 | 最弱 | 创建时间 |
卡片模板
<template>
<view class="user-card">
<!-- 主信息行 -->
<view class="user-card__header">
<wd-img :src="user.avatar" width="80rpx" height="80rpx" round />
<view class="user-card__identity">
<text class="user-card__name">{{ user.nickname }}</text>
<text class="user-card__role">{{ user.roleName }} · {{ user.deptName }}</text>
</view>
<wd-tag :type="user.status === 1 ? 'success' : 'danger'" plain>
{{ user.status === 1 ? "正常" : "禁用" }}
</wd-tag>
</view>
<!-- 辅助信息行 -->
<view class="user-card__contact">
<text>📱 {{ user.mobile }}</text>
<text v-if="user.email">📧 {{ user.email }}</text>
</view>
<!-- 操作按钮 -->
<view class="user-card__actions" @click.stop="showActions">
<wd-icon name="more" />
</view>
</view>
</template>
<script setup lang="ts">
import type { UserVO } from "@/api/system/user/types";
defineProps<{ user: UserVO }>();
const emit = defineEmits<{ edit: [id: number]; delete: [id: number] }>();
function showActions() {
uni.showActionSheet({
itemList: ["编辑", "删除"],
success: ({ tapIndex }) => {
if (tapIndex === 0) emit("edit", props.user.id);
if (tapIndex === 1) emit("delete", props.user.id);
},
});
}
</script>
<style lang="scss" scoped>
.user-card {
position: relative;
padding: 24rpx;
background: var(--color-bg);
border-radius: 16rpx;
&__header {
display: flex;
align-items: center;
gap: 16rpx;
}
&__identity {
flex: 1;
min-width: 0;
}
&__name {
display: block;
font-size: 28rpx;
font-weight: 600;
color: var(--color-text);
}
&__role {
display: block;
font-size: 24rpx;
color: var(--color-text-secondary);
margin-top: 4rpx;
}
&__contact {
display: flex;
gap: 24rpx;
margin-top: 12rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
}
&__actions {
position: absolute;
right: 24rpx;
bottom: 24rpx;
padding: 16rpx;
}
}
</style>
卡片规则
- 控制在 3-4 行以内
- 次信息用
· 分隔
- 辅助信息用图标前缀:📱📧
- 操作轻量化:
··· 更多按钮
Part 7: 主题变量系统
变量速查
主题色:
| 语义 | Light | Dark | 变量 |
|---|
| 主色 | #4d80f0 | #3b82f6 | --color-primary |
| 主色浅 | #e8f0fe | #1e3a5f | --color-primary-light |
| 主色深 | #2563eb | #60a5fa | --color-primary-dark |
| 成功 | #34d19d | #34d399 | --color-success |
| 成功浅 | #e6f7f1 | #064e3b | --color-success-light |
| 警告 | #f0883a | #fbbf24 | --color-warning |
| 警告浅 | #fff4e6 | #78350f | --color-warning-light |
| 危险 | #ff4757 | #f87171 | --color-danger |
| 危险浅 | #fff1f2 | #7f1d1d | --color-danger-light |
背景色:
| 语义 | Light | Dark | 变量 |
|---|
| 主背景 | #ffffff | #1f2937 | --color-bg |
| 次背景 | #f5f5f5 | #111827 | --color-bg-secondary |
| 三级背景 | #f1f5f9 | #1e293b | --color-bg-tertiary |
文字色:
| 语义 | Light | Dark | 变量 |
|---|
| 主文字 | #1f2937 | #f9fafb | --color-text |
| 次文字 | #6b7280 | #9ca3af | --color-text-secondary |
| 占位文字 | #9ca3af | #6b7280 | --color-text-placeholder |
| 禁用文字 | #d1d5db | #4b5563 | --color-text-disabled |
边框色:
| 语义 | Light | Dark | 变量 |
|---|
| 默认 | #e5e7eb | #374151 | --color-border |
| 浅色 | #f3f4f6 | #1f2937 | --color-border-light |
使用原则
.card { background: var(--color-bg); border: 1rpx solid var(--color-border); }
.card { background: #ffffff; border: 1rpx solid #e5e7eb; }
Wot Design 变量桥接
项目在 theme.scss 中已桥接 Wot Design 变量:
--wot-color-theme: var(--color-primary);
--wot-color-success: var(--color-success);
--wot-color-warning: var(--color-warning);
--wot-color-danger: var(--color-danger);
--wot-color-bg: var(--color-bg);
--wot-color-text: var(--color-text);
--wot-color-text-secondary: var(--color-text-secondary);
--wot-color-text-placeholder: var(--color-text-placeholder);
--wot-color-border: var(--color-border);
z-index 层级
只有 position: fixed/sticky/absolute 且存在层叠竞争的元素才设 z-index,禁止魔法数字。
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-popup: 400;
--z-dialog: 500;
--z-toast: 600;
--z-navbar: 700;
--z-fab: 800;
间距系统
项目在 theme.scss 中定义了语义间距变量:
| 语义 | 值 | 场景 |
|---|
| xxs | 4rpx | 极小间距 |
| xs | 8rpx | 图标与文字 |
| sm | 16rpx | 模块间分隔 |
| md | 24rpx | 卡片内边距 |
| lg | 32rpx | 页面左右边距 |
| xl | 40rpx | 大区块内边距 |
| xxl | 48rpx | 大区块分隔 |
语义别名:--spacing-page: 32rpx、--spacing-card: 24rpx、--spacing-section: 24rpx、--spacing-element: 16rpx、--spacing-compact: 8rpx
圆角
| 场景 | 值 |
|---|
| 小元素(按钮、标签) | 8rpx |
| 中元素(输入框) | 16rpx |
| 大卡片 | 24rpx |
| 圆形 | 50% |
阴影
| 级别 | Light | Dark |
|---|
| sm | 0 2rpx 4rpx rgba(0,0,0,0.05) | 0 2rpx 4rpx rgba(0,0,0,0.2) |
| md | 0 8rpx 12rpx rgba(0,0,0,0.1) | 0 8rpx 12rpx rgba(0,0,0,0.3) |
| lg | 0 20rpx 30rpx rgba(0,0,0,0.1) | 0 20rpx 30rpx rgba(0,0,0,0.4) |
字体
| 场景 | 字号 | 字重 | 行高 |
|---|
| 辅助信息 | 24rpx | 400 | 1.4 |
| 次要文字 | 26rpx | 400 | 1.5 |
| 正文 | 28rpx | 400 | 1.6 |
| 小标题 | 30rpx | 600 | 1.4 |
| 标题 | 32rpx | 600 | 1.3 |
| 大标题 | 36rpx | 700 | 1.2 |
UnoCSS Shortcuts
| 快捷类 | 展开 | 用途 |
|---|
flex-center | flex justify-center items-center | 居中 |
flex-between | flex justify-between items-center | 两端对齐 |
flex-start | flex justify-start items-center | 起始对齐 |
flex-col-center | flex flex-col items-center | 纵向居中 |
新增 Shortcut 条件:全局出现 ≥ 5 次 + 有明确语义 + 不含业务色值。
Part 8: rpx 单位规范
| 场景 | 字号 | 说明 |
|---|
| 辅助信息 | 24rpx | 描述、标签、页脚 |
| 按钮/次要 | 26rpx | 次要操作、筛选按钮 |
| 基础字号 | 28rpx | 正文、列表项 |
| 标题 | 32rpx | 页面标题、重要信息 |
- 间距:4 的倍数
16rpx、24rpx、32rpx
Part 9: 小程序兼容性
WXSS 限制
| 特性 | 状态 | 替代 |
|---|
:deep() | 编译后残留,WXSS 报错 | 组件 custom-class + 非 scoped style |
[attr*="val"] | 不支持 | 具体类名 |
:has() / :is() | 不支持 | 具体选择器 |
::before / ::after | 小程序不支持 | 真实元素 |
覆盖组件样式
<!-- ✅ custom-class + 非 scoped 覆盖组件样式 -->
<wd-card custom-class="my-card">
<template #title>标题</template>
</wd-card>
<style lang="scss">
.my-card { margin: 0 !important; }
.my-card .wd-card__body { padding: 24rpx !important; }
</style>
避免使用伪元素
小程序不支持 ::before、::after,改用真实元素:
<!-- ❌ 错误:使用伪元素 -->
<style>
.hero::after {
content: "";
position: absolute;
/* ... */
}
</style>
<!-- ✅ 正确:使用真实元素 -->
<template>
<view class="hero">
<view class="hero-fade"></view>
</view>
</template>
避免使用 DOM API
小程序不支持 document、window,使用 UniApp API:
document.getElementById("app");
uni.createSelectorQuery().select("#app");
Part 10: 反模式
| 反模式 | 正确做法 |
|---|
| 硬编码颜色值 | 使用 var(--color-*) CSS 变量 |
| 普通流内容设 z-index | 只有定位元素才设 |
::deep([attr*="val"]) | 使用具体类名 |
Block 名过于通用(如 .card) | 使用页面/功能前缀(如 .mine-card) |
height: 100vh | 使用 min-height: 100vh |
| z-index 魔法数字 | 使用 var(--z-*) |
SCSS 大量 @apply | 原子类留在模板 |
| AI 蓝紫渐变 | 纯色或品牌色 |
| Emoji 当图标 | SVG 图标或 Wot Design Icon |
| 过度装饰(阴影+渐变+动画) | 最多一个装饰效果 |
| 同一元素原子类 >3 个 | 归入 BEM 类 |
Part 11: 代码质量检查清单