hap-view-plugin
创建和开发明道云 HAP 自定义视图插件的技能。立即触发条件:用户提到"HAP 视图插件"、"自定义视图"、"mdye"、"视图开发"、"插件开发"、"初始化视图项目"、"启动视图调试"。提供完整的开发工作流程、API 使用指南和最佳实践。
How do I install this agent skill?
npx skills add https://github.com/mingdaocom/hap-skills --skill hap-view-pluginIs this agent skill safe to install?
- Gen Agent Trust Hubfail
The skill automates Mingdao Cloud HAP view plugin development but includes dangerous instructions that minimize user oversight. It commands the AI agent to execute high-privilege shell commands (like sudo npm install) while explicitly suppressing technical logs, debugging addresses, and execution status. This concealment of activity, combined with automated package installation and code deployment, creates a high-risk environment for non-transparent operations.
- Socketpass
No alerts
- Snykfail
Risk: CRITICAL · 3 issues
What does this agent skill do?
HAP 自定义视图插件开发技能
此技能提供创建和开发明道云 HAP 自定义视图插件的完整工作流程和开发规范。
关于此技能
此技能专门用于开发明道云 HAP(High-performance Application Platform)自定义视图插件。通过集成的脚手架工具,可以快速创建 React 基础示例模板项目,安装依赖并启动开发环境。
前置条件
在使用此技能前,确保:
- 已安装 16.20 或更高版本的 Node.js
- 拥有明道云开发者账号和插件开发权限
- 了解基本的 React 开发知识
开发环境配置
编辑器规则配置(可选)
可在视图开发项目根目录下创建 AI 编辑器规则文件(按所用工具选择,如 Cursor 的 .cursorrules、Claude Code 的 CLAUDE.md 等),写入明道云视图插件开发的代码规范与约定,即可在编辑器中获得相应的智能提示和规范检查。具体规范可参考本技能 references/ 下的开发指南。
开发参考
明道云视图插件的 API 使用实例与最佳实践,请参考本技能 references/ 下的开发指南(如 references/hap-view-plugin-dev-guide.md)。
核心功能
1. 安装 mdye-cli 工具
- 全局安装插件开发专用的命令行工具
- 验证工具安装是否成功
2. 初始化本地项目
- 创建唯一的插件项目文件夹
- 使用 React 基础示例模板
- 生成项目配置文件
3. 安装项目依赖
- 安装项目所需的 npm 依赖包
- 配置开发环境
4. 启动开发环境
- 启动本地开发服务器
- 支持热重载和实时预览
- 提供线上调试能力
开发工作流程
步骤 1:检查并安装 mdye-cli 工具
首先检查是否已安装:
mdye --version
如果显示版本号,说明已安装,可以跳过安装步骤。
如果未安装,根据系统安装:
Mac OS 用户:
sudo npm install -g mdye-cli
Windows/Linux 用户:
npm install -g mdye-cli
验证安装:
mdye --version
步骤 2:初始化本地项目
创建项目命令:
mdye init view --id 你的worksheetID-你的视图ID --template React
参数说明:
--id: 插件 ID(示例 ID,实际使用时需要替换)--template React: 使用 React 基础示例模板
项目结构:
mdye_view_你的视图ID/
├── package.json
├── mdye.json
├── src/
│ ├── index.jsx
│ ├── App.jsx
│ └── styles.less
└── .gitignore
步骤 3:进入项目目录并安装依赖
进入项目目录:
cd mdye_view_你的视图ID
安装依赖:
npm i
步骤 4:启动开发环境
启动命令:
mdye start
启动后:
- 开发服务器将在
http://localhost:3000/启动 - 将调试地址
http://localhost:3000/bundle.js粘贴到明道云视图配置开发调试输入框 - 支持实时编辑和热重载
API 使用指南
1. 环境变量及配置获取
1.1 获取 env 环境变量
// 使用辅助函数安全获取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 配置
import { config } from "mdye";
// 获取应用、工作表、视图的ID
const { appId, worksheetId, viewId, controls } = config;
// 获取字段控件信息
const fieldControl = _.find(controls, { controlId: fieldId });
2. 数据获取 API
2.1 获取工作表数据 (getFilterRows)
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)
async function getRecordDetail(rowId) {
const result = await api.getRowDetail({
appId,
worksheetId,
viewId,
rowId
});
return result.data;
}
2.3 获取关联记录 (getRowRelationRows)
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)
async function addRecord(fieldsData) {
const response = await api.addWorksheetRow({
appId,
worksheetId,
receiveControls: [
{
controlId: "fieldId1",
type: 2,
value: "测试文本"
}
]
});
return response;
}
3.2 更新记录 (updateWorksheetRow)
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)
async function deleteRecord(rowId) {
const response = await api.deleteWorksheetRow({
appId,
worksheetId,
rowIds: [rowId]
});
return response;
}
4. 工具函数 (utils)
4.1 打开记录详情(推荐使用!)
使用 utils.openRecordInfo 打开明道云原生行记录组件是最佳实践:
优势:
- ✅ 原生体验,与明道云界面一致
- ✅ 功能完整:支持编辑、删除、讨论、日志、附件等所有功能
- ✅ 自动处理权限验证
- ✅ 无需自己开发弹窗 UI
- ✅ 返回操作结果,方便进行数据同步
基础用法:
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);
}
};
返回值说明:
{
action: 'update' | 'delete' | 'close', // 操作类型
value: object | null // 更新后的记录数据(仅 action='update' 时)
}
完整的 React Hook 示例(包含自动刷新):
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 (
<div>
{loading ? (
<div>加载中...</div>
) : (
<div>
{records.map(record => (
<div
key={record.rowid}
onClick={() => handleRecordClick(record.rowid)}
style={{ cursor: 'pointer' }}
>
{record.title}
</div>
))}
</div>
)}
</div>
);
}
性能优化建议:
// 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 打开新建记录窗口
utils.openNewRecord({
appId,
worksheetId
}).then(newRecord => {
if (newRecord) {
addLocalRecord(newRecord);
}
});
4.3 选择用户
const users = await utils.selectUsers({
projectId: "orgId1",
unique: false // 是否单选
});
4.4 选择部门
const departments = await utils.selectDepartments({
projectId: "orgId1",
unique: false
});
4.5 选择位置
const location = await utils.selectLocation({
distance: 1000,
defaultPosition: { lat: 39.915, lng: 116.404 },
multiple: false
});
4.6 选择记录
const records = await utils.selectRecord({
projectId: "orgId1",
relateSheetId: "worksheetId1",
multiple: true
});
5. 事件监听
5.1 筛选条件变更事件
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 新增记录事件
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 | 邮箱 | ❌ | ✅ | |
| 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 | 组织角色 | ❌ | ✅ |
常见错误示例
❌ 错误写法:
// 只查找 type 10 和 11,会遗漏 type 9 的单选字段
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 10 || ctrl.type === 11)
);
✅ 正确写法:
// 包含 type 9, 10, 11 所有选项字段类型
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 9 || ctrl.type === 10 || ctrl.type === 11)
);
字段解析函数
单选字段
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: "" };
}
}
多选字段
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 [];
}
}
成员字段
function parseMembers(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
附件字段
function parseAttachments(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
定位字段
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 属性分为两种类型,返回数据格式完全不同:
-
单条关联 (enumDefault=1 或 subType=1)
- 返回格式: JSON 数组字符串
- 示例:
"[{\"sid\":\"...\",\"name\":\"客户名称\",\"sourcevalue\":\"...\"}]" - 处理方式: 直接解析 JSON 字符串即可
-
多条关联 (enumDefault=2 或 subType=2)
- 返回格式: 数字(表示关联记录的数量)
- 示例:
2(表示关联了 2 条记录) - 处理方式: 必须调用
getRowRelationRowsAPI 才能获取实际数据
完整处理示例:
// 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;
}
字段配置示例:
// 在 config.controls 中查看关联字段配置
const relationControl = controls.find(ctrl => ctrl.controlId === 'relationFieldId');
// 单条关联配置
{
"controlId": "示例控件ID",
"type": 29,
"controlName": "关联客户",
"enumDefault": 1, // 或 subType: 1
// ... 其他属性
}
// 多条关联配置
{
"controlId": "示例控件ID2",
"type": 29,
"controlName": "关联产品",
"enumDefault": 2, // 或 subType: 2
// ... 其他属性
}
自动获取字段值的工具函数
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 命令行工具
基本命令
# 查看版本
mdye --version
# 授权登录
mdye auth
# 初始化项目
mdye init view --id <id> --template <template-name>
# 启动开发
mdye start
# 构建项目
mdye build
# 提交插件
mdye push -m "提交说明"
# 查看当前用户
mdye whoami
# 注销
mdye logout
# 同步插件参数配置
mdye sync-params -f <file-path>
插件发布流程(重要!)
插件开发完成后,需要按以下步骤提交发布到明道云平台。发布成功后,本插件在组织下所有应用均可使用。
第1步:构建项目
执行以下命令将本地项目打包:
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步:提交并发布
执行以下命令将本地项目提交并推送到线上待发布插件列表:
mdye push -m "提交说明"
提交说明编写建议:
建议在提交信息中包含以下内容:
- 功能特性:列出插件的主要功能
- 技术实现:说明关键技术点和优化
- 版本说明:首次发布/功能更新/Bug修复
完整示例:
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),而不是选项的显示文本。
原因分析:
- 明道云选项字段返回的原始值是 JSON 格式的 key 数组,如
"[\"42ad38bf-d3e6-441f-a960-670e704abe4a\"]" - 需要从
config.controls中找到对应字段的options,然后根据 key 匹配出 value
解决方案:
// 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 的单选字段
解决方案:
// ✅ 正确:包含所有选项字段类型
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),而不是实际的关联记录信息。
原因分析:
- 多条关联字段 (enumDefault=2 或 subType=2) 返回的原始值是数字,表示关联记录的数量
- 与单条关联不同,多条关联不会直接返回 JSON 数组字符串
- 必须调用
getRowRelationRowsAPI 才能获取实际的关联记录数据
解决方案:
// 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;
}
如何判断字段是单条还是多条关联:
// 方法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
问题 5:mdye 命令不存在
- 重新安装 mdye-cli
- 检查 PATH 环境变量
- 使用
which mdye检查安装位置
问题 6:项目启动失败
- 检查端口是否被占用
- 检查依赖是否完整安装
- 查看错误日志信息
问题 7:插件 ID 冲突
- 使用新的唯一后缀
- 删除旧的冲突项目
- 重新初始化项目
参考资源
- 明道云开发者文档
- React 官方文档
- Node.js 官方文档
- 明道云开发者社区
注意: 此技能提供的是开发工作流程指导和 API 使用规范,实际开发中请根据具体需求调整配置和代码。
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/mingdaocom/hap-skills/hap-view-plugin">View hap-view-plugin on skillZs</a>