원클릭으로
cube-add-page
根据后端 NewLife.Cube 控制器定义,在前端项目创建页面组件。当用户说"新增页面"、"创建页面"、"添加功能页面"、"给 XX 模块加页面"时使用。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
根据后端 NewLife.Cube 控制器定义,在前端项目创建页面组件。当用户说"新增页面"、"创建页面"、"添加功能页面"、"给 XX 模块加页面"时使用。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
初始化一个新的前端项目,使用 cube-front 框架。 当用户说"初始化项目"、"创建新项目"、"搭建前端项目"、"使用 cube-front 初始化"时使用此技能。 自动配置布局、状态管理(Pinia)、路由、API请求库、多语言支持、容器化部署等核心功能,开箱即用。
在 cube-front 框架中新增、注册或切换页面布局。 当用户说"新增布局"、"自定义布局"、"修改布局"、"切换布局"、"使用 XXX 布局"时使用。 包含布局组件创建、框架注册、应用使用的完整流程。
LOV(List of Values)值集系统使用指南。涵盖后端配置、前端组件、API 封装、枚举自动注册全链路。当用户说"配置值集"、"使用LovSelect"、"添加LOV"、"值集选择"、"枚举下拉"、"使用LOV"时使用。
命令式弹窗的代码组织规范与最佳实践。 当用户问"弹窗怎么组织"、"弹窗放哪"、"弹窗目录结构"、"弹窗代码规范"、"新增弹窗怎么写"、"重构弹窗结构"时使用。 核心原则:Vue 组件 + openXxx.ts 打开函数作为"一对"放在一起,按业务归属就近放置。
(已迁移) 此技能已更名为 cube-add-page,请使用 cube-add-page 技能根据控制器创建前端页面。当你需要创建或修改 Cube 前端 API 文件时也请使用 cube-add-page。
在 cube-front 微前端架构中新增一个子应用/模块。 当用户说"新增应用"、"创建子应用"、"添加新模块"、"新建页面"、"创建新页面"时使用。 注意:新增的应用放在调用技能的目录(项目根目录 apps/),而不是 cube-front 目录。
| name | cube-add-page |
| description | 根据后端 NewLife.Cube 控制器定义,在前端项目创建页面组件。当用户说"新增页面"、"创建页面"、"添加功能页面"、"给 XX 模块加页面"时使用。 |
Cube 前端新增页面技能。根据后端控制器定义,在前端对应应用目录创建页面组件,并配置菜单图标、列表展示字段等。
只需创建页面组件文件,不需要关心页面如何注册和渲染。 本框架会自动加载页面、自动注册路由,无需手工配置。
当需要新增或修改前端页面时使用,例如:
{前端项目}/apps/{app-name}/)cube-add-app 技能创建应用| 参数 | 说明 | 示例 |
|---|---|---|
controllerName | 控制器名称(PascalCase) | Product, Device, Alarm |
area | 业务区域(与后端 Area 名一致) | Basic, Device, Demo |
entityName | 实体名称(可选,默认同 controllerName) | Product |
menuIcon | 菜单图标(Element Plus 图标名) | Files, Setting, User |
listFields | 列表页要展示的字段数组(可选,默认全部) | ["Id", "Name", "Code", "Enable"] |
detailUrl | 详情页 URL 模板(可选) | "/{area}/{controller}/Detail?id={Id}" |
每个页面在前端应用目录下创建(路径规则:{area}/{controller}/index.vue):
{前端项目}/apps/{app-name}/
└── src/
└── views/
└── {area}/
└── {controller}/
└── index.vue ← 唯一需要创建的文件
框架自动完成: 路由注册、菜单加载、侧边栏渲染均由框架自动处理,不要手工注册路由。
EntityController<TEntity>AreaBase,构造函数传入 Area 名,基类自动调用 RegisterArea)apps/{app-name}/),不存在则调用 cube-add-app在后端 Controller 上通过 [Menu] 特性设置图标:
[Menu(30, true, Icon = "Files")] // Icon 为 Element Plus 图标名
public class DemoController : EntityController<DemoEntity>
图标设置后可通过 Cube 后台管理 → 菜单管理 → 修改 Icon 字段覆盖。
初始图标推荐对应功能的 Element Plus 图标名,如
Files、Setting、User、List、Document、DataBoard、Coin、Clock等。
在 Controller 静态构造函数中配置 ListFields:
static ProductController()
{
// 1. 清空默认字段,只保留需要的
var list = ListFields;
list.Clear();
var allows = new[] { "Id", "Name", "Code", "Category", "Enable", "Version", "UpdateTime" };
foreach (var item in allows)
list.AddListField(item);
// 2. 设置名称列的详情链接
var df = ListFields.GetField("Name") as ListField;
df.Url = "/{area}/{controller}/Detail?id={Id}";
df.Target = "_blank";
// 3. 添加自定义操作列(如日志)
var logField = ListFields.AddListField("Log", "UpdateTime");
logField.DisplayName = "日志";
logField.Url = "/Admin/Log?category={实体名}&linkId={Id}";
}
常用 ListFields 方法:
| 方法 | 说明 | 示例 |
|---|---|---|
list.Clear() | 清空所有字段 | list.Clear() |
list.AddListField(field) | 添加字段 | list.AddListField("Name") |
ListFields.RemoveField(fields) | 移除字段 | ListFields.RemoveField("Creator", "Updater") |
ListFields.RemoveCreateField() | 移除创建审计字段 | ListFields.RemoveCreateField() |
ListFields.RemoveUpdateField() | 移除更新审计字段 | ListFields.RemoveUpdateField() |
ListFields.GetField(name) | 获取字段引用 | ListFields.GetField("Name") as ListField |
ListFields.AddListField(name, afterField) | 在指定字段后追加 | ListFields.AddListField("Log", "UpdateTime") |
ListField 常用属性:
| 属性 | 说明 | 示例 |
|---|---|---|
Url | 点击跳转链接 | "/Area/Controller/Detail?id={Id}" |
Target | 链接打开方式 | "_blank" / "_frame" |
DisplayName | 显示名称 | df.DisplayName = "操作" |
Align | 对齐方式 | df.Align = "center" |
Width | 列宽 | df.Width = 200 |
Header | 表头标题 | df.Header = "名称" |
本框架路由和 CRUD 由后端自动驱动。 你的唯一任务就是创建页面组件文件。
若只需默认的表格列表 + 弹窗新增/编辑,无需创建任何前端文件,后端 Controller 创建完成后刷新即可访问。
若需要自定义页面(非标准 CRUD 布局、自定义交互、看板、图表等),只需在以下路径创建 index.vue:
apps/{app-name}/src/views/{area}/{controller}/index.vue
创建此文件后,框架会自动加载并渲染此组件,无需做以下任何操作:
/{area}/{controller}/{action}/{id?})routes.ts 或 routes/index.ts[Menu] 特性控制)main.ts创建自定义页面前,必须先提供原型参考:
根据原型直接实现为 Vue 组件即可。
创建自定义页面时,所有样式必须使用 Element Plus CSS token(--el-*)或 Cube Layout token(--cube-layout-*),禁止硬编码色值、自定义 CSS 变量或第三方 token 体系。
✅ 正确写法(使用 --el-*):
.search-bar {
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-base);
padding: 12px 16px;
color: var(--el-text-color-primary);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
margin: 0;
}
❌ 错误写法(硬编码/自定义 token):
.search-bar {
background: #ffffff; /* 硬编码 */
border: 1px solid #ebeef5; /* 硬编码 */
color: #303133; /* 硬编码 */
}
.sidebar { background: var(--bg-primary); } /* 已废弃的自定义 token */
常用 token 速查:
| 语义 | token | 用途 |
|---|---|---|
| 背景色 | var(--el-bg-color) | 页面主体背景 |
| 卡片/浮层面板背景 | var(--el-bg-color-overlay) | 弹窗、卡片、下拉面板 |
| 填充色 | var(--el-fill-color-light) | 输入框背景、搜索栏背景 |
| 一级文字色 | var(--el-text-color-primary) | 标题、正文 |
| 二级文字色 | var(--el-text-color-regular) | 次要信息 |
| 三级文字色 | var(--el-text-color-secondary) | 提示文字、占位符 |
| 边框色 | var(--el-border-color) | 表格、卡片边框 |
| 浅边框色 | var(--el-border-color-light) | 分割线、搜索栏边框 |
| 主色 | var(--el-color-primary) | 按钮、链接、激活态 |
| 成功色 | var(--el-color-success) | 成功状态 |
| 警告色 | var(--el-color-warning) | 警告状态 |
| 危险色 | var(--el-color-danger) | 错误、删除 |
| 圆角 | var(--el-border-radius-base) | 卡片、弹窗圆角 |
| 小圆角 | var(--el-border-radius-small) | 按钮、输入框圆角 |
| 浅阴影 | var(--el-box-shadow-light) | 卡片阴影 |
| 深阴影 | var(--el-box-shadow) | 下拉面板、弹窗阴影 |
| 侧边栏宽度 | var(--cube-layout-sidebar-width) | 布局结构 |
| 导航栏高度 | var(--cube-layout-nav-height) | 布局结构 |
| 内容区域内边距 | var(--cube-layout-content-padding) | 布局结构 |
自定义页面中可通过 usePageApi(area, controller) composable 快速对接后端 CRUD API,无需为每个模块手写请求逻辑。
前提:前端项目需在
package.json中添加"@cube/api-core": "workspace:*"依赖,根目录pnpm-workspace.yaml包含- "Cube/packages/*"。
框架提供全局 cubeApi 实例(位于 src/cubeApi.ts),基于 @cube/api-core 的 createCubeApi() 创建,自动处理 Token 注入、401 跳转、响应拦截、字段名归一化等。
import { createCubeApi } from '@cube/api-core';
const cubeApi = createCubeApi({
baseURL: import.meta.env.DEV ? '/base-api' : (import.meta.env.VITE_API_URL ?? ''),
onUnauthorized: () => { window.location.href = '/'; },
});
通过 usePageApi(area, controller) 创建页面级 API 对象,内置路径拼接逻辑:
| 方法 | 功能 | 对应后端接口 |
|---|---|---|
getPage() | 获取页面元数据(字段配置、页面设置) | GET /{area}/{controller}/GetPage |
getFields(kind) | 获取指定类型的字段列表 | GET /{area}/{controller}/GetFields |
getList(params) | 分页列表查询 | GET /{area}/{controller} |
getDetail(id) | 查看详情 | GET /{area}/{controller}/Detail |
add(data) | 新增 | POST /{area}/{controller} |
update(data) | 编辑 | PUT /{area}/{controller} |
remove(id) | 删除单条 | DELETE /{area}/{controller} |
deleteSelect(keys) | 批量删除 | DELETE /{area}/{controller} |
uploadFile(file) | 上传文件 | POST /{area}/{controller}/UploadFile |
importFile(file) | 导入文件 | POST /{area}/{controller}/ImportFile |
getExportUrl(format) | 获取导出下载 URL | 直接返回 URL |
getChartData() | 获取图表数据 | GET /{area}/{controller}/GetChartData |
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { usePageApi } from "@/composables/usePageApi";
// 传入区域和控制器名,即得完整 CRUD(路径自动拼接为 /{Area}/{Controller})
const api = usePageApi("Demo", "Demo");
const list = ref<Record<string, unknown>[]>([]);
const loading = ref(false);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
/** 获取分页列表 */
async function fetchList() {
loading.value = true;
try {
const res = await api.getList({
pageIndex: currentPage.value - 1,
pageSize: pageSize.value,
});
list.value = res.data ?? [];
total.value = res.page?.totalCount ?? 0;
} finally {
loading.value = false;
}
}
/** 新增 */
async function handleAdd(data: Record<string, unknown>) {
await api.add(data);
ElMessage.success("新增成功");
await fetchList();
}
/** 编辑 */
async function handleEdit(data: Record<string, unknown>) {
await api.update(data);
ElMessage.success("更新成功");
await fetchList();
}
/** 删除(带确认弹窗) */
async function handleDelete(row: Record<string, unknown>) {
try {
await ElMessageBox.confirm(
`确定要删除 "${row.name ?? row.Name}" 吗?`,
"确认删除",
{ confirmButtonText: "删除", cancelButtonText: "取消", type: "warning" }
);
const id = (row.id ?? row.Id) as number;
await api.remove(id);
ElMessage.success("删除成功");
await fetchList();
} catch (err: any) {
if (err !== "cancel") ElMessage.error(err?.message || "删除失败");
}
}
onMounted(() => fetchList());
</script>
关键设计:
usePageApicomposable 是唯一的 API 入口,所有自定义页面都通过它调用后端。这样既不需要为每个模块创建api/xxx.ts文件,也保持了统一的错误处理和 Token 管理。
/{area}/{controller},无需手动输入路由配置namespace MyApp.Web.Areas.Demo.Controllers;
[Menu(30, true, Icon = "Files")] // 菜单图标
[DemoArea] // Area 注册
public class DemoController : EntityController<DemoEntity>
{
static DemoController()
{
// 列表展示字段
var list = ListFields;
list.Clear();
var allows = new[] { "Id", "Name", "Code", "Category", "Enable", "Version", "UpdateTime" };
foreach (var item in allows)
list.AddListField(item);
// 名称列跳转详情
var df = ListFields.GetField("Name") as ListField;
df.Url = "/Demo/Demo/Detail?id={Id}";
df.Target = "_blank";
// 日志列
var logField = ListFields.AddListField("Log", "UpdateTime");
logField.DisplayName = "日志";
logField.Url = "/Admin/Log?category=示例&linkId={Id}";
}
}
只需在 apps/{app-name}/src/views/{area}/{controller}/index.vue 创建 Vue 组件,框架自动加载。
<template>
<div>
<el-table :data="list" v-loading="loading" stripe>
<el-table-column prop="name" label="名称" />
<el-table-column prop="code" label="编码" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { usePageApi } from "@/composables/usePageApi";
const api = usePageApi("Demo", "Demo");
const list = ref<Record<string, unknown>[]>([]);
const loading = ref(false);
async function fetchList() {
loading.value = true;
try {
const res = await api.getList({ pageIndex: 0, pageSize: 20 });
list.value = res.data ?? [];
} finally {
loading.value = false;
}
}
async function handleEdit(row: Record<string, unknown>) {
await api.update(row);
ElMessage.success("更新成功");
await fetchList();
}
async function handleDelete(row: Record<string, unknown>) {
await api.remove(row.id as number);
ElMessage.success("删除成功");
await fetchList();
}
onMounted(() => fetchList());
</script>
| 项 | 位置 | 说明 |
|---|---|---|
| 菜单图标 | Controller [Menu(Icon = "Files")] | 使用 Element Plus 图标名 |
| 列表字段 | Controller 静态构造函数 ListFields | 增删改字段及配置链接 |
| API 对接 | 页面内 usePageApi(area, controller) | 传入区域+控制器名即得完整 CRUD,无需手写请求 |
| 页面组件 | apps/{app-name}/src/views/{area}/{controller}/index.vue | 默认 CRUD 无需创建,自定义页面需提供原型 |
| 路由 | 框架自动注册,不要手工配置 | 无需关心 |
Files、Setting、User、DataBoard、Coin),不可用 fa- 旧格式[XxxArea] 特性(即 Area 类名),Area 类继承 AreaBase 即可,基类构造函数自动注册,无需手动调用 RegisterArearoutes.ts 中写路由,不要修改 main.ts,只需创建 views 目录下的 index.vueapps/*/src/views/{area}/{controller}/index.vue 并自动匹配后端菜单路由index.vue 文件,刷新浏览器即可看到效果usePageApi(area, controller) 对接后端,该 composable 包装了 @cube/api-core 的通用 CRUD 方法,不需要为每个模块建 api/xxx.ts 文件getList 需传 pageIndex: page - 1;totalCount 在 res.page.totalCount 中| 场景 | 代码 |
|---|---|
| 移除审计字段 | ListFields.RemoveCreateField().RemoveUpdateField() |
| 移除指定字段 | ListFields.RemoveField("CreatorId", "UpdaterId") |
| 清空并自定义 | list.Clear(); foreach(...) list.AddListField(item) |
| 添加链接列 | var df = ListFields.AddListField(name, afterField) |
| 设置详情 URL | df.Url = "/{area}/{controller}/Detail?id={Id}" |
| 设置目标窗口 | df.Target = "_blank" |
| 场景 | 代码 |
|---|---|
| 移除审计字段 | AddFormFields.RemoveCreateField().RemoveUpdateField() |
| 移除指定字段 | EditFormFields.RemoveField("Remark", "CreatorId") |
Cube 框架的自动表格和下拉选择已内置枚举处理:
枚举类型通过
/Cube/Lookup?codes=Type.FullName接口获取,返回[{ label, value }]格式。
自定义页面中通过 useEnumLabel composable 统一处理枚举显示:
import { createEnumLabel, useEnumLookup } from "@/composables/useEnumLabel";
import { usePageApi } from "@/composables/usePageApi";
const api = usePageApi("Equipments", "Equipment");
// 方式一:配合 useEnumLookup 自动获取枚举并生成标签函数
const { labels, loading } = useEnumLookup(api, {
kind: 'SmartMES.Data.Equipments.EquipmentKinds',
status: 'SmartMES.Data.Equipments.EquipmentStatus',
});
// 模板中直接使用
// <el-table-column label="设备类型">
// <template #default="{ row }">{{ labels.kind(row.kind) }}</template>
// </el-table-column>
// 方式二:已有选项数组时直接创建标签函数
const typeOptions = ref([{ value: 1, label: '注塑机' }, { value: 2, label: '机械手' }]);
const typeLabel = createEnumLabel(typeOptions);
// 模板中:{{ typeLabel(row.kind) }}
createEnumLabel 自动处理以下情况:
| 输入值 | 选项中有匹配 | 选项中无匹配 | 说明 |
|---|---|---|---|
null / undefined / '' | — | "未设置" | 空值统一显示 |
0 | 匹配成员的 label | "未设置" | 0 且无对应成员 |
1/2/... | 匹配成员的 label | 原始值字符串 | 有匹配显示 label |
不需要再手写
if (v == null || v === '' || Number(v) === 0) return '未设置'这类重复代码。