---
name: hap-view-plugin
description: 创建和开发明道云 HAP 自定义视图插件的技能。此技能应该在使用者需要开发 HAP 自定义视图插件、初始化本地开发环境、安装依赖、启动调试时使用。
license: MIT
---
# HAP 自定义视图插件开发技能
此技能提供创建和开发明道云 HAP 自定义视图插件的完整工作流程和开发规范。
## 关于此技能
此技能专门用于开发明道云 HAP(High-performance Application Platform)自定义视图插件。通过集成的脚手架工具,可以快速创建 React 基础示例模板项目,安装依赖并启动开发环境。
### 前置条件
在使用此技能前,确保:
1. 已安装 16.20 或更高版本的 Node.js
2. 拥有明道云开发者账号和插件开发权限
3. 了解基本的 React 开发知识
### 开发环境配置
#### Cursor 编辑器配置
下载 `mdye-cursorrules.md` 文件并复制其中内容到视图开发项目根目录下的 `.cursorrules` 文件中,即可在 Cursor 编辑器中获得明道云视图插件开发的智能提示和代码规范检查。
#### 教学 DEMO
请下载明道云视图插件开发教学 DEMO,此插件为开发者提供直观、可交互的 API 使用实例。
### 核心功能
#### 1. 安装 mdye-cli 工具
- 全局安装插件开发专用的命令行工具
- 验证工具安装是否成功
#### 2. 初始化本地项目
- 创建唯一的插件项目文件夹
- 使用 React 基础示例模板
- 生成项目配置文件
#### 3. 安装项目依赖
- 安装项目所需的 npm 依赖包
- 配置开发环境
#### 4. 启动开发环境
- 启动本地开发服务器
- 支持热重载和实时预览
- 提供线上调试能力
## 开发工作流程
### 步骤 1:检查并安装 mdye-cli 工具
**首先检查是否已安装:**
```bash
mdye --version
```
如果显示版本号,说明已安装,可以跳过安装步骤。
**如果未安装,根据系统安装:**
**Mac OS 用户:**
```bash
sudo npm install -g mdye-cli
```
**Windows/Linux 用户:**
```bash
npm install -g mdye-cli
```
**验证安装:**
```bash
mdye --version
```
### 步骤 2:初始化本地项目
**创建项目命令:**
```bash
mdye init view --id 693d2fed8474b99be3d3c12e-69563e5df03728c888c04f05 --template React
```
**参数说明:**
- `--id`: 插件 ID(示例 ID,实际使用时需要替换)
- `--template React`: 使用 React 基础示例模板
**项目结构:**
```
mdye_view_69563e5df03728c888c04f05/
├── package.json
├── mdye.json
├── src/
│ ├── index.jsx
│ ├── App.jsx
│ └── styles.less
└── .gitignore
```
### 步骤 3:进入项目目录并安装依赖
**进入项目目录:**
```bash
cd mdye_view_69563e5df03728c888c04f05
```
**安装依赖:**
```bash
npm i
```
### 步骤 4:启动开发环境
**启动命令:**
```bash
mdye start
```
**启动后:**
- 开发服务器将在 `http://localhost:3000/` 启动
- 将调试地址 `http://localhost:3000/bundle.js` 粘贴到明道云视图配置开发调试输入框
- 支持实时编辑和热重载
## HAP 产品线说明
### 🌐 多产品线支持
HAP 支持多个产品线和私有部署,在视图插件中调用 API 时需要注意 **host 配置**:
| 产品线 | API Host | 说明 |
|--------|----------|------|
| **明道云 HAP** | `https://api.mingdao.com` | 官方 SaaS 服务 |
| **Nocoly HAP** | `https://www.nocoly.com` | Nocoly SaaS 服务 |
| **私有部署 HAP** | `https://your-domain.com/api` | ⚠️ **注意:私有部署需要在域名后加 `/api`** |
**示例**:
- 明道云:`https://api.mingdao.com/v3/open/worksheet/getFilterRows`
- 私有部署:`https://p-demo.mingdaoyun.cn/api/v3/open/worksheet/getFilterRows` ← 注意 `/api`
**建议**:视图插件应该从 `window.mdye.env` 中获取 host 配置,而不是硬编码。
---
## API 使用指南
### 1. 环境变量及配置获取
#### 1.1 获取 env 环境变量
```javascript
// 使用辅助函数安全获取env中的配置项
function getEnvValue(env, key, defaultValue = null) {
if (!env || !key) return defaultValue;
const value = env[key];
// 处理数组类型(字段选择器)
if (Array.isArray(value)) {
return value.length > 0 ? value[0] : defaultValue;
}
// 处理普通值
return value !== undefined ? value : defaultValue;
}
// 使用示例
const titleFieldId = getEnvValue(env, 'title');
const maxRecords = getEnvValue(env, 'maxRecords', '50');
```
#### 1.2 获取 config 配置
```javascript
import { config } from "mdye";
// 获取应用、工作表、视图的ID
const { appId, worksheetId, viewId, controls } = config;
// 获取字段控件信息
const fieldControl = _.find(controls, { controlId: fieldId });
```
### 2. 数据获取 API
#### 2.1 获取工作表数据 (getFilterRows)
```javascript
import { api } from "mdye";
async function loadRecords() {
const result = await api.getFilterRows({
worksheetId, // 必填-工作表ID
viewId, // 必填-视图ID
pageIndex: 1, // 可选-页码
pageSize: 50, // 可选-每页记录数
sortId: "fieldId", // 可选-排序字段
isAsc: true, // 可选-升序排序
// 获取关联字段数据
requestParams: {
plugin_detail_control: relationFieldId
}
});
return result.data; // 记录数组
}
```
#### 2.2 获取记录详情 (getRowDetail)
```javascript
async function getRecordDetail(rowId) {
const result = await api.getRowDetail({
appId,
worksheetId,
viewId,
rowId
});
return result.data;
}
```
#### 2.3 获取关联记录 (getRowRelationRows)
```javascript
async function loadRelationRows({ controlId, rowId }) {
const result = await api.getRowRelationRows({
worksheetId,
controlId, // 关联字段ID
rowId, // 主记录ID
pageIndex: 1,
pageSize: 10
});
return result.data;
}
```
### 3. 数据操作 API
#### 3.1 新增记录 (addWorksheetRow)
```javascript
async function addRecord(fieldsData) {
const response = await api.addWorksheetRow({
appId,
worksheetId,
receiveControls: [
{
controlId: "fieldId1",
type: 2,
value: "测试文本"
}
]
});
return response;
}
```
#### 3.2 更新记录 (updateWorksheetRow)
```javascript
async function updateRecord(rowId, fieldId, newValue) {
const response = await api.updateWorksheetRow({
appId,
worksheetId,
rowId,
newOldControl: [
{
controlId: fieldId,
type: 2,
value: newValue
}
]
});
return response;
}
```
#### 3.3 删除记录 (deleteWorksheetRow)
```javascript
async function deleteRecord(rowId) {
const response = await api.deleteWorksheetRow({
appId,
worksheetId,
rowIds: [rowId]
});
return response;
}
```
### 4. 工具函数 (utils)
#### 4.1 打开记录详情(推荐使用!)
**使用 `utils.openRecordInfo` 打开明道云原生行记录组件是最佳实践:**
优势:
- ✅ 原生体验,与明道云界面一致
- ✅ 功能完整:支持编辑、删除、讨论、日志、附件等所有功能
- ✅ 自动处理权限验证
- ✅ 无需自己开发弹窗 UI
- ✅ 返回操作结果,方便进行数据同步
**基础用法:**
```javascript
import { utils } from "mdye";
// 打开记录详情
const handleRecordClick = async (recordId) => {
try {
const result = await utils.openRecordInfo({
appId,
worksheetId,
viewId,
recordId
});
// 处理返回结果
if (result) {
console.log('操作结果:', result);
// 根据操作类型处理
switch (result.action) {
case 'update':
// 记录被更新,刷新数据
console.log('记录已更新:', result.value);
loadRecords(); // 重新加载数据
break;
case 'delete':
// 记录被删除,刷新列表
console.log('记录已删除');
loadRecords(); // 重新加载数据
break;
case 'close':
// 用户关闭弹窗(无修改)
console.log('用户关闭了弹窗');
break;
}
}
} catch (error) {
console.error('打开记录详情失败:', error);
}
};
```
**返回值说明:**
```javascript
{
action: 'update' | 'delete' | 'close', // 操作类型
value: object | null // 更新后的记录数据(仅 action='update' 时)
}
```
**完整的 React Hook 示例(包含自动刷新):**
```javascript
import React, { useEffect, useState } from 'react';
import { config, api, utils } from 'mdye';
function RecordsList() {
const { appId, worksheetId, viewId } = config;
const [records, setRecords] = useState([]);
const [loading, setLoading] = useState(false);
// 加载记录列表
const loadRecords = async () => {
try {
setLoading(true);
const result = await api.getFilterRows({
worksheetId,
viewId,
pageSize: 100,
pageIndex: 1
});
setRecords(result.data || []);
} catch (error) {
console.error('加载记录失败:', error);
} finally {
setLoading(false);
}
};
// 打开记录详情
const handleRecordClick = async (recordId) => {
try {
const result = await utils.openRecordInfo({
appId,
worksheetId,
viewId,
recordId
});
// 自动刷新列表
if (result?.action === 'update' || result?.action === 'delete') {
loadRecords(); // 刷新数据
}
} catch (error) {
console.error('打开记录详情失败:', error);
}
};
// 初始加载
useEffect(() => {
loadRecords();
}, []);
return (
{loading ? (
加载中...
) : (
{records.map(record => (
handleRecordClick(record.rowid)}
style={{ cursor: 'pointer' }}
>
{record.title}
))}
)}
);
}
```
**性能优化建议:**
```javascript
// 1. 使用 useCallback 避免重复创建函数
const handleRecordClick = useCallback(async (recordId) => {
const result = await utils.openRecordInfo({
appId, worksheetId, viewId, recordId
});
if (result?.action === 'update' || result?.action === 'delete') {
loadRecords();
}
}, [appId, worksheetId, viewId]);
// 2. 只在需要时刷新
const handleRecordClick = async (recordId) => {
const result = await utils.openRecordInfo({
appId, worksheetId, viewId, recordId
});
// 根据具体操作决定是否刷新
if (result?.action === 'update') {
// 局部更新(性能更好)
setRecords(prev =>
prev.map(r => r.rowid === recordId ? result.value : r)
);
} else if (result?.action === 'delete') {
// 从列表中移除
setRecords(prev => prev.filter(r => r.rowid !== recordId));
}
};
```
#### 4.2 打开新建记录窗口
```javascript
utils.openNewRecord({
appId,
worksheetId
}).then(newRecord => {
if (newRecord) {
addLocalRecord(newRecord);
}
});
```
#### 4.3 选择用户
```javascript
const users = await utils.selectUsers({
projectId: "orgId1",
unique: false // 是否单选
});
```
#### 4.4 选择部门
```javascript
const departments = await utils.selectDepartments({
projectId: "orgId1",
unique: false
});
```
#### 4.5 选择位置
```javascript
const location = await utils.selectLocation({
distance: 1000,
defaultPosition: { lat: 39.915, lng: 116.404 },
multiple: false
});
```
#### 4.6 选择记录
```javascript
const records = await utils.selectRecord({
projectId: "orgId1",
relateSheetId: "worksheetId1",
multiple: true
});
```
### 5. 事件监听
#### 5.1 筛选条件变更事件
```javascript
import { md_emitter } from "mdye";
useEffect(() => {
const handleFiltersUpdate = (newFilters) => {
console.log('筛选条件已更新:', newFilters);
// 重新获取数据
};
md_emitter.addListener('filters-update', handleFiltersUpdate);
return () => {
md_emitter.removeListener('filters-update', handleFiltersUpdate);
};
}, []);
```
#### 5.2 新增记录事件
```javascript
useEffect(() => {
const handleNewRecord = (newRecord) => {
console.log('新增记录:', newRecord);
setRecords(prev => [...prev, newRecord]);
};
md_emitter.addListener('new-record', handleNewRecord);
return () => {
md_emitter.removeListener('new-record', handleNewRecord);
};
}, []);
```
## 特殊字段类型处理
### ⚠️ 重要提示:字段类型编号
**明道云字段类型编号与文档中的枚举值不完全一致,开发时务必注意:**
根据明道云 API V3 版本的实际字段类型定义:
- **Type 9** = 单选 (SingleSelect) ⚠️ 注意不是 type 11
- **Type 10** = 多选 (MultipleSelect)
- **Type 11** = 下拉 (Dropdown)
### 完整字段类型对照表(V3 实用版)
| 类型编号 | 枚举名称 | 字段类型 | API 创建 | API 返回 |
|---------|---------|---------|---------|---------|
| 2 | Text | 文本框 | ✅ | ✅ |
| 3 | PhoneNumber | 手机 | ❌ | ✅ |
| 4 | LandlinePhone | 座机 | ❌ | ✅ |
| 5 | Email | 邮箱 | ❌ | ✅ |
| 6 | Number | 数值 | ✅ | ✅ |
| 7 | Certificate | 证件 | ❌ | ✅ |
| 8 | Currency | 金额 | ❌ | ✅ |
| **9** | **SingleSelect** | **单选** | ✅ | ✅ |
| 10 | MultipleSelect | 多选 | ✅ | ✅ |
| 11 | Dropdown | 下拉 | ❌ | ✅ |
| 14 | Attachment | 附件 | ✅ | ✅ |
| 15 | Date | 日期 | ✅ | ✅ |
| 16 | DateTime | 时间 | ✅ | ✅ |
| 19/23/24 | Region | 地区 | ❌ | ✅ |
| 21 | DynamicLink | 自由链接 | ❌ | ✅ |
| 22 | Divider | 分段 | ❌ | ❌ |
| 25 | AmountInWords | 大写金额 | ❌ | ✅ |
| 26 | Collaborator | 成员 | ✅ | ✅ |
| 27 | Department | 部门 | ❌ | ✅ |
| 28 | Rating | 等级 | ❌ | ✅ |
| 29 | Relation | 连接他表 | ✅ | ✅ |
| 30 | Lookup | 他表字段 | ❌ | ✅ |
| 31 | Formula | 公式 | ❌ | ✅ |
| 32 | Concatenate | 文本拼接 | ❌ | ✅ |
| 33 | AutoNumber | 自动编号 | ❌ | ✅ |
| 34 | SubTable | 子表 | ❌ | ✅ |
| 35 | CascadingSelect | 级联选择 | ❌ | ✅ |
| 36 | Checkbox | 检查框 | ❌ | ✅ |
| 37 | Rollup | 汇总 | ❌ | ✅ |
| 38 | DateFormula | 公式(日期) | ❌ | ✅ |
| 39 | CodeScan | 扫码 | ❌ | ✅ |
| 40 | Location | 定位 | ❌ | ✅ |
| 41 | RichText | 富文本 | ❌ | ✅ |
| 42 | Signature | 签名 | ❌ | ✅ |
| 43 | OCR | 文字识别 | ❌ | ✅ |
| 44 | Role | 角色 | ❌ | ✅ |
| 45 | Embed | 嵌入 | ❌ | ❌ |
| 46 | Time | 时间 | ✅ | ✅ |
| 47 | Barcode | 条码 | ❌ | ✅ |
| 48 | OrgRole | 组织角色 | ❌ | ✅ |
### 常见错误示例
❌ **错误写法:**
```javascript
// 只查找 type 10 和 11,会遗漏 type 9 的单选字段
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 10 || ctrl.type === 11)
);
```
✅ **正确写法:**
```javascript
// 包含 type 9, 10, 11 所有选项字段类型
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 9 || ctrl.type === 10 || ctrl.type === 11)
);
```
### 字段解析函数
#### 单选字段
```javascript
function parseSingleSelect(value, control) {
try {
if (!value) return { key: "", text: "" };
const keys = typeof value === 'string'
? JSON.parse(value)
: (Array.isArray(value) ? value : []);
const selectedKey = keys[0] || "";
let selectedText = "";
if (control && control.options) {
const option = control.options.find(opt => opt.key === selectedKey);
selectedText = option ? option.value : "";
}
return { key: selectedKey, text: selectedText };
} catch (err) {
console.error("解析单选字段失败:", err);
return { key: "", text: "" };
}
}
```
#### 多选字段
```javascript
function parseMultiSelect(value, control) {
try {
if (!value) return [];
const keys = typeof value === 'string'
? JSON.parse(value)
: (Array.isArray(value) ? value : []);
const result = [];
if (control && control.options) {
keys.forEach(key => {
const option = control.options.find(opt => opt.key === key);
if (option) {
result.push({ key: key, text: option.value });
}
});
}
return result;
} catch (err) {
console.error("解析多选字段失败:", err);
return [];
}
}
```
#### 成员字段
```javascript
function parseMembers(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
```
#### 附件字段
```javascript
function parseAttachments(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
```
#### 定位字段
```javascript
function parseLocation(value) {
try {
if (!value) return { title: "", address: "", x: 0, y: 0 };
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return { title: "", address: "", x: 0, y: 0 };
}
}
```
#### 关联记录字段(⚠️ 重要!)
**关联字段 (type 29) 的特殊处理规则:**
关联字段根据 `enumDefault` 或 `subType` 属性分为两种类型,返回数据格式完全不同:
1. **单条关联** (enumDefault=1 或 subType=1)
- 返回格式: JSON 数组字符串
- 示例: `"[{\"sid\":\"...\",\"name\":\"客户名称\",\"sourcevalue\":\"...\"}]"`
- 处理方式: 直接解析 JSON 字符串即可
2. **多条关联** (enumDefault=2 或 subType=2)
- 返回格式: 数字(表示关联记录的数量)
- 示例: `2` (表示关联了 2 条记录)
- 处理方式: **必须调用 `getRowRelationRows` API** 才能获取实际数据
**完整处理示例:**
```javascript
// 1. 判断是否为多条关联
function isMultipleRelation(value) {
return typeof value === 'number' || (!isNaN(value) && value !== '');
}
// 2. 解析单条关联数据
function parseRelationData(value) {
try {
if (!value) return [];
const relations = typeof value === 'string' ? JSON.parse(value) : value;
if (!Array.isArray(relations)) return [];
return relations.map(item => {
let sourceValue = {};
if (item.sourcevalue) {
try {
sourceValue = typeof item.sourcevalue === 'string'
? JSON.parse(item.sourcevalue)
: item.sourcevalue;
} catch (e) {
console.error("解析sourcevalue失败:", e);
}
}
return {
sid: item.sid || '',
name: item.name || '',
rowid: sourceValue.rowid || '',
...item
};
});
} catch (err) {
console.error("解析关联记录字段失败:", err);
return [];
}
}
// 3. 完整使用示例(包含单条和多条处理)
async function loadOrdersWithProducts() {
const result = await api.getFilterRows({
worksheetId,
viewId,
pageSize: 1000,
pageIndex: 1
});
// 使用 Promise.all 并行处理所有订单
const ordersData = await Promise.all(
result.data.map(async (row) => {
// 获取关联产品字段值
const productsValue = row['relationFieldId'];
let products = [];
// 判断是单条还是多条关联
if (isMultipleRelation(productsValue)) {
// 多条关联:调用 API 获取详情
try {
const relationResult = await api.getRowRelationRows({
worksheetId,
controlId: 'relationFieldId', // 关联字段ID
rowId: row.rowid,
pageSize: 100,
pageIndex: 1
});
if (relationResult && relationResult.data) {
products = relationResult.data.map(item => ({
name: item['productNameFieldId'], // 产品名称字段ID
code: item['productCodeFieldId'], // 产品编码字段ID
price: item['productPriceFieldId'], // 产品单价字段ID
rowid: item.rowid
}));
}
} catch (error) {
console.error('获取多条关联失败:', error);
}
} else {
// 单条关联:直接解析
products = parseRelationData(productsValue);
}
return {
id: row.rowid,
products: products,
productsCount: isMultipleRelation(productsValue)
? Number(productsValue)
: products.length
};
})
);
return ordersData;
}
```
**字段配置示例:**
```javascript
// 在 config.controls 中查看关联字段配置
const relationControl = controls.find(ctrl => ctrl.controlId === 'relationFieldId');
// 单条关联配置
{
"controlId": "692ed1d0f34d7ea4df717c67",
"type": 29,
"controlName": "关联客户",
"enumDefault": 1, // 或 subType: 1
// ... 其他属性
}
// 多条关联配置
{
"controlId": "692ed1d0f34d7ea4df717c6e",
"type": 29,
"controlName": "关联产品",
"enumDefault": 2, // 或 subType: 2
// ... 其他属性
}
```
### 自动获取字段值的工具函数
```javascript
function getFieldValue(fieldId, record, controls) {
if (!fieldId || !record) return null;
const rawValue = record[fieldId];
if (rawValue === undefined) return null;
const control = controls.find(ctrl => ctrl.controlId === fieldId);
if (!control) return rawValue;
const fieldType = getFieldTypeByControlType(control.type);
switch (fieldType) {
case 'text':
case 'email':
case 'phone':
return rawValue;
case 'number':
return parseFloat(rawValue) || 0;
case 'select':
return parseSingleSelect(rawValue, control);
case 'multiselect':
return parseMultiSelect(rawValue, control);
case 'user':
return parseMembers(rawValue);
case 'department':
return parseDepartments(rawValue);
case 'attachment':
return parseAttachments(rawValue);
case 'location':
return parseLocation(rawValue);
case 'boolean':
return rawValue === "1" || rawValue === 1 || rawValue === true;
case 'relation':
return parseRelationData(rawValue);
default:
return rawValue;
}
}
function getFieldTypeByControlType(controlType) {
const typeMap = {
2: 'text', // 文本框
3: 'phone', // 手机
4: 'phone', // 座机
5: 'email', // 邮箱
6: 'number', // 数值
7: 'certificate', // 证件
8: 'number', // 金额
9: 'select', // 单选 ⚠️ 重要:type 9 是单选
10: 'multiselect', // 多选
11: 'select', // 下拉
14: 'attachment', // 附件
15: 'date', // 日期
16: 'datetime', // 时间
19: 'region', // 地区
23: 'region', // 地区
24: 'region', // 地区
26: 'user', // 成员
27: 'department', // 部门
28: 'rating', // 等级
29: 'relation', // 连接他表
36: 'boolean', // 检查框
40: 'location', // 定位
41: 'richtext', // 富文本
42: 'signature', // 签名
46: 'time', // 时间
48: 'role', // 组织角色
};
return typeMap[controlType] || 'unknown';
}
```
## mdye 命令行工具
### 基本命令
```bash
# 查看版本
mdye --version
# 授权登录
mdye auth
# 初始化项目
mdye init view --id --template
# 启动开发
mdye start
# 构建项目
mdye build
# 提交插件
mdye push -m "提交说明"
# 查看当前用户
mdye whoami
# 注销
mdye logout
# 同步插件参数配置
mdye sync-params -f
```
### 插件发布流程(重要!)
插件开发完成后,需要按以下步骤提交发布到明道云平台。发布成功后,本插件在组织下所有应用均可使用。
#### 第1步:构建项目
执行以下命令将本地项目打包:
```bash
cd your_plugin_project
mdye build
```
**构建过程说明:**
- Webpack 会编译并打包所有源代码
- 生成优化后的 `bundle.js` 文件
- 通常需要 1-2 秒完成编译
- 成功后会显示 "构建代码完成" 和 bundle 文件大小
**构建输出示例:**
```
[21:20:33] 开始构建代码
ℹ Compiling Webpack
✔ Webpack: Compiled successfully in 1.94s
asset bundle.js 228 KiB [emitted] [minimized] (name: main)
webpack 5.98.0 compiled successfully in 1947 ms
[21:20:35] 构建代码完成
```
#### 第2步:提交并发布
执行以下命令将本地项目提交并推送到线上待发布插件列表:
```bash
mdye push -m "提交说明"
```
**提交说明编写建议:**
建议在提交信息中包含以下内容:
1. **功能特性**:列出插件的主要功能
2. **技术实现**:说明关键技术点和优化
3. **版本说明**:首次发布/功能更新/Bug修复
**完整示例:**
```bash
mdye push -m "订单状态视图插件首次发布
功能特性:
- 按订单状态分类展示(待付款/已付款/已发货/已完成/已取消)
- 完整订单信息展示(订单编号/客户/联系人/日期/金额/负责人)
- 多条关联产品信息展示(产品名称/编号/分类/单价)
- 点击订单卡片打开原生行记录弹窗
- 支持编辑/删除订单并自动刷新列表
- 响应式网格布局和流畅动画效果
技术实现:
- 正确处理单选字段(type 9)和关联记录字段(type 29)
- 使用 getRowRelationRows API 处理多条关联
- 使用 utils.openRecordInfo 实现原生交互
- Promise.all 并行加载提升性能"
```
#### 第3步:登录认证
提交时需要登录账户,按提示输入:
- 用户名(手机号或邮箱地址)
- 密码
如果已登录,可以通过 `mdye whoami` 查看当前登录用户。
#### 第4步:确认发布成功
发布成功后会显示插件信息:
```
[21:20:54] 文件上传成功
[21:20:55] push成功
┌──────────────────────────────────────────────────────────┐
│ ---- 插件信息 ---- │
│ │
│ 插件名称: 自定义视图 │
│ 视图名称: 自定义视图 │
│ 视图地址: https://www.mingdao.com/worksheet/... │
│ 提交信息: 订单状态视图插件首次发布 │
│ 提交人: 用户名 │
└──────────────────────────────────────────────────────────┘
```
#### 发布后的状态
✅ **插件已发布** - 可以在组织内所有应用中使用
✅ **视图地址** - 可以通过返回的 URL 直接访问插件
✅ **组织共享** - 组织内其他成员可以使用该插件
#### 常见问题
**问题1: 构建失败**
- 检查代码语法错误
- 确保所有依赖已正确安装 (`npm install`)
- 查看错误日志定位问题
**问题2: 推送失败**
- 确认已登录:`mdye whoami`
- 检查网络连接
- 验证账号权限是否支持插件开发
**问题3: 登录超时**
- 重新登录:`mdye auth`
- 输入正确的手机号/邮箱和密码
### 本地项目结构
```
plugin_project/
├── .config/ # 配置文件目录
├── src/ # 源代码目录
│ ├── components/ # 组件目录
│ ├── utils/ # 工具函数目录
│ ├── App.js # 主应用组件
│ ├── index.js # 入口文件
│ └── style.less # 样式文件
├── mdye.json # 插件配置文件
└── package.json # 项目依赖配置
```
## 最佳实践
### 1. 项目组织
- 保持项目结构清晰
- 合理划分组件和模块
- 使用有意义的文件命名
### 2. 代码质量
- 遵循 React 最佳实践
- 使用 ESLint 和 Prettier
- 编写清晰的注释和文档
### 3. 性能优化
- 使用 React.memo 优化渲染
- 避免不必要的重渲染
- 优化状态管理
- 使用代码分割
### 4. 安全注意事项
- 避免硬编码敏感信息
- 使用环境变量管理配置
- 验证用户输入
- 防止 XSS 攻击
## 常见问题解决
### 问题 1:选项字段显示 key 而不是文本
**问题描述:** 单选或多选字段显示的是 UUID 格式的 key (如 `42ad38bf-d3e6-441f-a960-670e704abe4a`),而不是选项的显示文本。
**原因分析:**
1. 明道云选项字段返回的原始值是 JSON 格式的 key 数组,如 `"[\"42ad38bf-d3e6-441f-a960-670e704abe4a\"]"`
2. 需要从 `config.controls` 中找到对应字段的 `options`,然后根据 key 匹配出 value
**解决方案:**
```javascript
// 1. 获取字段控件定义(包含options)
const control = config.controls.find(ctrl => ctrl.controlId === fieldId);
// 2. 解析选项字段
function parseSingleSelect(value, control) {
try {
if (!value) return { key: "", text: "" };
// 解析 JSON 字符串得到 key 数组
let keys = [];
if (typeof value === 'string') {
try {
keys = JSON.parse(value); // ["42ad38bf-..."]
} catch {
keys = [value];
}
} else if (Array.isArray(value)) {
keys = value;
}
const selectedKey = keys[0] || "";
// 从 options 中查找对应的显示文本
let selectedText = "";
if (control && control.options) {
const option = control.options.find(opt => opt.key === selectedKey);
selectedText = option ? option.value : selectedKey;
}
return { key: selectedKey, text: selectedText };
} catch (err) {
console.error("解析单选字段失败:", err, value);
return { key: "", text: "" };
}
}
```
### 问题 2:找不到单选字段
**问题描述:** 使用 `controls.find()` 查找单选字段时,返回 `undefined`。
**原因分析:**
- 单选字段的 type 是 **9** 而不是 11
- type 10 是多选,type 11 是下拉
- 如果只检查 `ctrl.type === 11`,会遗漏 type 9 的单选字段
**解决方案:**
```javascript
// ✅ 正确:包含所有选项字段类型
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') &&
(ctrl.type === 9 || ctrl.type === 10 || ctrl.type === 11)
);
// ❌ 错误:会遗漏 type 9
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') &&
(ctrl.type === 10 || ctrl.type === 11)
);
```
### 问题 3:多条关联字段只显示数字
**问题描述:** 关联字段显示的是数字(如 `2`、`3`),而不是实际的关联记录信息。
**原因分析:**
1. 多条关联字段 (enumDefault=2 或 subType=2) 返回的原始值是数字,表示关联记录的数量
2. 与单条关联不同,多条关联不会直接返回 JSON 数组字符串
3. 必须调用 `getRowRelationRows` API 才能获取实际的关联记录数据
**解决方案:**
```javascript
// 1. 判断是否为多条关联
function isMultipleRelation(value) {
return typeof value === 'number' || (!isNaN(value) && value !== '');
}
// 2. 处理关联字段(支持单条和多条)
async function handleRelationField(worksheetId, controlId, rowId, fieldValue) {
let relationData = [];
if (isMultipleRelation(fieldValue)) {
// 多条关联:调用 API 获取详情
try {
const result = await api.getRowRelationRows({
worksheetId,
controlId,
rowId,
pageSize: 100,
pageIndex: 1
});
if (result && result.data) {
relationData = result.data.map(item => ({
rowid: item.rowid,
name: item['titleFieldId'], // 使用实际的标题字段ID
// 解析其他需要的字段
}));
}
} catch (error) {
console.error('获取多条关联失败:', error);
}
} else {
// 单条关联:直接解析
relationData = parseRelationData(fieldValue);
}
return relationData;
}
// 3. 完整使用示例
async function loadRecordsWithRelations() {
const result = await api.getFilterRows({
worksheetId,
viewId,
pageSize: 100,
pageIndex: 1
});
// 使用 Promise.all 并行处理
const records = await Promise.all(
result.data.map(async (row) => {
const relationValue = row['relationFieldId'];
const relations = await handleRelationField(
worksheetId,
'relationFieldId',
row.rowid,
relationValue
);
return {
...row,
relations: relations
};
})
);
return records;
}
```
**如何判断字段是单条还是多条关联:**
```javascript
// 方法1: 查看字段配置
const control = config.controls.find(ctrl => ctrl.controlId === 'relationFieldId');
if (control) {
const isSingle = control.enumDefault === 1 || control.subType === 1;
const isMultiple = control.enumDefault === 2 || control.subType === 2;
console.log('单条关联:', isSingle, '多条关联:', isMultiple);
}
// 方法2: 根据返回值类型判断
const value = row['relationFieldId'];
if (typeof value === 'number' || !isNaN(value)) {
console.log('这是多条关联,需要调用 getRowRelationRows');
} else if (typeof value === 'string') {
console.log('这是单条关联,可以直接解析 JSON');
}
```
### 问题 4:npm 安装失败
- 检查网络连接
- 清理 npm 缓存:`npm cache clean --force`
- 使用淘宝镜像:`npm config set registry https://registry.npmmirror.com`
### 问题 2:mdye 命令不存在
- 重新安装 mdye-cli
- 检查 PATH 环境变量
- 使用 `which mdye` 检查安装位置
### 问题 3:项目启动失败
- 检查端口是否被占用
- 检查依赖是否完整安装
- 查看错误日志信息
### 问题 4:插件 ID 冲突
- 使用新的唯一后缀
- 删除旧的冲突项目
- 重新初始化项目
## 🤖 AI 助手执行指南
当用户请求开发明道云视图插件时,AI 必须按照以下流程执行:
### 1. 前置环境检查(必须执行)
**Step 1.1: 检查 Node.js 版本**
```bash
node --version
```
- ✅ 如果版本 >= 16.20: 继续下一步
- ❌ 如果版本 < 16.20 或未安装:
- 告知用户需要安装 Node.js 16.20 或更高版本
- 提供安装链接: https://nodejs.org/
- 等待用户安装完成后再继续
**Step 1.2: 检查 mdye-cli 是否已安装**
```bash
mdye --version
```
- ✅ 如果显示版本号: mdye-cli 已安装,跳过安装步骤
- ❌ 如果命令不存在: **自动帮用户安装 mdye-cli**
**Step 1.3: 自动安装 mdye-cli(如果未安装)**
**Mac OS 用户:**
```bash
sudo npm install -g mdye-cli
```
**Windows/Linux 用户:**
```bash
npm install -g mdye-cli
```
**安装后验证:**
```bash
mdye --version
```
**告知用户:**
```
✅ mdye-cli 工具已安装
📋 安装信息:
- 工具名称: mdye-cli
- 版本: [显示版本号]
- 用途: 明道云视图插件开发专用命令行工具
💡 下一步:
- 现在可以开始创建视图插件项目了
```
### 2. 模板选择(根据需求自动选择)
根据用户需求选择合适的模板:
| 用户需求 | 推荐模板 | 说明 |
|---------|---------|------|
| 简单展示、学习示例 | `--template React` | React 基础模板,适合快速上手 |
| 需要 UI 组件库 | `--template React-Tailwind` | 包含 Tailwind CSS,适合快速构建界面 |
| 复杂业务逻辑 | `--template React` | 基础模板,可自行添加需要的库 |
| Vue 技术栈 | `--template Vue` | Vue 模板(如果可用) |
**默认推荐**: `--template React-Tailwind`(适合大多数场景)
### 3. 项目初始化(自动执行)
**Step 3.1: 询问或生成插件 ID**
- 如果用户提供了插件 ID: 直接使用
- 如果用户未提供: 使用示例 ID 或询问用户
**Step 3.2: 初始化项目**
```bash
mdye init view --id [插件ID] --template React-Tailwind
```
**Step 3.3: 进入项目目录并安装依赖**
```bash
cd mdye_view_[插件后缀]/
npm i
```
**如果 npm 安装失败:**
- 建议使用淘宝镜像: `npm config set registry https://registry.npmmirror.com`
- 清理缓存: `npm cache clean --force`
- 重新安装: `npm i`
### 4. 启动开发环境(自动执行)
```bash
mdye start
```
**启动后告知用户:**
```
✅ 视图插件开发环境已启动!
📋 项目信息:
- 项目目录: mdye_view_[插件后缀]/
- 开发服务器: http://localhost:3000/
- 调试地址: http://localhost:3000/bundle.js
💡 下一步:
1. 将调试地址粘贴到明道云视图配置的开发调试输入框
2. 在项目中编辑代码,支持热重载
3. 开发完成后运行 `npm run build` 生成发布包
📖 开发指南:
- 主入口文件: src/index.jsx
- 样式文件: src/styles.less
- API 使用: 参考技能文档中的 API 使用指南
```
### 5. 开发过程中的辅助
**用户请求数据查询时:**
- 使用 `api.getFilterRows()` 获取工作表数据
- 正确处理关联字段(参考"常见问题"部分)
- 处理选项字段(使用 key 值而非 value)
**用户请求数据操作时:**
- 使用 `api.addWorksheetRow()` 新增记录
- 使用 `api.updateWorksheetRow()` 更新记录
- 使用 `api.deleteWorksheetRows()` 删除记录
**用户遇到问题时:**
- 参考"常见问题与解决方案"部分
- 提供详细的诊断步骤和解决方案
### 6. AI 执行原则
- ✅ **主动检查**: 开发前必须检查 Node.js 和 mdye-cli
- ✅ **自动安装**: 检测到工具未安装时,自动帮用户安装
- ✅ **选择模板**: 根据用户需求自动选择合适的模板
- ✅ **完整流程**: 从环境检查到启动开发环境,一气呵成
- ✅ **错误处理**: 遇到错误时提供详细诊断和解决方案
- ❌ **不要跳过**: 不要跳过前置环境检查步骤
- ❌ **不要假设**: 不要假设用户已安装所有工具
---
## 参考资源
- 明道云开发者文档
- React 官方文档
- Node.js 官方文档
- 明道云开发者社区
---
**注意:** 此技能提供的是开发工作流程指导和 API 使用规范,实际开发中请根据具体需求调整配置和代码。