HAP 自定义视图插件开发技能
此技能提供创建和开发明道云 HAP 自定义视图插件的完整工作流程和开发规范。
关于此技能
此技能专门用于开发明道云 HAP(High-performance Application Platform)自定义视图插件。通过集成的脚手架工具,可以快速创建 React 基础示例模板项目,安装依赖并启动开发环境。
前置条件
在使用此技能前,确保:
-
已安装 16.20 或更高版本的 Node.js
-
拥有明道云开发者账号和插件开发权限
-
了解基本的 React 开发知识
开发环境配置
Cursor 编辑器配置
下载 mdye-cursorrules.md 文件并复制其中内容到视图开发项目根目录下的 .cursorrules 文件中,即可在 Cursor 编辑器中获得明道云视图插件开发的智能提示和代码规范检查。
教学 DEMO
请下载明道云视图插件开发教学 DEMO,此插件为开发者提供直观、可交互的 API 使用实例。
核心功能
- 安装 mdye-cli 工具
-
全局安装插件开发专用的命令行工具
-
验证工具安装是否成功
- 初始化本地项目
-
创建唯一的插件项目文件夹
-
使用 React 基础示例模板
-
生成项目配置文件
- 安装项目依赖
-
安装项目所需的 npm 依赖包
-
配置开发环境
- 启动开发环境
-
启动本地开发服务器
-
支持热重载和实时预览
-
提供线上调试能力
开发工作流程
步骤 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 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:进入项目目录并安装依赖
进入项目目录:
cd mdye_view_69563e5df03728c888c04f05
安装依赖:
npm i
步骤 4:启动开发环境
启动命令:
mdye start
启动后:
-
开发服务器将在 http://localhost:3000/ 启动
-
将调试地址 http://localhost:3000/bundle.js 粘贴到明道云视图配置开发调试输入框
-
支持实时编辑和热重载
API 使用指南
- 环境变量及配置获取
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 });
- 数据获取 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; }
- 数据操作 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; }
- 工具函数 (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.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 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 组织角色 ❌ ✅
常见错误示例
❌ 错误写法:
// 只查找 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 条记录)
-
处理方式: 必须调用 getRowRelationRows API 才能获取实际数据
完整处理示例:
// 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": "692ed1d0f34d7ea4df717c67", "type": 29, "controlName": "关联客户", "enumDefault": 1, // 或 subType: 1 // ... 其他属性 }
// 多条关联配置 { "controlId": "692ed1d0f34d7ea4df717c6e", "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 # 项目依赖配置
最佳实践
- 项目组织
-
保持项目结构清晰
-
合理划分组件和模块
-
使用有意义的文件命名
- 代码质量
-
遵循 React 最佳实践
-
使用 ESLint 和 Prettier
-
编写清晰的注释和文档
- 性能优化
-
使用 React.memo 优化渲染
-
避免不必要的重渲染
-
优化状态管理
-
使用代码分割
- 安全注意事项
-
避免硬编码敏感信息
-
使用环境变量管理配置
-
验证用户输入
-
防止 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 数组字符串
-
必须调用 getRowRelationRows API 才能获取实际的关联记录数据
解决方案:
// 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
问题 2:mdye 命令不存在
-
重新安装 mdye-cli
-
检查 PATH 环境变量
-
使用 which mdye 检查安装位置
问题 3:项目启动失败
-
检查端口是否被占用
-
检查依赖是否完整安装
-
查看错误日志信息
问题 4:插件 ID 冲突
-
使用新的唯一后缀
-
删除旧的冲突项目
-
重新初始化项目
参考资源
-
明道云开发者文档
-
React 官方文档
-
Node.js 官方文档
-
明道云开发者社区
注意: 此技能提供的是开发工作流程指导和 API 使用规范,实际开发中请根据具体需求调整配置和代码。