This skill provides the complete workflow and development standards for creating and developing Mingdao Cloud HAP custom view plugins.
About This Skill
This skill is specifically for developing Mingdao Cloud HAP (High-performance Application Platform) custom view plugins. Through the integrated scaffolding tool, you can quickly create a React basic example template project, install dependencies, and start the development environment.
Prerequisites
Before using this skill, ensure:
- Node.js version 16.20 or higher is installed
- You have a Mingdao Cloud developer account and plugin development permissions
- You understand basic React development knowledge
Development Environment Configuration
Cursor Editor Configuration
Download the mdye-cursorrules.md file and copy its contents to the .cursorrules file in the root directory of the view development project, and you can get intelligent hints and code specification checks for Mingdao Cloud view plugin development in the Cursor editor.
Teaching DEMO
Please download the Mingdao Cloud view plugin development teaching DEMO. This plugin provides developers with intuitive, interactive API usage examples.
Core Features
1. Install the mdye-cli Tool
- Globally install the command-line tool dedicated to plugin development
- Verify whether the tool installation was successful
2. Initialize a Local Project
- Create a unique plugin project folder
- Use the React basic example template
- Generate project configuration files
3. Install Project Dependencies
- Install the npm dependency packages required by the project
- Configure the development environment
4. Start the Development Environment
- Start the local development server
- Support hot reload and real-time preview
- Provide online debugging capabilities
Development Workflow
Step 1: Check and Install the mdye-cli Tool
First check whether it is already installed:
mdye --version
If the version number is displayed, it means it is already installed, and you can skip the installation step.
If it is not installed, install it according to your system:
Mac OS users:
sudo npm install -g mdye-cli
Windows/Linux users:
npm install -g mdye-cli
Verify the installation:
mdye --version
Step 2: Initialize a Local Project
Create project command:
mdye init view --id 693d2fed8474b99be3d3c12e-69563e5df03728c888c04f05 --template React
Parameter description:
--id: Plugin ID (example ID, needs to be replaced in actual use)--template React: Use the React basic example template
Project structure:
mdye_view_69563e5df03728c888c04f05/
├── package.json
├── mdye.json
├── src/
│ ├── index.jsx
│ ├── App.jsx
│ └── styles.less
└── .gitignore
Step 3: Enter the project directory and install dependencies
Enter the project directory:
cd mdye_view_69563e5df03728c888c04f05
Install dependencies:
npm i
Step 4: Start the Development Environment
Start command:
mdye start
After starting:
- The development server will start at
http://localhost:3000/startup - Paste the debugging address
http://localhost:3000/bundle.jsinto the Mingdao Cloud view configuration development debugging input box - Support real-time editing and hot reload
API Usage Guide
1. Environment Variables and Configuration Retrieval
1.1 Get env environment variables
// 使用辅助函数安全获取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 Get config configuration
import { config } from "mdye";
// 获取应用、工作表、视图的ID
const { appId, worksheetId, viewId, controls } = config;
// 获取字段控件信息
const fieldControl = _.find(controls, { controlId: fieldId });
2. Data Retrieval API
2.1 Get worksheet data (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 Get record details (getRowDetail)
async function getRecordDetail(rowId) {
const result = await api.getRowDetail({
appId,
worksheetId,
viewId,
rowId
});
return result.data;
}
2.3 Get related records (getRowRelationRows)
async function loadRelationRows({ controlId, rowId }) {
const result = await api.getRowRelationRows({
worksheetId,
controlId, // 关联字段ID
rowId, // 主记录ID
pageIndex: 1,
pageSize: 10
});
return result.data;
}
3. Data Operation API
3.1 Add record (addWorksheetRow)
async function addRecord(fieldsData) {
const response = await api.addWorksheetRow({
appId,
worksheetId,
receiveControls: [
{
controlId: "fieldId1",
type: 2,
value: "测试文本"
}
]
});
return response;
}
3.2 Update record (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 Delete record (deleteWorksheetRow)
async function deleteRecord(rowId) {
const response = await api.deleteWorksheetRow({
appId,
worksheetId,
rowIds: [rowId]
});
return response;
}
4. Utility functions (utils)
4.1 Open record details (recommended!)
Using utils.openRecordInfo to open the Mingdao Cloud native row record component is the best practice:
Advantages:
- ✅ Native experience, consistent with the Mingdao Cloud interface
- ✅ Full functionality: supports all features such as editing, deleting, discussions, logs, attachments, etc.
- ✅ Automatically handles permission verification
- ✅ No need to develop your own popup UI
- ✅ Returns operation results, making data synchronization convenient
Basic usage:
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);
}
};
Return value description:
{
action: 'update' | 'delete' | 'close', // 操作类型
value: object | null // 更新后的记录数据(仅 action='update' 时)
}
Complete React Hook example (including automatic refresh):
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>
);
}
Performance optimization suggestions:
// 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 Open new record window
utils.openNewRecord({
appId,
worksheetId
}).then(newRecord => {
if (newRecord) {
addLocalRecord(newRecord);
}
});
4.3 Select user
const users = await utils.selectUsers({
projectId: "orgId1",
unique: false // 是否单选
});
4.4 Select department
const departments = await utils.selectDepartments({
projectId: "orgId1",
unique: false
});
4.5 Select location
const location = await utils.selectLocation({
distance: 1000,
defaultPosition: { lat: 39.915, lng: 116.404 },
multiple: false
});
4.6 Select record
const records = await utils.selectRecord({
projectId: "orgId1",
relateSheetId: "worksheetId1",
multiple: true
});
5. Event Listening
5.1 Filter condition change event
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 Add record event
useEffect(() => {
const handleNewRecord = (newRecord) => {
console.log('新增记录:', newRecord);
setRecords(prev => [...prev, newRecord]);
};
md_emitter.addListener('new-record', handleNewRecord);
return () => {
md_emitter.removeListener('new-record', handleNewRecord);
};
}, []);
Special field type handling
⚠️ Important note: field type numbers
The Mingdao Cloud field type numbers are not completely consistent with the enum values in the documentation, so be sure to pay attention during development:
According to the actual field type definitions of Mingdao Cloud API V3:
- Type 9 = Single Select (SingleSelect) ⚠️ Note it is not type 11
- Type 10 = Multiple Select (MultipleSelect)
- Type 11 = Dropdown (Dropdown)
Complete field type reference table (V3 practical version)
| Type Number | Enum Name | Field Type | API Create | API Return |
|---|---|---|---|---|
| 2 | Text | Text Box | ✅ | ✅ |
| 3 | PhoneNumber | Mobile | ❌ | ✅ |
| 4 | LandlinePhone | Landline | ❌ | ✅ |
| 5 | ❌ | ✅ | ||
| 6 | Number | Number | ✅ | ✅ |
| 7 | Certificate | ID | ❌ | ✅ |
| 8 | Currency | Amount | ❌ | ✅ |
| 9 | SingleSelect | Single Select | ✅ | ✅ |
| 10 | MultipleSelect | Multiple Select | ✅ | ✅ |
| 11 | Dropdown | Dropdown | ❌ | ✅ |
| 14 | Attachment | Attachment | ✅ | ✅ |
| 15 | Date | Date | ✅ | ✅ |
| 16 | DateTime | Time | ✅ | ✅ |
| 19/23/24 | Region | Region | ❌ | ✅ |
| 21 | DynamicLink | Free Link | ❌ | ✅ |
| 22 | Divider | Segment | ❌ | ❌ |
| 25 | AmountInWords | Amount in Words | ❌ | ✅ |
| 26 | Collaborator | Member | ✅ | ✅ |
| 27 | Department | Department | ❌ | ✅ |
| 28 | Rating | Rating | ❌ | ✅ |
| 29 | Relation | Link to Other Table | ✅ | ✅ |
| 30 | Lookup | Field from Other Table | ❌ | ✅ |
| 31 | Formula | Formula | ❌ | ✅ |
| 32 | Concatenate | Text Concatenation | ❌ | ✅ |
| 33 | AutoNumber | Auto Number | ❌ | ✅ |
| 34 | SubTable | Subtable | ❌ | ✅ |
| 35 | CascadingSelect | Cascading Select | ❌ | ✅ |
| 36 | Checkbox | Checkbox | ❌ | ✅ |
| 37 | Rollup | Summary | ❌ | ✅ |
| 38 | DateFormula | Formula (Date) | ❌ | ✅ |
| 39 | CodeScan | Scan Code | ❌ | ✅ |
| 40 | Location | Location | ❌ | ✅ |
| 41 | RichText | Rich Text | ❌ | ✅ |
| 42 | Signature | Signature | ❌ | ✅ |
| 43 | OCR | Text Recognition | ❌ | ✅ |
| 44 | Role | Role | ❌ | ✅ |
| 45 | Embed | Embed | ❌ | ❌ |
| 46 | Time | Time | ✅ | ✅ |
| 47 | Barcode | Barcode | ❌ | ✅ |
| 48 | OrgRole | Organization Role | ❌ | ✅ |
Common Error Examples
❌ Incorrect usage:
// 只查找 type 10 和 11,会遗漏 type 9 的单选字段
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 10 || ctrl.type === 11)
);
✅ Correct usage:
// 包含 type 9, 10, 11 所有选项字段类型
const selectField = controls?.find(ctrl =>
ctrl.controlName?.includes('状态') && (ctrl.type === 9 || ctrl.type === 10 || ctrl.type === 11)
);
Field Parsing Functions
Single-select field
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: "" };
}
}
Multi-select field
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 [];
}
}
Member field
function parseMembers(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
Attachment field
function parseAttachments(value) {
try {
if (!value) return [];
return typeof value === 'string' ? JSON.parse(value) : value;
} catch (err) {
return [];
}
}
Location field
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 };
}
}
Related record field (⚠️ Important!)
Special handling rules for the related field (type 29):
The related field is divided into two types based on the enumDefault or subType attribute, and the returned data formats are completely different:
-
Single related record (enumDefault=1 or subType=1)
- Return format: JSON array string
- Example:
"[{\"sid\":\"...\",\"name\":\"客户名称\",\"sourcevalue\":\"...\"}]" - Handling method: Simply parse the JSON string
-
Multiple related records (enumDefault=2 or subType=2)
- Return format: Number (indicating the number of related records)
- Example:
2(indicates 2 related records) - Handling method: You must call
getRowRelationRowsAPI to get the actual data
Complete handling example:
// 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;
}
Field configuration example:
// 在 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
// ... 其他属性
}
Utility function to automatically get field values
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 command-line tool
Basic commands
# 查看版本
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>
Plugin publishing process (Important!)
After plugin development is complete, you need to submit and publish it to the Mingdao Cloud platform by following the steps below. After successful publishing, this plugin can be used by all applications under the organization.
Step 1: Build the project
Run the following command to package the local project:
cd your_plugin_project
mdye build
Build process description:
- Webpack will compile and bundle all source code
- Generate the optimized
bundle.jsfile - Compilation usually takes 1-2 seconds
- After success, it will display "Build code complete" and the bundle file size
Build output example:
[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] 构建代码完成
Step 2: Submit and publish
Run the following command to submit the local project and push it to the online pending-publish plugin list:
mdye push -m "提交说明"
Suggestions for writing the submission description:
It is recommended to include the following in the submission information:
- Features: List the main features of the plugin
- Technical implementation: Explain key technical points and optimizations
- Version description: First release/feature update/Bug fix
Complete example:
mdye push -m "订单状态视图插件首次发布
功能特性:
- 按订单状态分类展示(待付款/已付款/已发货/已完成/已取消)
- 完整订单信息展示(订单编号/客户/联系人/日期/金额/负责人)
- 多条关联产品信息展示(产品名称/编号/分类/单价)
- 点击订单卡片打开原生行记录弹窗
- 支持编辑/删除订单并自动刷新列表
- 响应式网格布局和流畅动画效果
技术实现:
- 正确处理单选字段(type 9)和关联记录字段(type 29)
- 使用 getRowRelationRows API 处理多条关联
- 使用 utils.openRecordInfo 实现原生交互
- Promise.all 并行加载提升性能"
Step 3: Login authentication
Login is required when submitting. Enter as prompted:
- Username (mobile phone number or email address)
- Password
If you are already logged in, you can view the current logged-in user through mdye whoami .
Step 4: Confirm successful publishing
After successful publishing, the plugin information will be displayed:
[21:20:54] 文件上传成功
[21:20:55] push成功
┌──────────────────────────────────────────────────────────┐
│ ---- 插件信息 ---- │
│ │
│ 插件名称: 自定义视图 │
│ 视图名称: 自定义视图 │
│ 视图地址: https://www.mingdao.com/worksheet/... │
│ 提交信息: 订单状态视图插件首次发布 │
│ 提交人: 用户名 │
└──────────────────────────────────────────────────────────┘
Status after publishing
✅ Plugin published - Can be used in all applications within the organization ✅ View address - You can directly access the plugin through the returned URL ✅ Organization sharing - Other members within the organization can use this plugin
Common issues
Issue 1: Build failure
- Check for code syntax errors
- Ensure all dependencies are correctly installed (
npm install) - Check the error logs to locate the issue
Issue 2: Push failure
- Confirm you are logged in:
mdye whoami - Check the network connection
- Verify whether the account permissions support plugin development
Issue 3: Login timeout
- Log in again:
mdye auth - Enter the correct phone number/email and password
Local project structure
plugin_project/
├── .config/ # 配置文件目录
├── src/ # 源代码目录
│ ├── components/ # 组件目录
│ ├── utils/ # 工具函数目录
│ ├── App.js # 主应用组件
│ ├── index.js # 入口文件
│ └── style.less # 样式文件
├── mdye.json # 插件配置文件
└── package.json # 项目依赖配置
Best practices
1. Project organization
- Keep the project structure clear
- Divide components and modules reasonably
- Use meaningful file naming
2. Code quality
- Follow React best practices
- Use ESLint and Prettier
- Write clear comments and documentation
3. Performance optimization
- Use React.memo to optimize rendering
- Avoid unnecessary re-renders
- Optimize state management
- Use code splitting
4. Security considerations
- Avoid hardcoding sensitive information
- Use environment variables to manage configuration
- Validate user input
- Prevent XSS attacks
Common issue solutions
Issue 1: Option fields display key instead of text
Issue description: Single-select or multi-select fields display a key in UUID format (such as 42ad38bf-d3e6-441f-a960-670e704abe4a), instead of the option's display text.
Cause analysis:
- The raw value returned by a Mingdao Cloud option field is an array of keys in JSON format, such as
"[\"42ad38bf-d3e6-441f-a960-670e704abe4a\"]" - You need to find the corresponding field's
config.controlsfromoptions, and then match the value based on the key
Solution:
// 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: "" };
}
}
Issue 2: Single-select field not found
Issue description: When using controls.find() to find a single-select field, it returns undefined。
Cause analysis:
- The type of a single-select field is 9 rather than 11
- type 10 is multi-select, type 11 is dropdown
- If you only check
ctrl.type === 11, you will miss single-select fields with type 9
Solution:
// ✅ 正确:包含所有选项字段类型
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)
);
Issue 3: Multiple associated fields only display numbers
Issue description: The associated field displays a number (such as 2、3), instead of the actual associated record information.
Cause analysis:
- The raw value returned by a multiple associated field (enumDefault=2 or subType=2) is a number, indicating the number of associated records
- Unlike a single association, a multiple association does not directly return a JSON array string
- You must call the
getRowRelationRowsAPI to obtain the actual associated record data
Solution:
// 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;
}
How to determine whether a field is a single or multiple association:
// 方法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');
}
Issue 4: npm installation failure
- Check the network connection
- Clear the npm cache:
npm cache clean --force - Use the Taobao mirror:
npm config set registry https://registry.npmmirror.com
Issue 2: mdye command does not exist
- Reinstall mdye-cli
- Check the PATH environment variable
- Use
which mdyeto check the installation location
Issue 3: Project startup failure
- Check whether the port is already in use
- Check whether dependencies are fully installed
- View error log information
Issue 4: Plugin ID conflict
- Use a new unique suffix
- Delete the old conflicting project
- Reinitialize the project
Reference resources
- Mingdao Cloud developer documentation
- React official documentation
- Node.js official documentation
- Mingdao Cloud developer community
Note: This skill provides development workflow guidance and API usage specifications. During actual development, please adjust the configuration and code according to specific needs.