作者:魏子玄
推荐理由
本文章主要内容是介绍如何通过飞书开放平台来自动化生成飞书文档,包括:飞书文档结构介绍、如何从各平台获取数据等,希望可以帮助你提升工作效率~
本文章主要内容是介绍如何通过飞书开放平台来自动化生成飞书文档,包括:飞书文档结构介绍、如何从各平台获取数据、调用api生成文档。
1.背景
在研发团队的日常工作中,每周的版本owner都会手动根据工作看板(如图1-1)中对应的需求来编辑一个版本日报(如图1-2),便于每个需求的相关负责人会在每日站会的时候对齐需求进展,而编辑这样一个版本日报的时间平均大约需要5-10min(根据需求数量的不同上下浮动)。
另外还有客户端组会周报,一个没有编辑过客户端组会周报,可能需要30mins到1h还不止。
250px|700px|reset
可以看出,版本日报中的内容(版本关键时间点、每个需求名称、各端负责人)是从工作看板中读取的,版本日报的结构也是固定不变的。所以我们想能不能通过飞书机器人来帮助我们完成这样重复性的搬运工作,来提升工作效率。
下面以日报小助手(某研发团队飞书机器人)生成版本日报为例,介绍调用飞书openAPI自动化生成飞书文档的步骤。
2.飞书云文档结构
如图2-1所示,首先介绍一下我们常用的飞书文档结构:
250px|700px|reset
2.1. 一级结构
如图2-2所示,一篇飞书文档(Document)由标题(Title)、正文(Body)和版本(Revision)组成,即:
飞书文档(Document)= 标题(Title) + 正文(Body) + 版本号(Revision)
其中,版本是一个从0开始自增的整数,每次编辑保存后都会+1。
2.2. 二级结构
飞书文档的二级结构是块(Block)。多个Block组成了正文(Body)。
如图2-3所示,一个一级标题、一段文字、一张图片、一个表格、乃至一个空行都是一个Block。他们共同的特点是将光标放置在上面时,均会展示一个样式编辑菜单,并且每一个Block的背景会置为浅蓝色。所以可以看出,一行并不代表一个Block,一个Block并不止一行。
250px|700px|reset
250px|700px|reset
250px|700px|reset
250px|700px|reset
2.3. 三级结构
飞书文档的三级结构就是不同的块,如:文本段落(paragraph)、图片(gallery)、格式化表格(table)、数据表或看板(bitable) 等等。
如表2-1所示,为各种Block的说明。
由于版本日报是由中文本段落和格式化表格组成的,也是应用最多的Block,故下面对其进行进一步说明。
2.3.1. 文本段落(paragraph)
如图2-4所示,一行文本段落(paragraph),由多个行内元素组成,行内元素有文本、文档链接、@用户等几种类型。
让我们举一些小例子来聊聊文本段落:
例子1:
250px|700px|reset
例子2:
250px|700px|reset
两段例子的区别:
第一段例子:对整段paragraph设置了不同的样式,这个样式是属于这段文本段落整体的
第二段例子:对文本段落内各个部分设置不同的样式,可以改变颜色、可以加粗、可以@不同的人、可以是一段链接。
如图2-5所示,在例子2中的一段文本段落由三种行内元素组成:文本(textRun)、@用户(person)和文档链接(docsLink)。
250px|700px|reset
所以在生成一段文本段落时,我们应该考虑:
1.这段文本整体的样式;
2.这段文本由哪些行内元素组成;
3.每个行内元素是否有自身独特的样式(主要还是指文本颜色、加粗、斜体等)。
2.3.2. 格式化表格
如图2-6所示,飞书文档中的表格有三种表格:格式化表格(table)、电子表格(sheet)和多维表格(bitable)。
由于在飞书文档中无法通过调用飞书openAPI编辑电子表格和多维表格,所以在这里只介绍格式化表格的结构。如表2-2所示,为一个1*4的表格。
250px|700px|reset
故如图2-7所示,生成一个表格需要指定表格的行数、列数、表格样式(每列单元格的宽度) 、以及每个单元格中的内容(不讨论合并单元格的特殊情况)。
250px|700px|reset
3.获取生成飞书文档所需要的数据
版本日报中主要包括两部分内容:当前版本的关键时间点 和 当前版本的所有需求。
数据来源:
1.当前版本的关键时间点:从内部某平台获取; (此部分包含内部信息,省略)
2.当前版本的所有需求:从研发团队的多维表格工作看板中获取;(使用飞书开放平台提供的openAPI)
工作看板是一个多维表格,调用api获取多维表格中每一条记录的数据就好了
响应体示例://调用飞书openAPI的公共请求
const getLarkCommonRequest = async function(){
// 首先需要获取 access token
const tokenRes = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/', {
app_id : inspirecloud.env.PAY_APP_ID,
app_secret : inspirecloud.env.PAY_APP_SECRET,
});
const token = tokenRes.data.tenant_access_token;
// 设置认证头
const request = axios.create({
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': application/json; charset=utf-8 ,
},
});
return request;
}
//在某个sheet的table中,通过filter筛选记录
const listRecordsFromSheet = async function(app_token,table_id,filter){
const larkRequest = await getLarkCommonRequest();
var result = await larkRequest.get(`/bitable/v1/apps/${app_token}/tables/${table_id}/records?filter=${encodeURIComponent(filter)}`,{});
return result.data;
}
//双端工作看板的filter
const getJobPanelFilter = function(byWhat,value) {
if (byWhat == 版本 ) {
return contansFilter( 版本 ,value);
}
return null;
}
//包含关系的filter:field + value
const contansFilter = function(field,value) {
return `CurrentValue.[${field}].contains( ${value} )`;
}
获取数据的过程:调用各平台的接口获取想要的数组—> 组装成自己定义的数据结构{
code :0,
data :{
has_more :false,
items :[
{
fields :{
人力耗时 :64,
人力评估 : 8 ,
任务执行人 :[
{
en_name : Paopao Huang ,
id : ou_5fb00e0112212cc7012fe3a697336989 ,
name : 黄泡泡
}
],
任务描述 : 我是最大的功能开发🥕 ,
任务附件 :[
{
file_token : boxcnkQWfV4XbHwzDngmezMGzXe ,
name : 2.gif ,
size :10250625,
type : image/gif ,
}
],
对应 OKR :[
{
text : 新功能评审 ,
type : text
}
],
截止日期 :1612108800000,
文档地址 :{
text : 文档备份
},
是否完成 :false,
状态 : 开发中 ,
相关部门 :[
研发
]
},
record_id : recn0hoyXL
},
{
fields :{
人力耗时 :16,
人力评估 : 2 ,
任务执行人 :[
{
en_name : Paopao Huang ,
id : ou_5fb00e0112212cc7012fe3a697336989 ,
name : 黄泡泡
}
],
任务描述 : 新功能评审 ,
任务附件 :[
{
file_token : boxcnGk0FfG678EEurDN7dRyxag ,
name : Hawaii_1_15Retina_R.jpg ,
size :5069121,
type : image/jpeg ,
}
],
对应 OKR :[
{
text : 我是最大的功能开发🥕 ,
type : text
}
],
截止日期 :1612368000000,
文档地址 :{
text : 百度一下,你就知道
},
是否完成 :true,
状态 : 未进行 ,
相关部门 :[
产品 ,
设计 ,
研发
],
多行文本 :[ // text_field_as_array 为true时的结构
{
text : hello ,
type : text
},
{
mentionType : User ,
mentionNotify :false,
name : test ,
text : @test ,
token : ou_sfsdfsdfsdfsdfdsfsdfdsf ,
type : mention
},
{
mentionType : Bitable ,
text : 未命名多维表格 ,
token : basbcq2aFvW8nFJpfOXalx57ffb ,
type : mention
},
{
text : 测试链接标题 ,
type : url
}
]
},
record_id : reciKaDyVO
}
],
page_token : reciKaDyVO ,
total :2
},
msg : Success
}
4. 调用飞书openAPI生成飞书文档
创建文档的过程:
创建文档(拿到docToken)->获取创建文档所需要的信息(从各平台获取) ->编辑文档->转移文档权限给执行命令者(因为是小助手生成的,所以文档所有者是小助手,需要将文档所有者转交给执行命令者);
首先写好 :权限转移、获取文档信息、创建文档、更新文档的请求方法, 剩下的就是构造参数了:
其中 创建文档 和 转移文档权限 给执行命令者是两个固定的步骤,无需有任何代码改动;const { getLarkCommonRequest } = require('../requestUtils');
const { getPersonOpenID } = require('../msgUtils/larkMessageUtils')
const { traceError } = require('../traceUtils')
//转移文档的所有者
/**
*
* @param {*} params
* {
* token: doccncH6gR0u71ienZ27v6pAfNc , //sting 对应文件的token
* type: doc , //string 文件类型
* need_notification: true, //boolean 添加权限后是否通知对方
* member_type: openid , //string 用户类型,与路径参数中的member_id要对应,可选:(email、openid、openchat、opendepartmentid、userid)
* member_id: ou_3792c8f7ac2f4b3f8205b02510334c22 ,//stirng 用户类型下的值
* perm: edit //string 需要更新的权限,可选值有:view: 可阅读、edit: 可编辑、full_access: 所有权限
* }
* @returns
*/
const DocPermissions = async function(params) {
// console.log(params);
const larkRequest = await getLarkCommonRequest();
var result = await larkRequest.post('drive/permission/member/transfer', {
token: params.token,
type: params.type,
owner:{
member_type:params.member_type,
member_id: params.member_id
}
});
// console.log(result.data)
if (result.data.code != 0) {
await traceError(getPersonOpenID( 魏子玄 ),`转移文档所有者失败,code:${result.data.code},msg:${result.data.msg}`);
return null;
}
return result.data;
}
/**
* 获取文档的富文本内容
* @param {*} doc_token 文档的token
* @returns
*/
const DocContent = async function(doc_token) {
const larkRequest = await getLarkCommonRequest();
const result = await larkRequest.get(`/doc/v2/${doc_token}/content`, {});
if (result.data.code != 0) {
await traceError(getPersonOpenID( 魏子玄 ),`获取文档的富文本信息失败,code:${result.data.code},msg:${result.data.msg}`);
return null;
}
return result.data
}
//创建飞书文档
const DocCreate = async function() {
const larkRequest = await getLarkCommonRequest();
var result = await request.post('doc/v2/create', {});
// console.log(result.data)
if (result.data.code != 0) {
await traceError(getPersonOpenID( 魏子玄 ),`创建文档失败,code:${result.data.code},msg:${result.data.msg}`);
return null;
}
return result.data;
}
/**
* 更新文档
* @param {*} params
* @param {*} context
* @returns 非正常情况均返回null
*/
const DocUpdate = async function(params, context) {
const larkRequest = await getLarkCommonRequest();
const { doc_token, operationRequest} = params;
//无参数,报警🚔!
if (doc_token == null || doc_token == undefined || operationRequest == null || operationRequest == undefined) {
await traceError(getPersonOpenID( 魏子玄 ), 编辑文档时,未指定文档(doc_token)或未指定编辑操作(operationRequest) );
return null;
}
//获取当前文档富文本信息
const doc_token = params.token;
const doc = await DocContent(doc_token);
// console.log( doc: , doc);
if (doc == null) {
await traceError(getPersonOpenID( 魏子玄 ),'编辑文档时,获取文档信息失败');
return null;
}
//编辑当前飞书文档
var result = await request.post(`/doc/v2/${doc_token}/batch_update`, {
docToken: doc_token,
Revision: doc.data.revision,
Requests: operationRequest
});
// console.log( updataResult: ,result)
if (result.data.code != 0) {
await traceError(getPersonOpenID( 魏子玄 ),`编辑文档时,编辑失败,code:${result.data.code},msg:${result.data.msg}`);
return null;
}
return result.data;
}
获取创建文档所需要的信息则是在第三节介绍过的从各个平台获取数据。
因为不同文档的结构不同,所以要编辑文档则需要根据不同的业务需求进行改动
编辑文档接口说明:
需要关注的内容有三个:docToken(所编辑文档的token)、Revision(版本号)、Requests(更新内容)
1.docToken:决定编辑哪一个文档;(创建文档时会返回)
2.Revision:当然是编辑最新版本咯;(已知docToken,调用获取获取文档的富文本内容的接口就可以知道文档的最新版本)
3.Requests:决定文档的结构和内容;(真正需要关注的内容)
所以我们只需要关注如何构建Requests即可。
构建Requests实际上就是构建title(标题)和body(正文)。title实际上就是一行文本,body实际上就是由block组成
- 插入文本请求:
图4-2 插入文本请求说明:
250px|700px|reset
250px|700px|reset
250px|700px|reset
我们已经知道了生成文档就是构建block,那先看一段创建组内周会文档中的部分代码:
预期生成:
250px|700px|reset
let attribute_foreword = {
blocks : [
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text : 本周主持人:
}
},
{
type : person ,
person : {
openID : current_owner.open_id
}
}
]
paragraphStyle : {
align: left
}
}
},
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text : 本周会议记录:
}
},
{
type : person ,
person : {
openID : next_owner.open_id
}
}
]
}
},
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text : 本周会议纪要:
}
}
]
}
},
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text :
}
}
]
}
},
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text : 上周会议纪要:
}
}
]
}
},
{
type : paragraph ,
paragraph : {
elements : [
{
type : textRun ,
textRun : {
text :
}
}
]
}
}
]
}
- 实际上是有几个问题(飞书文档已经支持2.0,飞书openAPI暂未支持生成飞书文档):
1.代码量很大,如果有100行文本怎么办;
2.如果飞书文档结构更新了怎么办,字段名称发生变化怎么办;
生成不同文档的入口:
/**
*
* @param params {
* type: , string //更新doc的操作类型 titleAndContentsInit (标题和目录初始化)和 insertBlocksUnderTitle (在对应标题下插入内容)
* token: , string //doc的token
* titleAndContentsInit/ titleAndContentsInit是object
* insertBlocksUnderTitle: titleAndContentsInit是object[] //key与type对应
* }
* @param context
* @returns
*
*
* titleAndContentsInit: {
* title: ,
* contents:[], Contents[] //Contents:{
* value: , string //目录名称
* subContents:{} Contents[] //Contents数组
* }
* }
*
* insertBlocksUnderTitle: linkBlock[] //linkBlock:{
* title: , string //需要插到哪一个人标题的名称
* week: , string //第几周的文章
* content:[] textUrl[] //textUrl:{
* text: string //链接的名称
* url: string //链接的url
* }
* }
*/
const DocUpdate = async function(params, context) {
//const doc = createDoc(params,context)
// 首先需要获取 access token
const tokenRes = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/', {
app_id : inspirecloud.env.PAY_APP_ID,
app_secret : inspirecloud.env.PAY_APP_SECRET,
});
const token = tokenRes.data.tenant_access_token;
console.log( tenant_access_token: , token);
// 设置认证头
const request = axios.create({
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': application/json; charset=utf-8 ,
},
});
const doc_token = params.token
const doc = await request.get(`/doc/v2/${doc_token}/content`, {
});
const content = JSON.parse(doc.data.data.content)
console.log( doc: , doc);
var operationRequest = createRequests([])
if(params.type == titleAndContentsInit ){
operationRequest = create_TitleAndContentsInit_Requests(params.titleAndContentsInit)
} else if (params.type == insertBlocksUnderTitle ){
console.log( insertBlocksUnderTitle )
operationRequest = create_InsertBlocksUnderTitle_Requests(content,params.insertBlocksUnderTitle)
} else if (params.type == initVersionDaily ) {//生成版本日报
operationRequest = await create_InitVersionDaily_Requests(params.initVersionDaily)
} else if (params.type == createAndroidPayWeekly ) {
operationRequest = await createAndroidPayWeeklyRequest(params)
} else if (params.type == createCJWeekly ) {
console.log( createCJWeekly )
operationRequest = await createCJWeeklyRequest(params)
}
//console.log( operationRequest: ,operationRequest)
var result = await request.post(`/doc/v2/${doc_token}/batch_update`, {
docToken: doc_token,
Revision: doc.data.data.revision,
Requests: operationRequest
});
console.log( updataResult: ,result)
return result.data;
}
- 生成版本日报的数据(在调用编辑文档接口前获得,作为参数传入的):
versionDailyBean:
{
version = 6.0.2 //sdk版本
frozenDate = 10.28 ,//封板日
versionDailyDate,//各种封板时间
android_items = [],//安卓需求
ios_items = [],//ios需求
com_all_items = [[],[]]//双端需求,com_all_items[0]-android看板数据,com_all_items[1]-ios看板数据
}
所以当字段名称改变时,只需要修改createParagraphBlock、createParagraphStyle、createParagraphElement处即可。相比于直接写json对象,代码量已经少多了,还可以哪些地方可以优化?async function create_InitVersionDaily_Requests(versionDailyBean){
//拿数据start
const versionDailyDate = versionDailyBean.versionDailyDate
//拿数据end
//构建标题start
const titleRequest = createUpdateTitleRequest(`支付SDK${versionDailyBean.version}版本日报`,undefined)
const insertLocation = new Location( 0 ,undefined,true,undefined)
//构建标题end
//构建正文start
var blocks = new Array()
const titleParagraphStyle_1 = createParagraphStyle(1,undefined,undefined,undefined, left )
const titleParagraphStyle2 = createParagraphStyle(2,undefined,undefined,undefined, left )
const titleParagraphStyle3 = createParagraphStyle(3,undefined,undefined,undefined, left )
const timeParagraphElement = createParagraphElement( textRun , 时间节点 ,undefined)
const timeBlock = createParagraphBlock(titleParagraphStyle_1,[timeParagraphElement])
blocks.push(timeBlock)
const textParagraphStyle_1 = createParagraphStyle(undefined,undefined,undefined,undefined, left )
const timeParagraphElement_1 = createParagraphElement( textRun ,`封板时间:${versionDailyDate.codeFrozenDate}封板`,)
const timeParagraphElement_2 = createParagraphElement( textRun , 本版本App跟版情况: ,undefined)
const emptyParagraphElement = createParagraphElement( textRun , ,undefined)
blocks.push(createParagraphBlock(textParagraphStyle_1,[timeParagraphElement_1]))
blocks.push(createParagraphBlock(textParagraphStyle_1,[timeParagraphElement_2]))
const tableBlock = await create_InitVersionDaily_tableBlock(versionDailyBean.version,versionDailyDate)
console.log( ----->tableBlock: ,tableBlock)
blocks.push(tableBlock)
blocks.push(createParagraphBlock(textParagraphStyle_1,[emptyParagraphElement]))
blocks.push(createParagraphBlock(textParagraphStyle_1,[emptyParagraphElement]))
const todoParagraphElement = createParagraphElement( textRun , TODO ,undefined)
const todoBlock = createParagraphBlock(titleParagraphStyle_1,[todoParagraphElement])
blocks.push(todoBlock)
const todoParagraphElement_0 = createParagraphElement( textRun , X月X日 ,undefined)
blocks.push(createParagraphBlock(titleParagraphStyle2,[todoParagraphElement_0]))
const todoParagraphElement_1 = createParagraphElement( textRun , 昨日TODO ,undefined)
blocks.push(createParagraphBlock(titleParagraphStyle3,[todoParagraphElement_1]))
const todoParagraphElement_2 = createParagraphElement( textRun , 今日TODO ,undefined)
blocks.push(createParagraphBlock(titleParagraphStyle3,[todoParagraphElement_2]))
blocks.push(createParagraphBlock(textParagraphStyle_1,[emptyParagraphElement]))
blocks.push(createParagraphBlock(textParagraphStyle_1,[emptyParagraphElement]))
const featureParagraphElement = createParagraphElement( textRun , 需求列表 ,undefined)
const featureBlock = createParagraphBlock(titleParagraphStyle_1,[featureParagraphElement])
blocks.push(featureBlock)
const itemParagraphStyle_1 = createParagraphStyle(undefined,undefined,new List( bullet ,1,undefined),undefined, left )
const itemParagraphStyle_2 = createParagraphStyle(undefined,undefined,new List( bullet ,2,undefined),undefined, left )
for (var t = 0; t < 3 ;t++) {
let curItems = [];
let curIOSItem = [];
let itemsSoucesName = ;//Android单端需求、iOS单端需求、双端需求
if (t==0) {
curItems = versionDailyBean.com_items;
curIOSItem = versionDailyBean.com_all_items[1];
itemsSoucesName = 双端需求 ;
} else if (t==1){
curItems = versionDailyBean.android_items;
itemsSoucesName = Android单端需求 ;
} else {
curItems = versionDailyBean.ios_items;
itemsSoucesName = iOS单端需求 ;
}
if (curItems.length == 0) {
continue;
}
blocks.push(createParagraphBlock(createParagraphStyle(2,undefined,undefined,undefined, left ),[createParagraphElement( textRun ,`${itemsSoucesName}`,undefined)]))
for (var i = 0; i < curItems.length; i++){
//需求名字
const list = new List( number ,1,i+1)
const titleParagraphStyle_3 = createParagraphStyle(3,undefined,list,undefined, left )
const featureNameParagraphElement = createParagraphElement( textRun ,`${curItems[i].fields.任务描述}`,undefined)
blocks.push(createParagraphBlock(titleParagraphStyle_3,[featureNameParagraphElement]))
//需求负责人
var ownerParagraphElements = []
const ownerParagraphElement_1 = createParagraphElement( textRun ,`各方面负责人:Android:`,undefined)
ownerParagraphElements.push(ownerParagraphElement_1)
if (itemsSoucesName == 双端需求 ) {
for (var j = 0; j < curItems[i].fields.负责人.length; j++){
console.log( 负责人: ,` ${curItems[i].fields.负责人[j].id} `)
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.负责人[j].id}`,undefined))
}
ownerParagraphElements.push(createParagraphElement( textRun ,` iOS:`,undefined))
if (curIOSItem[i].fields.hasOwnProperty ( 负责人 ) && curIOSItem[i].fields.负责人 != null){
for (var j = 0; j < curIOSItem[i].fields.负责人.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curIOSItem[i].fields.负责人[j].id}`,undefined))
}
}
} else if (itemsSoucesName == Android单端需求 ) {
for (var j = 0; j < curItems[i].fields.负责人.length; j++){
console.log( 负责人: ,` ${curItems[i].fields.负责人[j].id} `)
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.负责人[j].id}`,undefined))
}
ownerParagraphElements.push(createParagraphElement( textRun ,` iOS:`,undefined))
if (curItems[i].fields.hasOwnProperty ( IOSRD ) && curItems[i].fields.IOSRD != null){
for (var j = 0; j < curItems[i].fields.IOSRD.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.IOSRD[j].id}`,undefined))
}
} else {
ownerParagraphElements.push(createParagraphElement( textRun , ,undefined))
}
} else {
if (curItems[i].fields.hasOwnProperty ( AndroidRD ) && curItems[i].fields.AndroidRD != null){
for (var j = 0; j < curItems[i].fields.AndroidRD.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.AndroidRD[j].id}`,undefined))
}
} else {
ownerParagraphElements.push(createParagraphElement( textRun , ,undefined))
}
ownerParagraphElements.push(createParagraphElement( textRun ,` iOS:`,undefined))
if (curItems[i].fields.hasOwnProperty ( 负责人 ) && curItems[i].fields.负责人 != null){
for (var j = 0; j < curItems[i].fields.负责人.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.负责人[j].id}`,undefined))
}
} else {
ownerParagraphElements.push(createParagraphElement( textRun , ,undefined))
}
}
ownerParagraphElements.push(createParagraphElement( textRun ,` 测试:`,undefined))
if (curItems[i].fields.hasOwnProperty ( 测试 ) && curItems[i].fields.测试 != null){
for (var j = 0; j < curItems[i].fields.测试.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.测试[j].id}`,undefined))
}
} else {
ownerParagraphElements.push(createParagraphElement( textRun , ,undefined))
}
ownerParagraphElements.push(createParagraphElement( textRun ,` 后端:`,undefined))
if (curItems[i].fields.hasOwnProperty ( 后端 ) && curItems[i].fields.后端 != null){
for (var j = 0; j < curItems[i].fields.后端.length; j++){
ownerParagraphElements.push(createParagraphElement( person ,`${curItems[i].fields.后端[j].id}`,undefined))
}
} else {
ownerParagraphElements.push(createParagraphElement( textRun , ,undefined))
}
blocks.push(createParagraphBlock(itemParagraphStyle_1,ownerParagraphElements))
//需求文档
var featureDocParagraphElements = []
const featureDocParagraphElement_1 = createParagraphElement( textRun ,`需求文档:`,undefined)
featureDocParagraphElements.push(featureDocParagraphElement_1)
if (curItems[i].fields.需求文档!=undefined){
const textStyle = new TextStyle(undefined,undefined,undefined,undefined,undefined,undefined,undefined,new Link(encodeURIComponent(curItems[i].fields.需求文档.link)))
const featureDocParagraphElement_2 = createParagraphElement( textRun ,curItems[i].fields.任务描述,textStyle)
featureDocParagraphElements.push(featureDocParagraphElement_2)
}
blocks.push(createParagraphBlock(itemParagraphStyle_1,featureDocParagraphElements))
const keyTimeParagraphStyle_1 = createParagraphStyle(undefined,undefined,new List( bullet ,1,undefined),undefined, left )
const keyTimeParagraphStyle_2 = createParagraphStyle(undefined,undefined,new List( bullet ,2,undefined),undefined, left )
const keyTimeParagraphElement_1 = createParagraphElement( textRun , 关键时间点: ,undefined)
const keyTimeParagraphElement_2 = createParagraphElement( textRun , 提测时间: ,undefined)
const keyTimeParagraphElement_3 = createParagraphElement( textRun , 测试时间: ,undefined)
const keyTimeParagraphElement_4 = createParagraphElement( textRun , 上线时间: ,undefined)
const keyTimeParagraphElement_5 = createParagraphElement( textRun , 当前进度: ,undefined)
const keyTimeParagraphElement_6 = createParagraphElement( textRun , 开发进度: ,undefined)
const keyTimeParagraphElement_7 = createParagraphElement( textRun , 测试进度: ,undefined)
const keyTimeParagraphElement_8 = createParagraphElement( textRun , 埋点验收: ,undefined)
const keyTimeParagraphElement_9 = createParagraphElement( textRun , 风险: ,undefined)
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_1]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_2]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_3]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_4]))
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_5]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_6]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_7]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_8]))
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_9]))
}
}
//构建正文start
//最后将titleRequest和contentsRequest合并为operationRequests
const contentsRequest = createInsertBlocksRequest(insertLocation,blocks)
var operationRequests = []
operationRequests.push(titleRequest)
operationRequests.push(contentsRequest)
console.log( end create_InitVersionDaily_Requests )
return createRequests(operationRequests)
}
1.段落样式,可以统一管理,例如一个左对齐的paragraphStyle其实只需要写一次
2.可以使用递归来实现:
- 关键时间点:
- 提测时间:
- 测试时间:
- 上线时间:
- 当前进度:
- 开发进度:
- 测试进度:
- 埋点验收:
- 风险:
//改进前:
const keyTimeParagraphStyle_1 = createParagraphStyle(undefined,undefined,new List( bullet ,1,undefined),undefined, left )
const keyTimeParagraphStyle_2 = createParagraphStyle(undefined,undefined,new List( bullet ,2,undefined),undefined, left )
const keyTimeParagraphElement_1 = createParagraphElement( textRun , 关键时间点: ,undefined)
const keyTimeParagraphElement_2 = createParagraphElement( textRun , 提测时间: ,undefined)
const keyTimeParagraphElement_3 = createParagraphElement( textRun , 测试时间: ,undefined)
const keyTimeParagraphElement_4 = createParagraphElement( textRun , 上线时间: ,undefined)
const keyTimeParagraphElement_5 = createParagraphElement( textRun , 当前进度: ,undefined)
const keyTimeParagraphElement_6 = createParagraphElement( textRun , 开发进度: ,undefined)
const keyTimeParagraphElement_7 = createParagraphElement( textRun , 测试进度: ,undefined)
const keyTimeParagraphElement_8 = createParagraphElement( textRun , 埋点验收: ,undefined)
const keyTimeParagraphElement_9 = createParagraphElement( textRun , 风险: ,undefined)
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_1]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_2]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_3]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_4]))
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_5]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_6]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_7]))
blocks.push(createParagraphBlock(itemParagraphStyle_2,[keyTimeParagraphElement_8]))
blocks.push(createParagraphBlock(itemParagraphStyle_1,[keyTimeParagraphElement_9]))
//改进后:
/**
* @function createBulletBlocks 生成无序Blocks
* @param {int} indentLevel [1,16] 缩进,1到16可选,一般使用的时候直接置1就好了
* @param {item[]} contents 是一个item数组
* item的结构:
* {
* elements:[],
* subElements:item[] //当没有子内容时,可以不需要这个字段,或者是个空数组
* }
* @returns
*/
function createContentsInitBlocks(headingLevel,contents){
var blocks = new Array();
if(headingLevel < 1 || headingLevel > 16){
console.log( headingLevel is not between 1 and 16 )
return blocks
}
for(var i = 0; i < contents.length; i++){
const paragraphStyle = createParagraphStyle(headingLevel,undefined,undefined,undefined, left )
const paragraphElement = createParagraphElement( textRun ,contents[i].value,undefined)
const block = createParagraphBlock(paragraphStyle,[paragraphElement])
blocks.push(block)
if(contents[i].hasOwnProperty('subContents') && contents[i].subContents.length != 0){
blocks.push.apply(blocks,createContentsInitBlocks(headingLevel+1,contents[i].subContents))
}
}
return blocks
}