ワンクリックで
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 '未设置'这类重复代码。