本规范适用于 forge-admin-ui 的 Vue 3 页面、组件和接口代码。项目级约束以仓库根目录 AGENTS.md 为准;本文件补充前端实现细则。
<script setup>,不新增 Options API 页面。pnpm。前端校验优先执行:source ~/.nvm/nvm.sh && nvm use v20.19.0
pnpm exec eslint <changed-files>
pnpm build
src/
├── api/ # 按业务域定义接口请求
├── components/ # 可跨页面复用的展示与交互组件
│ ├── ai-form/ # AiCrudPage、AiForm、AiTable 等配置化能力
│ ├── common/ # 通用工作台、树、选择器、鉴权图片等
│ └── lowcode-builder/ # 低代码设计、预览和协议解释器
├── composables/ # useXxx 组合式状态与行为
├── config/ # 应用级静态配置
├── layouts/ # 应用导航、页签和整体壳层
├── router/ # 路由与动态路由装配
├── stores/ # Pinia 状态;跨页面且有明确生命周期的数据才放这里
├── styles/ # 全局变量、重置和主题样式
├── utils/ # 无 UI 的纯工具、请求、加密、文件和校验能力
└── views/ # 路由页面,按业务域组织,例如 system/、flow/、ai/
文件应放在最小且正确的作用域:只服务一个页面的子组件放在该页面同级 components/;可被两个以上业务域使用的组件放入 src/components/。
/system/** 路由会由应用级 SystemPageLayout 提供统一的满高和滚动边界。页面工作区外边距由各 Layout 统一提供 8px;组件内部不负责页面级外边距。
普通 CRUD 页面根节点保持最小:
<template>
<div class="system-config-page">
<AiCrudPage ... />
</div>
</template>
当左侧树、角色、分类等对象决定右侧数据时,使用 MasterDetailWorkspace,不再分别制作两个独立卡片。默认使用连体模式:一条外框、内部细分隔线,视觉上是同一个工作台。
<MasterDetailWorkspace
:collapsed="collapsed"
:aside-width="220"
:collapsed-aside-width="72"
>
<template #aside>
<OrgTreePanel />
</template>
<AiCrudPage ... />
</MasterDetailWorkspace>
aside 插槽放树、对象列表或筛选导航;默认插槽放表格、详情或页签工作区。:attached="false"。min-height: 0 并在内容区滚动,避免整页和表格双重滚动。| 场景 | 首选组件/能力 | 约定 |
|---|---|---|
| 标准列表、新增、编辑、删除、导入导出 | AiCrudPage |
API 占位符使用 :id,分页使用 pageNum、pageSize |
| 配置化表单 | AiForm |
Schema 只描述字段与行为,不在页面重复实现控件渲染 |
| 表格 | AiTable / AiCrudPage 内置表格 |
默认密度为 medium;需要切换时使用紧凑/默认/宽松语义 |
| 字典下拉与回显 | DictSelect、DictTag、useDict() |
禁止硬编码业务枚举和状态标签 |
| 行政区划 | RegionTreeSelect |
不自行复制区划树转换逻辑 |
| 鉴权图片 | AuthImage |
文件字段保存的是 fileId,不是可直接使用的 URL |
| 左树右表、主从页 | MasterDetailWorkspace |
左侧和右侧通过插槽组织 |
| 页面级统一边界 | SystemPageLayout |
/system/** 已自动接入,无需手工包裹 |
| 用户、组织、租户等实体单元格 | SystemTableCell |
实体使用主标题 + 换行辅助标识;用户可提供详情入口和首字标识,多值归属显示主值与可展开 +N |
新公共组件应明确:输入 Props、输出事件、插槽职责、空态/加载态和键盘可访问性。图标按钮必须有 title 或 aria-label。
AiCrudPage 只负责搜索、工具栏、表格和表单业务区域;不要在组件内加入页面级 margin、padding 或外框。外边距统一由当前 Layout 提供,主从工作台的外框由 MasterDetailWorkspace 提供。
| 对象 | 规则 | 示例 |
|---|---|---|
| Vue 组件文件与组件名 | PascalCase.vue |
MasterDetailWorkspace.vue |
| 页面文件 | 新页面用 kebab-case.vue;历史文件不为统一命名而改路由 |
storage-config.vue |
| 组合式函数 | use + PascalCase,文件同名 |
useDict.js、usePermission.js |
| Pinia Store | use + 领域 + Store |
useUserStore |
| 普通函数/变量 | camelCase |
loadUserList、selectedOrgNode |
| 常量 | UPPER_SNAKE_CASE |
USER_STATUS_DICT |
| CSS 类 | 业务/组件前缀 + kebab-case |
master-detail-workspace__aside |
| 事件 | 动词开头的 kebab-case | @selection-change、@submit-success |
避免含义宽泛的 data、list、handleClick。使用能表达领域和动作的名字,例如 tenantOptions、handleOrgNodeSelect。
src/api/<domain>.js,页面只调用领域接口或 AiCrudPage 配置,不直接散落 Axios 配置。request;与后端 @ApiDecrypt 对应的敏感提交使用 postEncrypt。GET /page、GET /:id、POST /、PUT /、DELETE /:id 语义;配置化 API 路径占位符用 :id,不用 {id}。useDict('<dict_type>') 获取。Schema 中的 options 用 computed 派生,确保异步加载后可回显。ref;需要跨路由共享、可恢复或全局可见时才放 Pinia。--primary-color、--bg-primary、--border-light、--text-primary、--text-tertiary。text-primary,详情 text-info,警告 text-warning,删除 text-error,成功操作 text-success。AiCrudPage 再套大圆角白色卡片。color、background、border、opacity、transform,时长控制在 120–180ms;不要动画宽高或位置。git diff --check,确认没有空白错误。pnpm exec eslint <files>。pnpm build。AiCrudPage、字典、文件或低代码协议时,至少确认相关页面的默认值、回显、空态与窄屏行为。.env.local、密钥、Token、真实用户数据或无关构建产物。单文件组件按以下顺序组织,保持同一类组件易读、易审查:
<template>
<!-- 页面结构或组件结构 -->
</template>
<script setup>
// 1. 第三方与项目 import
// 2. defineOptions / defineProps / defineEmits
// 3. refs 与静态常量
// 4. computed 与 watch
// 5. 数据加载、事件处理、辅助函数
// 6. 生命周期
</script>
<style scoped>
/* 当前组件样式 */
</style>
@/ 项目路径 → 相对路径」分组;删除未使用 import。defineOptions({ name: '...' }),名称与组件文件一致。submit-success、selection-change。computed、resolveXxx 或 isXxx 函数。v-for 必须使用稳定的 :key,禁止用数组下标作为可排序、可编辑列表的 key。window.$message 或页面内错误态说明。| 状态类型 | 放置位置 | 示例 |
|---|---|---|
| 单个控件、弹窗、筛选值 | 当前组件 ref |
modalVisible、selectedOrgId |
| 基于当前状态推导的展示值 | computed |
filteredRows、canSubmit |
| 可复用请求/交互逻辑 | src/composables/useXxx |
useDict、usePermission |
| 登录态、主题、跨页会话 | Pinia Store | useUserStore、useAppStore |
| 不随渲染变化的映射/常量 | 模块顶层 const |
USER_STATUS_DICT |
watch 只处理副作用(重新请求、同步外部值、清理资源),不能替代本应使用的 computed。header、main、section、aside、nav、article,不要只堆叠 div。button,或补齐 role、tabindex、键盘 Enter/Space 行为。title 或 aria-label;图片必须说明 alt,装饰图片才可空 alt。computed 生成,特别是依赖字典、权限或上下文的 options。field 与后端 DTO 字段保持一致;展示 label 可以改中文,字段编码不随意改名。beforeRenderForm、beforeRenderDetail 或字段权限配置,禁止复制三套表单。editSchema,保证字典、枚举和敏感字段按业务规则展示;AiCrudPage 会在遗漏 Schema 时按可见表格列生成只读兜底详情,防止空白弹窗,但不能替代正式 Schema。beforeSubmit;返回 false 时明确阻止提交,异步逻辑必须 await。medium,仅高密度审计、日志或对比场景使用 small;不要把整站默认设为紧凑。columns 只承载列配置与必要渲染函数;超过三段逻辑的单元格渲染抽为小组件。SystemTableCell 的主标题 + 辅助标识;关联关系显示主值与可展开 +N;枚举状态才使用 DictTag;普通属性保持纯文字。不要在每个单元格叠加图标、色块和 Tag。@username 作为辅助标识;主标题区域可点击查看详情。性别等低频属性保留在详情或列设置,不挤占主列表。pageNum、pageSize;远端排序、筛选参数必须与后端接口定义一致。AuthImage 渲染;下载链接通过 getFileUrl(fileId) 获取。src/views/<domain>/,目录和路由业务域一致;动态路由由现有路由装配机制生成,不手写冲突路径。usePermission 或权限指令判断展示;不能仅依赖前端隐藏来保护操作。KeepAlive 页面需要处理路由参数切换、激活后的刷新与资源释放,不能假设 onMounted 只会运行一次。@/utils/request 或现有 API 模块;禁止在组件内新建 Axios 实例。fetchUserPage、createUser、updateUser、removeUsers。params,JSON 提交用 data;不要把对象序列化后拼进 URL。fileId,表单与接口不要把临时 URL 当作持久业务值。<style scoped>。src/styles/。!important 覆盖正常组件样式;确需覆盖第三方内部节点时局部使用 :deep() 并写明原因。min-width: 0、min-height: 0 与溢出边界。p-12 等大留白;所有 Layout 内容区统一提供 8px 工作区边距,流程画布等 flush 页面保持 0。| 变更 | 必做验证 |
|---|---|
| Vue、JS、CSS | 目标文件 ESLint + pnpm build |
| CRUD 页面 | 列表、查询、重置、分页、增改删、空态 |
| 字典字段 | 异步加载、下拉回显、表格标签回显 |
| 文件字段 | 上传、编辑回显、鉴权预览、下载 |
| 主从工作台 | 左侧选择、收起、右侧刷新、窄屏堆叠 |
| 权限操作 | 无权限隐藏/禁用、接口失败提示、权限变更后刷新 |
<script setup>
import { computed } from 'vue'
import DictTag from '@/components/DictTag.vue'
import { useDict } from '@/composables/useDict'
const { dict } = useDict('sys_user_status')
const userStatusOptions = computed(() => dict.value.sys_user_status || [])
</script>
<template>
<DictTag :options="userStatusOptions" :value="row.userStatus" />
</template>
<AiCrudPage
:api-config="{
list: 'get@/system/user/page',
detail: 'get@/system/user/:id',
delete: 'delete@/system/user/:id',
}"
/>
不要使用 /system/user/{id};组件只可靠识别冒号占位符。