SuperAPI AI Directory

hap-view-plugin

Coding & development

View repository

Updated 10-15 21:50

About this skill

HAP Custom View Plugin Development Skill

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:

  1. Node.js version 16.20 or higher is installed
  2. You have a Mingdao Cloud developer account and plugin development permissions
  3. 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.js into 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 Email Email ❌ ✅
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:

  1. Single related record (enumDefault=1 or subType=1)

    • Return format: JSON array string
    • Example: "[{\"sid\":\"...\",\"name\":\"客户名称\",\"sourcevalue\":\"...\"}]"
    • Handling method: Simply parse the JSON string
  2. 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 getRowRelationRows API 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.js file
  • 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:

  1. Features: List the main features of the plugin
  2. Technical implementation: Explain key technical points and optimizations
  3. 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:

  1. 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\"]"
  2. You need to find the corresponding field's config.controls from options, 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:

  1. The raw value returned by a multiple associated field (enumDefault=2 or subType=2) is a number, indicating the number of associated records
  2. Unlike a single association, a multiple association does not directly return a JSON array string
  3. You must call the getRowRelationRows API 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 mdye to 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.