/**
* IOC 低码组件模板
*
* 严格遵循 M8 低码开发规范:
* - 组件目录结构标准化
* - config.js 配置定义
* - mock/data.js 数据模拟
* - index.vue 组件主文件
* - css/index.scss 样式分离
* - plugin 插件引用
*/
interface IocTemplateOptions {
componentName: string; // 组件名称(PascalCase)
displayName: string; // 组件中文名称
description: string; // 功能描述
author: string; // 作者
dateStr: string; // 创建日期
}
/**
* 生成 IOC 组件 base.js 模板
* 基础信息配置(自动生成,勿改 module_name)
*/
export function generateIocBaseTemplate(options: IocTemplateOptions): string {
const { componentName, displayName } = options;
return `/**
* ${displayName} - 基础配置
*
* ⚠️ 注意:
* - name: 组件中文名称(在低码平台显示)
* - module_name: 必须与组件文件夹名完全一致(PascalCase)
* - 本文件由脚手架自动生成,请勿随意修改 module_name
*/
export default {
name: '${displayName}', // 组件中文名称(低码平台显示)
module_name: '${componentName}', // 必须和组件文件夹名匹配
version: '0.1.0'
// 如果当前组件需要使用三方npm库依赖,可以在这里配置;
// 配置参考文档:https://epointfe.feishu.cn/wiki/wikcnWfDygigkfyAELeQhjjl3Ch
// packages: [
// {
// title: 'ucharts 图表库',
// package: '@qiun/ucharts',
// version: '^2.5.0-20230101',
// urls: ['http://app.epoint.com.cn/h5/fileattaches/20230404/u-charts-20230404170212.js'],
// library: 'uCharts'
// }
// ]
};
`;
}
/**
* 生成 IOC 组件 config.js 模板
* 低码配置(属性面板、交互事件)
*
* 配置项类型说明:
* - text: 普通文本输入框
* - boolean: 开关 (True/False)
* - color: 颜色选择器
* - select: 下拉选择框
* - number: 数字输入框
* - image: 图片URL输入
* - uploadimage: 图片上传控件(推荐用于图片配置)
* - boxmodel: 盒子模型(边距)
* - array: 数组/列表配置(配合 dynamic: true 和 template 使用)
*/
export function generateIocConfigTemplate(options: IocTemplateOptions): string {
const { displayName } = options;
return `import base from './base';
// 通用容器样式
import boxOptions from '../plugin/boxoptions/config';
export default {
base,
dimension: {
width: '100%',
height: 'auto'
},
configuration: [
{
name: 'options',
value: [
boxOptions,
{
displayName: '基础配置',
name: 'baseConfig',
value: [
{
displayName: '显示标题',
name: 'showTitle',
type: 'boolean',
value: true
},
{
displayName: '标题文字',
name: 'title',
type: 'text',
value: '${displayName}'
}
]
},
{
displayName: '样式配置',
name: 'styleConfig',
value: [
{
displayName: '背景色',
name: 'backgroundColor',
type: 'color',
value: '#ffffff'
},
{
displayName: '标题颜色',
name: 'titleColor',
type: 'color',
value: '#333333'
},
{
displayName: '标题字号',
name: 'titleSize',
type: 'text',
value: '16px'
}
]
},
// ============ 图片配置示例(使用 uploadimage 类型)============
// 如需添加图片上传配置,参考以下格式:
// {
// displayName: '图片设置',
// name: 'imgConfig',
// value: [
// {
// name: 'bgurl',
// displayName: '背景图片',
// value: '',
// type: 'uploadimage',
// tip: '支持jpg、png、svg格式'
// },
// {
// displayName: '图片高度',
// name: 'imgHeight',
// value: '150px',
// type: 'text'
// }
// ]
// },
// ============ 数组配置示例 ============
// {
// displayName: '列表配置',
// name: 'listSetting',
// type: 'array',
// dynamic: true,
// tip: '可动态增删的列表项',
// value: [],
// template: {
// displayName: '列表项',
// value: [
// {
// name: 'imgOptions',
// displayName: '图片设置',
// value: [
// {
// name: 'imgurl',
// displayName: '图片',
// value: '',
// type: 'uploadimage'
// }
// ]
// }
// ]
// }
// },
{
displayName: '定位设置',
name: 'positionSetting',
tip: 'css中的定位',
value: [
{
displayName: '开启定位',
name: 'isOpen',
type: 'boolean',
value: false
},
{
displayName: 'left',
name: 'left',
type: 'text',
value: '10px'
},
{
displayName: 'top',
name: 'top',
type: 'text',
value: '10px'
},
{
displayName: 'right',
name: 'right',
type: 'text',
value: ''
},
{
displayName: 'bottom',
name: 'bottom',
type: 'text',
value: ''
}
]
}
]
},
{
name: 'interaction',
displayName: '交互',
value: [
{
name: 'callback',
displayName: '回调参数',
type: 'array',
dynamic: true,
value: [],
template: {
name: 'callback',
displayName: '参数',
value: [
{
name: 'param',
displayName: '变量名',
type: 'text',
value: ''
},
{
name: 'field',
displayName: '字段值',
type: 'text',
value: ''
}
]
}
},
{
name: 'event',
displayName: '事件(新)',
type: 'array',
value: [
{
name: 'onMounted',
displayName: 'onMounted事件',
dynamic: true,
type: 'array',
value: [],
template: {
name: 'templeteMounted_1',
displayName: '动作',
type: 'text',
value: 'console.log("onMounted事件:", e)'
}
},
{
name: 'onClick',
displayName: 'onClick事件',
dynamic: true,
type: 'array',
value: [],
template: {
name: 'templeteOnClick_1',
displayName: '动作',
type: 'text',
value: 'console.log("OnClick事件:", e)'
}
},
{
name: 'onChange',
displayName: 'onChange事件',
dynamic: true,
type: 'array',
value: [],
template: {
name: 'templeteOnChange_1',
displayName: '值变动',
type: 'text',
value: 'console.log("onChange事件:", e)'
}
}
]
}
]
}
]
};
`;
}
/**
* 生成 IOC 组件 mock/data.js 模板
* 组件 Mock 数据
*/
export function generateIocMockDataTemplate(
options: IocTemplateOptions,
): string {
const { displayName } = options;
return `/**
* ${displayName} - Mock 数据
*
* 规范说明:
* - data 外层数组容器勿删除
* - 组件通过 props.data 接收此数组
* - 数据结构需与 UI 展示一一对应
*/
export default {
data: [
{
id: '1',
name: '示例数据1',
status: '已完成',
description: '这是第一条示例数据'
},
{
id: '2',
name: '示例数据2',
status: '进行中',
description: '这是第二条示例数据'
},
{
id: '3',
name: '示例数据3',
status: '未开始',
description: '这是第三条示例数据'
}
]
};
`;
}
/**
* 生成 IOC 组件 index.vue 模板
* 组件 Vue 主文件
*/
export function generateIocVueTemplate(options: IocTemplateOptions): string {
const { componentName, displayName, description, author, dateStr } = options;
const kebabName = toKebabCase(componentName);
return `<!--
* @作者: ${author}
* @创建时间: ${dateStr}
* @修改时间: ${dateStr}
* @版本: [1.0]
* @版权: 国泰新点软件股份有限公司
* @描述: ${description}
*
* ⚠️ 重要提醒(AI 助手必读):
* 1. 【优先使用组件库】必须优先使用 em- 组件库中的组件,例如:
* - 轮播:使用 em-swipe + em-swipe-item,而非自行实现
* - 图片:使用 em-image(支持懒加载、错误处理)
* - 列表:使用 em-cell / em-cell-group
* - 按钮:使用 em-button
* - 弹窗:使用 em-popup / em-dialog
* 2. 【防御性编程】访问对象属性前必须判空,如 item?.url
* 3. 【v-for 安全】确保 myData 始终是数组,避免 undefined
-->
<template>
<div class="epoint-component" :style="[boxOptions, boxOptions.boxmodel, positionSetting]" @click="click">
<!--
🔴 开发提醒:
- 如需轮播功能,请删除此默认模板,改用 em-swipe 组件:
<em-swipe :autoplay="3000" indicator-color="white">
<em-swipe-item v-for="(item, index) in myData" :key="index">
<em-image :src="item.url" fit="cover" />
</em-swipe-item>
</em-swipe>
- 如需图片展示,请使用 em-image 组件替代 img 标签
-->
<!-- 标题区域 -->
<div v-if="baseConfig.showTitle" class="${kebabName}__header" :style="titleStyle">
{{ baseConfig.title }}
</div>
<!-- 内容区域 -->
<div class="${kebabName}__content">
<div
v-for="(item, index) in safeData"
:key="item?.id || index"
class="${kebabName}__item"
@click.stop="handleItemClick(item, index)"
>
<div class="${kebabName}__item-name">{{ item?.name || '' }}</div>
<div class="${kebabName}__item-status">{{ item?.status || '' }}</div>
</div>
</div>
</div>
</template>
<script>
// 小屏低码组件相关配置
import '@boot';
import mockData from './mock/data';
import defaultConfig from './js/config';
import base from './js/base';
// 事件交互
import eventMixin from './plugin/eventgenerate';
export default {
name: base.module_name,
mixins: [eventMixin],
props: {
// mock中的data.js
data: {
type: Array,
default: () => {
return mockData.data;
}
},
// 配置项
config: {
type: Object
},
//获取父级组件数据
cdata: Object
},
data() {
return {
// 内部状态
};
},
watch: {
// 监听数据变化触发onChange事件
myData(val) {
this.eventGenerate('onChange', val);
}
},
computed: {
// 注意:使用el.style等操作DOM的方式在微信小程序不支持
boxOptions() {
const { boxOptions } = this.config.options;
const boxmodel = boxOptions.boxmodel;
//根据实际情况进行单位修改
Object.keys(boxmodel).forEach((key) => {
if (!isNaN(Number(boxmodel[key]))) {
//是纯数字才加单位
boxmodel[key] = boxmodel[key] + 'px';
}
});
return boxOptions;
},
// 基础配置(防御性编程,提供默认值)
baseConfig() {
const config = this.config?.options?.baseConfig || {};
return {
showTitle: config.showTitle !== false,
title: config.title || '${displayName}'
};
},
// 样式配置
styleConfig() {
const config = this.config?.options?.styleConfig || {};
return {
backgroundColor: config.backgroundColor || '#ffffff',
titleColor: config.titleColor || '#333333',
titleSize: config.titleSize || '16px'
};
},
// 标题样式
titleStyle() {
return {
color: this.styleConfig.titleColor,
fontSize: this.styleConfig.titleSize
};
},
// 处理 Mock 数据和父级联动数据的优先级
myData() {
const pdata = this.cdata?.pdata;
if (pdata && Array.isArray(pdata) && pdata.length > 0) {
return pdata;
}
return this.data || mockData.data || [];
},
// 安全的数据访问(确保始终返回数组,防止 v-for 报错)
safeData() {
const data = this.myData;
if (!data || !Array.isArray(data)) {
return [];
}
// 过滤掉 null/undefined 项
return data.filter(item => item != null);
},
// 位置设置
positionSetting() {
let { positionSetting } = this.config.options;
if (positionSetting.isOpen) {
positionSetting.position = 'relative';
} else {
positionSetting.position = 'static';
}
return positionSetting;
},
/**
* 图片路径前缀处理
* 用于 uploadimage 类型配置项的图片地址拼接
*
* 使用示例:
* imgurl() {
* const { imgConfig } = this.config.options;
* if (Config && Config.envconfig && Config.envconfig.imgsrc) {
* return Config.envconfig.imgsrc + imgConfig.bgurl;
* }
* return this.prefix + imgConfig.bgurl;
* }
*/
prefix() {
const origin = location.origin;
let path = location.pathname;
if (path.indexOf('/') === 0) {
path = path.substring(1);
}
// 判断当前是否是测试环境
const basePath = '/' + path.split('/')[0];
if (!this.isTest()) {
// 正式地址
return origin + basePath + '/';
} else {
// 测试地址(请根据实际情况修改)
return 'http://218.4.136.120:8990/smallscreen-demo/';
}
}
},
mounted() {
this.eventGenerate('onMounted', '自定义参数');
},
methods: {
click(e) {
this.eventGenerate('onClick', e);
},
handleItemClick(item, index) {
this.eventGenerate('onChange', { item, index });
},
/**
* 判断是否为测试环境
* 用于 prefix 计算属性的环境判断
*/
isTest() {
let href = location.href;
if (href.includes('smallscreen-demo') && href.includes('218.4.136.120:8990')) {
return false; // 走正式地址 - 开发环境的外网映射
}
// 域名
if (href.startsWith('https:') || href.startsWith('http://app.epoint.com.cn')) {
return false; // https和公司域名走正式地址
}
if (
href.includes('smallscreen-demo') ||
href.startsWith('http://localhost')
) {
return true; // 走测试地址
}
return false;
}
},
// 所有组件必须设置_getConfig和_getMockData,用于低码平台获取组件配置信息
_getConfig: () => defaultConfig,
_getMockData: () => mockData
};
</script>
<style lang="scss" scoped>
@import './css/index.scss';
</style>
`;
}
/**
* 生成 IOC 组件 css/index.scss 模板
* 组件样式文件
*/
export function generateIocScssTemplate(options: IocTemplateOptions): string {
const { componentName, displayName } = options;
const kebabName = toKebabCase(componentName);
return `/**
* ${displayName} - 组件样式
*
* 命名规范:BEM (Block__Element--Modifier)
* - Block: .${kebabName}
* - Element: .${kebabName}__header
* - Modifier: .${kebabName}--active
*/
.${kebabName} {
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 8px;
overflow: hidden;
// 头部标题
&__header {
padding: 12px 16px;
font-size: 16px;
font-weight: 500;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
// 内容区域
&__content {
padding: 12px 16px;
}
// 列表项
&__item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
// 列表项名称
&__item-name {
flex: 1;
font-size: 14px;
color: #333;
}
// 列表项状态
&__item-status {
font-size: 12px;
color: #999;
padding: 2px 8px;
background-color: #f5f5f5;
border-radius: 4px;
}
// 空状态
&__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
}
`;
}
/**
* 生成 IOC 组件 index.js 模板
* 注册入口(自动生成)
*/
export function generateIocIndexJsTemplate(
options: IocTemplateOptions,
): string {
const { componentName, displayName } = options;
return `/**
* ${displayName} - 组件注册入口
*
* ⚠️ 此文件由脚手架自动生成,请勿手动修改
*/
import component from './index.vue';
import base from './js/base.js';
import config from './js/config.js';
// 组件注册
component.install = function(Vue) {
Vue.component(base.module_name, component);
};
// 导出组件配置
component.config = config;
component.base = base;
export default component;
`;
}
/**
* 生成 eventgenerate 插件模板
*/
export function generateEventGenerateTemplate(): string {
return `/**
* 事件生成插件
*
* 提供 eventGenerate 方法,用于触发低码平台配置的事件
*/
export default {
methods: {
/**
* 触发事件
* @param {string} eventName - 事件名称(必须与 config.js 中 interaction.event 的 name 一致)
* @param {any} params - 传递给低码平台的参数对象
*/
eventGenerate(eventName, params) {
// 获取事件配置
const events = this.config?.interaction?.event || [];
const eventConfig = events.find(e => e.name === eventName);
if (!eventConfig || !eventConfig.value || eventConfig.value.length === 0) {
return;
}
// 执行配置的动作
eventConfig.value.forEach(action => {
try {
// 创建执行上下文
const e = params;
// eslint-disable-next-line no-eval
eval(action.value);
} catch (error) {
console.error('[' + eventName + '] 事件执行失败:', error);
}
});
}
}
};
`;
}
/**
* 生成 boxoptions 插件模板(config.js)
* 与 ProductCard/plugin/boxoptions/config.js 格式保持一致
*/
export function generateBoxOptionsTemplate(): string {
return `/**
* 通用容器配置插件
*
* 提供盒子模型(边距)、背景、尺寸等容器样式配置
*/
export default {
displayName: '通用容器样式',
name: 'boxOptions',
value: [
{
displayName: '背景颜色',
name: 'background',
value: '',
type: 'color'
},
{
displayName: '盒子间距',
name: 'boxmodel',
tip: '设置盒子的内外间距',
type: 'boxmodel',
// 默认值 - 固定格式
value: [
{
name: 'marginTop',
value: '0'
},
{
name: 'marginRight',
value: '0'
},
{
name: 'marginBottom',
value: '0'
},
{
name: 'marginLeft',
value: '0'
},
{
name: 'paddingTop',
value: '0'
},
{
name: 'paddingRight',
value: '0'
},
{
name: 'paddingBottom',
value: '0'
},
{
name: 'paddingLeft',
value: '0'
}
]
},
{
displayName: '宽度',
name: 'width',
value: '',
type: 'text'
},
{
displayName: '高度',
name: 'height',
value: '',
type: 'text'
},
{
displayName: '圆角',
name: 'borderRadius',
value: '',
type: 'text'
},
{
displayName: '内容显示',
name: 'overflow',
value: 'hidden',
type: 'select',
options: [
{
name: '自适应',
value: 'auto'
},
{
name: '内容修剪,溢出内容滚动显示',
value: 'scroll'
},
{
name: '内容修剪,溢出内容不显示',
value: 'hidden'
},
{
name: '内容不修剪,呈现在元素框外',
value: 'visible'
}
]
}
]
};
`;
}
/**
* 转换为 kebab-case(短横线命名)
*/
function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
.toLowerCase();
}