diff --git a/build/docs.js b/build/docs.js index 139e3a4..ee850c7 100644 --- a/build/docs.js +++ b/build/docs.js @@ -1,6 +1,7 @@ var vueDocs = require('vue-docgen-api'); var rd = require('rd'); var fs = require('fs'); +var path = require('path'); var componentInfo = []; @@ -17,11 +18,11 @@ function deepFilter(obj, filterProperties = []) { return copy; } -rd.eachFileFilterSync('./src/components', /\.vue$/, function (f, s) { +rd.eachFileFilterSync(path.resolve(__dirname, '../src/components'), /\.vue$/, function(f, s) { let info = deepFilter(vueDocs.parse(f), ['path']); componentInfo.push(info); }); let content = 'module.exports = ' + JSON.stringify(componentInfo, undefined, 4); -fs.writeFileSync('./docs/components.js', content); +fs.writeFileSync(path.resolve(__dirname, '../docs/components.js'), content); diff --git a/build/snippet.js b/build/snippet.js new file mode 100644 index 0000000..a596533 --- /dev/null +++ b/build/snippet.js @@ -0,0 +1,301 @@ +require('./docs.js'); +console.log('成功解析组件'); + +const fs = require('fs'); +const path = require('path'); +const componentInfoList = require('../docs/components.js'); + +let childrenComponentNames = [ + // "button", + // "calendar", + 'origincalendar', + // "chart", + // "checkbox", + // "container", + // "input", + // "link", + // "vpagination", + // "processor", + // "querybuilder", + // "radio", + // "richtext", + // "select", + 'selectlist', + // "tab", + // "table", + // "tag", + // "text", + // "textarea", + // "tree", + 'upload-item', + // "upload" + 'async_modal', + 'modal', + 'tips-tpl' +]; +let supportExpandProps = ['attributes', 'validity']; + +const componentProsDesMap = {}; +let snippetCollection = {}; +let matchNum = /^\d+$/; +let matchLetter = /^'|"([a-zA-Z]+|[\u4e00-\u9fa5]+)'|"$/; +let matchFunc = /^\(\) => \[\]$/; +let matchEmptyStr = /^(''|\"\")$/; +let matchEmptyArr = /^\[\]$/; +let matchBool = /^(true|false)$/; +let pathConf = { + path: `${process.env.INIT_CWD.replace(/\\/g, '/')}/.vscode`, + file: 'snippets.code-snippets', + data: snippetCollection +}; + +function parseDefaultValue(defaultValue) { + let result = false; + let matchFuncArr = [ + value => matchNum.test(value) && 'number', + value => matchLetter.test(value) && 'string', + value => matchFunc.test(value) && 'array', + value => matchEmptyStr.test(value) && 'emptyString', + value => matchEmptyArr.test(value) && 'emptyArray', + value => matchBool.test(value) && 'boolean' + ]; + while (matchFuncArr.length !== 0 && !result) { + let func = matchFuncArr.pop(); + if (func) { + result = func(defaultValue) || false; + } + } + + if (!result) return defaultValue; + + switch (result) { + case 'number': + defaultValue = Number.parseInt(defaultValue); + break; + case 'string': + defaultValue = defaultValue.replace(/\'([a-zA-Z]+|[\u4e00-\u9fa5]+)\'/, '$1'); + break; + case 'array': + defaultValue = []; + break; + case 'emptyString': + defaultValue = ''; + break; + case 'emptyArray': + defaultValue = []; + break; + case 'boolean': + defaultValue = defaultValue === 'true'; + break; + } + return defaultValue; +} +function afterInitDirAndFile(conf) { + let { path: curPath, file: curFilePath, success: successCallBack, error: errorCallBack, data } = conf; + + let fileExistPath = path.resolve(__dirname, curPath, curFilePath); + Object.assign(conf, { file_exist_path: fileExistPath }); + + // ## 检测文件是否存在 + fs.access(fileExistPath, fs.constants.F_OK, err => { + if (!err) { + successCallBack(conf); + } else { + console.log(`${fileExistPath}不存在。已创建目录、文件。`); + + // ### 文件不存在,可能是 目录不存在,也可能是 文件不存在 + let folderPath = path.resolve(__dirname, curPath); + fs.mkdirSync(folderPath, { recursive: true }); + // 默认 flag = 'w',文件不存在会创建它 + fs.writeFileSync(path.resolve(__dirname, fileExistPath), JSON.stringify({}, undefined, 2), 'utf8', err => { + if (err) { + console.log(`${curPath} ${err}`); + } + }); + + errorCallBack(conf); + } + }); +} +function writeToProjectSnippets(conf = { path: '', file: '', data: {} }) { + afterInitDirAndFile({ + ...conf, + success(conf) { + fs.writeFile(conf.file_exist_path, JSON.stringify(conf.data, undefined, 2), 'utf8', err => { + if (err) { + console.log('writeToProjectSnippets', `${conf.path} ${err}`); + } + }); + }, + error(conf) { + writeToProjectSnippets(conf); + } + }); +} +function getWrapperNameFromDesc(desc) { + let wrapperNames = ['___attributes___', '___validity___']; + for (let i = 0; i < wrapperNames.length; i++) { + let wrapperName = wrapperNames[i]; + if (desc.includes(wrapperName)) { + return wrapperName; + } + } + return ''; +} +/** + * 为匹配属性添加备注;返回一个操作后的字符串数组 + * @param {object} conf {body: [], propsToDescMap: [] } + * @returns {array} bodyArr + */ +function addDescToMatchProp(conf = { body: [], propsToDescMap: {} }) { + let { body, propsToDescMap } = conf; + + let bodyArr = JSON.stringify(body, undefined, 2).split('\n') || []; + let bodyArrLength = bodyArr.length; + for (let i = 0; i < bodyArrLength; i++) { + let str = bodyArr[i]; + + // {id, attributes, ..} + if (propsToDescMap) { + let propsNames = Object.keys(propsToDescMap); + + let reg = /"([a-zA-Z]+)"/; + let matchPropName = propsNames.find(curName => { + // NOTE: validity 包含 id,造成干扰,属性名需要完全匹配 + let result = str.match(reg); + if (result && result[1] === curName) return true; + }); + if (matchPropName) { + let desc = propsToDescMap[matchPropName]; + desc = desc.replace(/\n/g, '; '); + bodyArr[i] = `${str} // ${desc}`; + } + } + } + + return bodyArr; +} +/** + * 从 prefix/desc 获取 snippet 基础结构 + * @param {object} conf + * @returns {object} result + */ +function getSnippetConstructor(conf = { prefix: '', desc: '' }) { + let { prefix, desc } = conf; + return { + [desc]: { + scope: ['javascript', 'vue'], + prefix, + description: desc, + body: [] + } + }; +} + +let componentPrefixes = []; +componentInfoList + .filter(curItem => !childrenComponentNames.includes(curItem.displayName.toLowerCase())) + .forEach(currentComponentInfo => { + let { displayName, props, events, methods, slots } = currentComponentInfo; + + let componentName = displayName.toLowerCase(); + let prefix = `cls-${componentName}`; + let desc = `@cls ${prefix}`; + let snippetConstructor = getSnippetConstructor({ prefix, desc }); + + let needExpand = []; + let snippetConstructorBody = { + component: componentName, + id: '$1', + name: '$1', + label: '', + value: undefined, + attributes: {}, + validity: {}, + decoration: [] + }; + if (props) { + Object.keys(props).forEach(propsKey => { + let { description, tags, defaultValue } = props[propsKey]; + + // ## 检测到 tagsProperty 中包含 'ignore',退出 + let { property: tagsProperty, ignore: tagsIgnore } = tags; + if (tagsIgnore && tagsIgnore.some(curItem => curItem.title === 'ignore')) return; + + // ## 构造属性默认值 + let curDefaultValue = ''; + if (defaultValue) { + ({ value: curDefaultValue } = defaultValue); + } + curDefaultValue = parseDefaultValue(curDefaultValue); + + // ## 从备注中获取 该属性是哪部分包裹属性(attributes/validity) + let wrapperName = getWrapperNameFromDesc(description).replace(/___/g, ''); + // ## 包裹属性可能会被写在 tag 中 + if (tagsProperty || wrapperName) { + let tagName = ''; + // ### tags 属性中包含 name 的部分,这个 name 是包裹属性名 + if (tagsProperty) { + let wrapperNameFindResult = tagsProperty.find(curItem => curItem.name); + if (wrapperNameFindResult) { + let { name: wrapperName } = wrapperNameFindResult; + tagName = wrapperName; + } + } + + wrapperName = tagName || wrapperName; + if (propsKey === 'wraperClass') propsKey = 'class'; + snippetConstructorBody[wrapperName][propsKey] = curDefaultValue; + } + + // ## 存储备注 + if (!componentProsDesMap[componentName]) componentProsDesMap[componentName] = {}; + // { input: {id: ''} } + componentProsDesMap[componentName][propsKey] = description; + + if (supportExpandProps.includes(wrapperName)) needExpand.push(wrapperName); + }); + } + + // ## 为匹配属性添加备注 - Full 版本 + snippetConstructor[desc].body = addDescToMatchProp({ + body: snippetConstructorBody, + propsToDescMap: componentProsDesMap[componentName] + }); + Object.assign(snippetCollection, snippetConstructor); + + // ## 构造/存储额外拓展的 snippet(目前仅支持 attributes/validity) + needExpand.forEach(curWrapperName => { + let newPrefix = `${prefix}-${curWrapperName}`; + let newDesc = `@cls ${newPrefix}`; + let newSnippetConstructor = getSnippetConstructor({ + prefix: newPrefix, + desc: newDesc + }); + + newSnippetConstructor[newDesc].body = addDescToMatchProp({ + body: snippetConstructorBody[curWrapperName], + propsToDescMap: componentProsDesMap[componentName] + }); + Object.assign(snippetCollection, newSnippetConstructor); + }); + + // ### 打印列表的存储 + componentPrefixes.push(prefix); + }); + +writeToProjectSnippets(pathConf); +console.log('成功写入到项目snippets'); + +const PLACEHOLDER_MAX = 2; +console.log('以下是调用指令列表:'); +console.log( + componentPrefixes + .map((curPrefix, index) => { + let num = index + 1 + ''; + let numLength = num.length; + + return `${num}${new Array(PLACEHOLDER_MAX - numLength).fill(' ').join('')}: ${curPrefix}`; + }) + .join('\n') +); diff --git a/docs/components.js b/docs/components.js index 61a726e..c7743ff 100644 --- a/docs/components.js +++ b/docs/components.js @@ -1057,6 +1057,19 @@ module.exports = [ }, "required": "" }, + "placeholder": { + "description": "无数据时的文案\n___attributes___", + "tags": {}, + "name": "placeholder", + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "'没有数据'" + } + }, "flexItemWrap": { "description": "若为true,当组件所在容器采用flex布局时,当前组件强制占用一行\n___attributes___", "tags": {}, @@ -2264,6 +2277,22 @@ module.exports = [ }, "required": "" }, + "valueFilterName": { + "description": "组件数据过滤函数名称,对应currentPageInstance中的Vue实例methods中的方法名", + "tags": {}, + "name": "valueFilterName", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "''" + } + }, "hide": { "description": "组件是否隐藏", "tags": { @@ -5039,6 +5068,330 @@ module.exports = [ } } }, + { + "displayName": "Tag", + "description": "", + "tags": {}, + "props": { + "id": { + "description": "组件id", + "tags": {}, + "name": "id", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "label": { + "description": "组件标题", + "tags": {}, + "name": "label", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "\"\"" + } + }, + "name": { + "description": "组件承载数据的key", + "tags": {}, + "name": "name", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "value": { + "description": "组件承载数据", + "tags": {}, + "name": "value", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string|array" + }, + "required": "" + }, + "hide": { + "description": "组件是否隐藏\n___attributes___", + "tags": {}, + "name": "hide", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "boolean" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "false" + } + }, + "placeholder": { + "description": "组件占位符\n___attributes___", + "tags": {}, + "name": "placeholder", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "disabled": { + "description": "组件是否禁用\n___attributes___", + "tags": {}, + "name": "disabled", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "boolean" + }, + "required": "" + }, + "readonly": { + "description": "组件是否只读\n___attributes___", + "tags": {}, + "name": "readonly", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "boolean" + }, + "required": "" + }, + "wraperClass": { + "description": "组件顶层class\n___attributes___", + "tags": {}, + "name": "wraperClass", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "animated": { + "description": "动画名称\n___attributes___", + "tags": {}, + "name": "animated", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "labelWidth": { + "description": "组件label宽度,单位为px\n___attributes___", + "tags": {}, + "name": "labelWidth", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "number" + }, + "required": "" + }, + "changeEventName": { + "description": "组件值变更事件名称\n___attributes___", + "tags": {}, + "name": "changeEventName", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "\"onTagChange\"" + } + }, + "help": { + "description": "组件值补充文案\n___attributes___", + "tags": {}, + "name": "help", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "\"\"" + } + }, + "flexItemWrap": { + "description": "若为true,当组件所在容器采用flex布局时,当前组件强制占用一行\n___attributes___", + "tags": {}, + "name": "flexItemWrap", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "boolean" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "false" + } + }, + "required": { + "description": "组件值是否必填\n___validity___", + "tags": {}, + "name": "required", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "boolean" + }, + "required": "" + }, + "format": { + "description": "组件值类型\n可选值:String, Number\n___validity___", + "tags": {}, + "name": "format", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "\"StringArray\"" + } + }, + "max": { + "description": "tag的最大个数\n___validity___", + "tags": {}, + "name": "max", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "number" + }, + "required": "" + }, + "min": { + "description": "tag的最小个数\n___validity___", + "tags": {}, + "name": "min", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "number" + }, + "required": "" + }, + "_validityErrorMessage": { + "description": "保存validity产生的错误信息", + "tags": { + "ignore": [ + { + "description": true, + "title": "ignore" + } + ] + }, + "name": "_validityErrorMessage", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "string" + }, + "required": "" + }, + "_tableData": { + "description": "", + "tags": { + "ignore": [ + { + "description": true, + "title": "ignore" + } + ] + }, + "name": "_tableData", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "object" + }, + "required": "" + }, + "_parentContainerAttributes": { + "description": "保存父容器组件attributes对象", + "tags": { + "ignore": [ + { + "description": true, + "title": "ignore" + } + ] + }, + "name": "_parentContainerAttributes", + "mixin": { + "name": "emitter" + }, + "type": { + "name": "object" + }, + "required": "" + } + }, + "events": {}, + "methods": [], + "slots": { + "top": { + "description": "组件顶部的组件组合", + "bindings": {} + }, + "left": { + "description": "组件左侧的组件组合", + "bindings": {} + }, + "right": { + "description": "组件右侧的组件组合", + "bindings": {} + }, + "bottom": { + "description": "组件底部的组件组合", + "bindings": {} + } + } + }, { "displayName": "Text", "description": "", @@ -5283,6 +5636,25 @@ module.exports = [ "value": "'onTextMouseLeave'" } }, + "required": { + "description": "组件值是否必填", + "tags": { + "property": [ + { + "title": "property", + "type": { + "name": "mixed" + }, + "name": "validity" + } + ] + }, + "name": "required", + "type": { + "name": "boolean" + }, + "required": "" + }, "help": { "description": "组件值补充文案", "tags": { @@ -5311,6 +5683,26 @@ module.exports = [ }, "required": "" }, + "enableHtml": { + "description": "组件是否支持富文本展示", + "tags": { + "ignore": [ + { + "description": true, + "title": "ignore" + } + ] + }, + "name": "enableHtml", + "type": { + "name": "boolean" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "false" + } + }, "_tableData": { "description": "", "tags": { @@ -6065,6 +6457,24 @@ module.exports = [ }, "required": "" }, + "finishEventName": { + "description": "", + "tags": {}, + "name": "finishEventName", + "type": { + "name": "string" + }, + "required": "" + }, + "uploadEventName": { + "description": "", + "tags": {}, + "name": "uploadEventName", + "type": { + "name": "string" + }, + "required": "" + }, "width": { "description": "", "tags": {}, @@ -6113,6 +6523,15 @@ module.exports = [ "func": false, "value": "'image'" } + }, + "accept": { + "description": "", + "tags": {}, + "name": "accept", + "type": { + "name": "array" + }, + "required": "" } }, "events": { @@ -6337,6 +6756,28 @@ module.exports = [ "value": "'onUploadChange'" } }, + "finishEventName": { + "description": "上传完成事件名称,无论上传成功还是失败,都会触发该事件\n为兼容之前版本,默认事件名称不包含on\n___attributes___", + "tags": {}, + "name": "finishEventName", + "type": { + "name": "string" + }, + "required": "", + "defaultValue": { + "func": false, + "value": "\"uploadFinish\"" + } + }, + "uploadEventName": { + "description": "自定义上传事件名称,若设置,则可在指向的方法中自定义上传逻辑\n___attributes___", + "tags": {}, + "name": "uploadEventName", + "type": { + "name": "string" + }, + "required": "" + }, "help": { "description": "组件值补充文案", "tags": { diff --git a/package.json b/package.json index 2dbddc5..0237366 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "example": "cross-env NODE_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.js", "docs": "node ./build/docs.js", "build": "node ./build/script.js", - "release": "node ./build/release.js" + "release": "node ./build/release.js", + "snippet": "node ./build/snippet.js" }, "repository": { "type": "git",