案例高仿项目协作工具【Worktile】,从零带你一步步实现组织架构、网盘、消息、项目、审批等功能
声明
本课程于2020年8月6日仿自北京易成星光科技开发的Worktile,相关素材版权归属于原网站,本课程作为仿站习作,仅做教学用,不得用作商业用途,商业合作请访问源站。
课程开发前试用了市面上几乎所有企业协作办公平台,选中Worktile是觉得它是当时最优秀的,也喜欢它的UI风格,推荐有需要的朋友选购。
本课程目的是演示一个复杂的协同类OA的SaaS应用软件,基于众触这样一个专业的低代码应用平台,如何开发一个复杂的功能完整的SaaS应用。Worktile是专为企业办公场景设计的项目协作管理系统,业务概念多,关系错综复杂;虽然大家很可能不需要做到这种复杂度的应用,但学习里面用到的思想和用法是非常有价值的。
源站随着时间的推移可能会不断迭代演变,而课程作品并不会一起跟着变化,所以作品跟你现在看到的源站不同是正常的。
本课程有非常详尽的视频讲解每个功能模块,都是是从空白页面开始一步步实现的:总览、组织架构、网盘、即时消息、项目管理、审批。
账号
为了演示公司的组织架构,虚拟了以下人员:
CEO:戴国强 13845678901 管理员
总经办经理:赵敏 13845678911 管理员
人事总监:陈颖 13845678902 管理员
财务总监:付媛 13845678906 管理员
项目一组组长:曾辉 13845678312
项目一组开发:黄鸿亮 13845678909
项目一组开发:黄姚谦 13845678223
技术总监:谢林华 13845678907
项目组经理:孙荣浩 13845678905
平台组组长:葛传富 13845678910
测试组组长:王伯祥 13845678908
尚未分配部门:黄飞凯 13845678102
这些人物和手机都是瞎编的,头像来自我来笔记。
本课程应用作为供学习者克隆的模板,数据不能太乱,没有提供演示账号也没开放注册,所以即使克隆了本应用也还不能直接登录。克隆以后到登录页面(/z/login),它使用的是账号登录管理插件,右键选中插件,在右边面板激活“注册”后保存再刷新页面即可注册了,注册后自己有账号可以登录了但还是没有密码以上面这些人员的账号登录,可以先在”后端服务“中注释掉$user.setRole里的限制,再在”成员管理“中点击某个成员的”设置“,在”修改成员账号“标签页中修改此成员的密码后就有密码以此人身份登录了。详细指导请移步教学视频。
准备深入研究的同学请注册登录众触官网后,点击克隆按钮,把整个应用复制一份随意调试更改吧。
全局$V.s
全局可持久化状态,s
是status首字母。应用开始时从localStorage读取,应用结束时(onbeforeunload)存回localStorage,这样可以立即恢复回上次离开时的状态。
$V.s.左导航:最新点开的左侧导航栏标签
$V.s.消息:最新点开的私聊联系人或群组的_id
$V.s.未读:各私聊或群组的最新未读消息的个数
$V.s.已读:各私聊或群组的最新已读消息的时间
$V.s.折叠:各种被折叠的菜单
$V.s.项目:最新点开的项目_id及其组件下标和视图下标
$V.s.任务栏:以项目为单位固定到任务栏的任务列表
全局事件
click:当有弹窗
$v.pop
时关闭弹窗,但keep
为真时点击弹窗内部或者zpage以外(confirm或alert)不关闭。keydown:当有弹窗或模态窗时按下退出键
Escape
可关闭弹窗或模态窗。
组织架构
公司的组织架构是其他功能的基础。 可以以树状结构管理各部门及其成员,也可以从通讯录里查看同事正在经手的任务状态。
职位列表
可添加、修改、上移、下移、删除。用户登录时($c.exp.login)拉取公司全员工信息和公司组织架构,即$c.exp.部门。
对员工和组织架构整合到全局变量中,方便应用的很多地方使用。
$V.部门
是组织架构的克隆,除了增加了个”未分配部门“$V.部门成员
是把组织架构拍平后把各部门成员加进去$V.部门总成员
是在$V.部门成员基础上再把所有子孙部门成员都加进去成员管理
基本信息设置,修改成员账号,角色设置调整部门
可以选中成员(单选或全选)后可批量调整他们的部门成员搜索(过滤)
重点、难点:
移动就是交换位置,常用原生函数
arr.splice(新下标, 0, arr.splice(旧下标, 1)[0])
,先从旧下标删除一个元素,删除得到的是数组,所以后面加个[0]
取得元素本身,再把被删除元素插入到新下标中。
也就容易理解$l.arr.splice($i - 1, 0, $l.arr.splice($i, 1)[0])是上移,$l.arr.splice($i + 1, 0, $l.arr.splice($i, 1)[0])是下移了。
等学习了后面课程掌握了如何使用Sortable.js,回过头来把这里的上移和下移按钮改成拖动排序将使操作更简单和直观。在数据库中删除数组的某个元素常用$pull,即从数组中拉走某个元素:
$xtk.modify("公司", "职位", {$pull: {"x.arr": $x}})数据结构
部门是以树形结构存储的,部门有名称、主管,zchildren数组放子部门。
数据组件的数据源包含zchildren时会递归渲染成树形结构。子部门缩进
为了体现部门层次结构在动态样式里根据层次深度($indexes)计算左缩进(paddingLeft)部门调整或成员更新后通过
$v.F5
让挂载组件获取最新数据重新渲染删除成员的部门用的是
$unset
。留意“未分配部门”这个特殊部门。删除子部门用的是
$pull
,从数组中“拉走”。
通讯录
同事列表
同事详情
同事任务列表及状态数
发消息
常用联系人
收藏
按部门查找同事
网盘
类似百度网盘、阿里网盘,以虚拟文件夹的形式分层分类来管理云端存储的文件,可批量上传整个文件夹。
网盘展示
读取文件夹及文件信息。
文件夹树
文件路径
搜索
排序
重点、难点:
树形数据结构
文件夹只是存储于product表中的简单记录,包含名称
、颜色
、父
(即父文件夹)信息,而非直接的树形结构,但我们要根据“父”构造出类似于上节课的树形组织架构来,作为数据组件的数据源。
文件夹可以有非常深的层次结构,不像组织架构那么单一稳固,不能一次性把所有的文件夹都读取出来。我们在ready先读取顶层文件和顶层文件夹(其父文件夹是"无"),并依次读取各自直接子文件夹(根据有无子文件夹决定是否显示“展开”小三角图标)。
读取文件或文件夹的一个要点是把父文件夹_id传入执行环境:{ 父: _id }
。
文件是上传到对象存储服务器的普通资源,并不无文件结构树信息,所以我们每次上传完文件后要给资源表添加当前文件夹作为其父文件夹:
$resource.modify(_id, { "x.类型": "网盘", "x.父": 当前文件夹_id })几个关键变量
$v.根
是文件夹结构树的根(名称唤作“网盘”吧,文件路径的第一个节点),是最大的一棵树。它的“zchildren”包含上面读取顶层文件夹,”文件“包含顶层文件。特别地,它的_id为”无“。$v.树
是以父文件夹_id作为key的各级文件夹起始的大大小小的树。所以$v.树.无
也即$v.根
。$v.路径
是树根到当前文件夹各级文件夹树的数组$v.文件夹
是当前文件夹_id。用户点击时(无论是左侧文件结构树还是主体文件夹列表)会以它作为父文件夹读取它里面的文件夹和文件信息。另外,点击”展开“小三角图标时只需读写孙辈文件夹信息,一方面树里不展示文件信息,另一方面子文件夹已经读取,孙辈文件夹信息是为了决定是否展示子文件夹的小三角。文件夹
动态颜色重新渲染
当$v.F5
变化时有三个挂载组件会重新组装数据并再次渲染。
A. 左侧文件结构树里的挂载组件就是用来组装$v.树
的,从根节点开始向下递归遍历所有已经搜索出来的文件夹:一方面把文件夹直接挂载$v.树
上,另一方面把它推入父文件夹的zchildren数组中。
B. 顶部的文件路径里的挂载组件就是用来组装$v.路径
的,从当前文件夹开始向上递归直至根节点,把每层文件夹推入路径数组头部(unshift)。
C. 主体文件列表里的挂载组件就是用来组装当前文件夹里的文件列表的。搜索、排序
我们总是先展示文件夹列表,然后才是文件列表,是分开搜索分开排序的,再把两种拼接起来:
($v.搜索.文件夹 || $v.树[$v.文件夹].zchildren || []).sort($v.sort.key, $v.sort.incr).concat(($v.搜索.文件 || $v.树[$v.文件夹].文件 || []).sort($v.sort.key, $v.sort.incr))
网盘上传
新建文件夹
上传文件(可多选)
上传文件夹(保持文件结构)
上传列表,上传进度和状态
重点、难点:
上传文件
按钮触发的是隐藏在里面的input元素:$el.firstElementChild.click()。
把上传成功后才要用到的$l.U
在刚开始选择文件(change时)就设置了是为了防止上传过程中受到用户点击了其他文件夹导致$v.文件夹发生变化的影响,而刷新用户当前文件列表($v.exp.文件.exc({父: $v.文件夹}))则没有这个忧虑。上传文件夹
为了避免受$v.文件夹变化的影响先赋值给临时变量:$l.文件夹 = $v.文件夹
把整个文件夹递归上传的关键是每个待上传文件有个相对路径webkitRelativePath
,循环每个路径节点,创建未创建的文件夹节点,直到最里层的文件夹赋给$l.当前文件夹,上传完成后把它作为父文件夹。上传列表
文件上传前把信息推入$v.上传
数组,标识为”正在上传“,如果是图片或视频给它创建一个缩略图:URL.createObjectURL(file),上传过程中及时改变它的进度样式,上传完成后删除”正在上传“标识。
网盘管理
重命名
移动
删除
修改颜色
发送到聊天
回收站
重点、难点:
我们删除文件夹或文件是让它脱离原来的位置,但随后有还原操作,所以还要保留原来位置的信息。
我们通过把父文件夹改成原父文件夹的形式来实现
$rename: {"x.父": "x.原父"}
这样没有父文件夹的资源就是被逻辑删除的。
"x.原父": {$exists: true}
即时消息
类似企业微信/飞书/钉钉的即时消息,支持私聊、群聊、历史消息、置顶,可以发送表情、图片、网盘文件、项目和审批到会话,还可以收藏文件、固定消息。 是学习连接的好课程。
消息初始化
获取私有群(群成员包含我)和公开群的列表信息,并把它们的_id组成列表
$V.消息.群ID
。打开连接
$socket.open()
:
群组_id列表作为channel
,这样可收到相关群组的消息;saveToDB
设为true,把信息都保存到数据库,这样通过$socket.hist()可以查看历史消息,即时不在线是别人发送的未读消息也能收到;login
设为2,这样打开的多个设备/浏览器都可以同时收到消息。onConn 已连上
通过$socket.hist()
获取所有私聊的历史消息。
通过$socket.hist($V.消息.群ID)
获取所有私有群和公开群的历史消息。
给各消息添加发送消息日期,为了方便在消息列表中显示分割用的日期和星期。
获取置顶会话的_id列表。
根据私聊对象_id或群聊_id:
把各消息列表放入$V.消息.chat
中;
把各更多消息的列表放入$V.消息.more
中,每当聊天窗口滚到顶部时就从中拿出一个_id去获取更早的历史消息;
把各消息_id放入$V.消息.histID
中,删除历史消息时用;
把各消息的发送时间大于其已读时间$V.s.已读
的消息条数放入其$V.s.未读
中;onData 收到新消息
根据聊天对象(私聊对象_id或群聊_id)把消息放入对应的消息列表$V.消息.chat
中。
如果此消息是当前正在打开的聊天会话中,则渲染后把新消息滚入视图,否则给其未读消息数加 1。
如果此消息包含@我的信息,弹出一个通知窗口notification(如未被阻止)。
消息会话
创建群组/群组设置
加入群组
发起私聊
会话列表。无论置顶会话是否有消息都全部先展示出来,然后展示除去置顶的有消息的会话:
$V.消息.置顶.concat($V.消息.chat.keys().filter('!$V.消息.置顶.includes($x)'))会话过滤:
.filter('$f.search.kw ? ($c.user[$x].x.姓名 || $c.xdb[$x].x.name).includes($f.search.kw) : 1')未读消息,包含历史未读和即时未读新消息。
未读总数:$V.s.未读.values().reduce('$acc + $x', 0)点击会话时
把会话ID赋给$V.s.消息,渲染消息列表后滚到最新一条消息
如果未读消息数小于6时把所有消息设为已读(清空未读并记录最新消息的发送时间)删除会话
A. 删除关联会话的文件列表
B. 删除文件列表中的文件本身
C. 删除关联会话的固定消息列表
D. 删除整个消息历史。
即时消息
消息列表
$V.消息.chat[$V.s.消息]当跟前一个消息不是同一天时展示日期分割。
date !== $arr[$i - 1].date把自己发送的消息向右对齐,以区别于别人发送的消息
$c.me._id === from ? " message-item--me" : ""消息弹出框
固定消息,删除自己发送的消息用
HTML组件
渲染消息体交叉观察器
如果还有更多消息未读取出来时($V.消息.more[$V.s.消息].length),当滚到消息框顶部是会触发交叉观察器从$V.消息.more
里拿出一项交给$socket.more()来获取更多消息,并放入消息列表头部,渲染后滚到第一个消息但交叉观察器却在窗口外的位置,这样避免连续触发,而是等用户再往上滚的时候又会触发一次。
一个注意点是引入临时变量$v.l.more来阻止每次新渲染交叉观察器时导致的首次执行,因为此时并不是由有户主动往上滚引起的。未读消息
展示未读消息数,点击时往上滚到上次的已读消息或者消息框顶部以触发获取更多消息的交叉观察器,然后触发“全部标记为已读”发送文本消息
把[表情文本]
替换成对应的img:.replace($c.reg.表情, $c.fun.表情);
@某人:在群组会话中按下@时(即Shift + 2,2的keyCode是50)弹出排除自己的群组成员列表,但在已弹窗时按下非Shift键(keyCode为16)则关闭弹窗;
把@24位的某人_id
替换成对应的姓名:.replace($c.reg.提及, $c.fun.提及);点击时还有成员
弹窗是因为添加了onClick事件:window.提及click();
$socket.send($V.s.消息, "text", $f.消息.txt);发送图片/文件消息
点击文件图标是弹出上传对话框,选择一个或多个文件后上传至服务器;
然后根据不同的文件类型转化成对应的HTML文本:i
为图片,直接显示图片缩略图;v
为视频,显示对应的视频截图;f
为其它文件类型,会根据其文件后缀format来显示不同的图标);
消息发送后把文件信息添加到此会话的文件列表中。
值得注意的是消息发送完了并没有立即操作数据库而是先把内容放在变量$v.待保存文件消息中,等待消息返回后在$c.exp.onData再执行,因为考虑到以后删除文件时也要把它从消息历史中删除,删除时要用到消息发送时间d:$socket.pull(_id, from, d),而这个时间是在服务器端生成的,只有收到消息后才知道。发送项目任务/审批/网盘消息
其它
文件列表
从网盘发送到聊天的文件不在此列表中
删除一个文件要:
A. 删除文件本身
B. 从关联会话的文件列表中移除
C. 消息历史中移除对应消息。固定消息列表
删除时从关联会话的固定消息列表中移除点击成员图标
群组时显示群组成员,可进一步显示成员资料或移除成员
私聊时显示成员资料点击设置图标
置顶会话和取消置顶会话
群组设置
退出群组
删除群组
项目管理
项目协作管理是Worktile的核心功能,用来计划、组织和跟进一些具有明确目的且相互关联的任务。 支持高度灵活的个性化配置以适应各种场景的项目需求。
Worktile以项目化的方式计划、管控和跟进多种工作领域和场景中的任务。
项目管理概念
项目
任务集合中心,用来计划、组织和跟进一些具有明确目的且相互关联的任务。项目可以是长期的也可以是短期的。
组件
任务的展示方式,将任务信息以不同的视觉形式呈现给用户,有多种组件类型:看板、列表、表格、甘特图、日历、报表。
视图
组件可以有多种视图来对任务进行分组、筛选、排序。
任务类型
承载不同业务场景的模型,由高度结构化的任务属性构成,可规定任务类型之间的关系和状态流转方式,从而形成信息存放和团队协作的规范。
属性:任务类型可以指定此类型的任务可包含哪些自定义信息,有多种属性类型:文本、富文本、数字、日期、成员、多个成员、下拉单选、下拉多选、继承。
任务
某种任务类型的实例。每个任务都有标题、状态,还有负责人、参与人、起止时间、评论等,主体部分是任务类型里定义的各种属性。
建议大家克隆本课程后尝试扩展更多属性类型,甚至扩展组件类型。
数据模型
项目:
{
名称: String,
任务类型: [ID],
描述: String,
颜色: String,
置顶: Boolean,
参与人: [ID],
组件: [{
类型: String,
名称: String,
任务类型: ID,
PC显示: [String],
视图: [{
名称: String,
分组: String,
筛选: String,
排序: String
}]
}]
}
任务类型:
{
名称: String,
项目: ID,
父任务类型: ID,
图标: String,
描述: String,
状态: [{
名称: String,
颜色: String,
类型: Number,
默认: Boolean
}],
状态流: {},
属性: [{
字段: String,
类型: String,
选项: [],
新建: Boolean,
必填: Boolean
}]
}
任务:
{
标题: String,
项目: ID,
任务类型: ID,
父任务: ID,
关联任务: [ID],
负责人: ID,
开始时间: Number,
截止时间: Number,
评论: [{
txt: String,
auth: ID,
d: Date
}],
状态: {
名称: String,
颜色: String,
类型: Number
},
属性: { ... } // 例子如下
}
其中任务属性是存储它所属任务类型属性指定的具体数据,比如招聘任务的属性:
{
职位: String,
部门: String,
薪资范围: String,
需求人数: Number,
在职人数: Number,
JD描述: String,
参与人: [ID]
}
安装数据库管理插件可以更直观地理解数据模型。
初始化
加载项目
加载我参与的私有项目
加载公开项目
把以上两种项目合并后抽取简要信息组成
$v.项目.菜单
列表从
$v.项目.菜单
过滤出$v.项目.置顶
列表如果有
$V.s.项目._id
就继续加载此项目信息加载此项目的任务类型
加载此项目的所有任务
恢复此项目的
任务栏
数组
任务整理
把上面加载的原始数据整理到$v.任务
。
如果任务类型里有指定父任务类型,就把它添加到其父任务类型的
$v.任务.子任务类型
里把各任务类型里自定义的属性赋给对应的
$v.任务.属性
列表中为了方便读取,继续把上述属性列表根据它里面的字段赋给对应的
$v.任务.字段
对象中不更改原$c常量,克隆所有任务数据赋给
$v.任务.arr
数组,并根据其_id组成$v.任务.O
对象如果一个任务有父任务,那就把该任务根据任务类型添加到其父任务的子任务数组
x.子任务
里,此外递归往上寻找父任务链条组成任务链数组x.父任务arr
,作为导航栏展示在任务弹窗顶部对于任务类型里定义的继承类型的属性,给其下所有任务里的此属性添加继承字段
分组整理
把上面加载的原始数据整理到$v.分组
把当前活跃的项目组件(即$V.s.项目.组件)赋给
$v.分组.组件
把当前活跃组件的任务类型下的任务,经过当前活跃视图筛选要求过滤后,根据当前活跃视图规定的分组字段进行分组,把每个任务添加到
$v.分组.O[分组属性]
数组中,不同的分组属性组成$v.分组.arr
数组根据任务状态统计各分组下的任务数,并合计分组总数
根据当前活跃视图的排序要求给各分组里的任务列表进行排序
把当前任务类型的
$v.任务.字段
赋给$v.分组.字段
对甘特图组件做额外处理
任务活动
把任务活动封装成函数,以方便在多个地方任务发生变化时调用:把传入的事件参数添加上变更人和变更时间后插入到任务对应的xtk数组头里。
任务弹窗
从其他页面跳转过来带有“任务”参数时($query.任务)弹出任务模态框。
resize
当窗体不足以容纳所有组件时把后面的组件/视图收到“更多”里面。 $v.项目.显示
表示可以容纳的组件数。
项目
项目列表、搜索
置顶、取消置顶
查看归档任务、激活归档任务
逻辑删除项目
置顶列表
回收站
创建项目
抽屉
抽屉把手.drawer-handler点击时会切换$V.s.折叠.左菜单的状态从而切换.drawer-wrap的类名drawer-wrap-collapse。组件列表
只展示特定个数:组件.slice(0, $v.项目.显示)更多组件
组件.slice($v.项目.显示, 组件.length)
选中更多列表里的组件时显示组件名称:$V.s.项目.组件 >= $v.项目.显示 ? 组件[$V.s.项目.组件].名称 : "更多"视图和更多视图
任务类型
新建任务类型与修改任务类型
属性配置
属性是保存到数据库中的关于任务的字段。
预定义属性:当前状态、负责人、开始时间、截止时间添加自定义属性
“属性类型”决定了属性的展示方式,选定后不可更改。
如有“父任务类型”则添加“继承”属性,有专属的“源属性名称”下拉框以选择可继承属性,包括父任务类型的预定义属性和自定义属性,继承可递归/追溯到顶级祖宗任务。定义自定义属性
选中一个自定义属性后配置其属性。
“属性名称”是展示和保存到数据库的字段名,下拉框有选项列表,打开“展示在新建页上”开关会在“新建任务”弹窗中展示,并出现“必填”选项,“必填”开关会在属性名前添加红色星号,并在入库前检查是否已填。排序与删除自定义属性
状态配置
状态列表决定了一个任务的所有可能的状态。
状态有名称、颜色、类型和备注。
状态有3中类型:未开始、进行中、已完成,决定了任务分组中的进度条的3种颜色。
初始状态是新建任务的默认状态,所以只可能有一个初始状态。状态流
状态流是指特定状态可变更成哪种几种状态,未勾选或已清除状态流则一个状态可以变更成任何一种状态。添加、编辑、移除、保存状态
组件
添加、编辑、删除、排序组件
每种任务类型可以添加多种不同形式的组件
但报表组件与任务类型无关,一个项目也只需要一个报表组件组件视图
每个组件可以有各种不同视图,定义其分组、筛选、排序的方式。
编辑、添加、删除PC显示
定义在PC端显示任务列表时应该显示哪些属性
添加、删除、排序要显示的属性看板
看板的主要特点是可以拖拽分组或排序
拖拽一个任务到另一个分组时要修改其分组的字段到新组名
拖动这个分组以排序会把组名列表存入到视图的”组名arr“数组里
当打开右下角的”拖拽排序“开关时会触发旁边的挂载组件里的表达式,加载并配置SmoothDnD库。表格、列表
两者类似,区别在于表格用table呈现,把字段名展示在表格头,列表用div呈现,字段名和它的值放在一起展示。日历
日历用的是FullCalendar插件,在挂载组件里的表达式里加载并配置。甘特图
下面专节讲解报表
有个挂载组件加载highcharts插件并初始化$v.任务.状态
和$v.任务.任务类型
。
每个图表都是直接渲染在各自的挂载组件$el
上面,在$obj.option
里定义其动态选项,在挂载事件里添加动态数据。
点击每个数量都会弹窗展示其任务列表。
甘特图
挂载时初始化
$v.甘特
move函数:每调用一次都会在时间轴添加一周,传入负值时就把时间轴往左延伸,轴首 - 7,正值便往右,轴尾 + 7,在队首或队尾推入新增的一周(包括年月日信息)。
日期计算比月份复杂点,我们以当周的星期几为基点,前面的日期推入队首(unshift),后面的日期推入队尾(push)。值得注意的是延伸过程中日期有可能是小于0或者大于本月最大天数,那就意味着日期跨月份了,小了就要加上上月天数,大了要减去本月天数。
月:也包含年份,用来渲染年月轴
日:用来渲染日期轴
首日:用来计算任务条和今日线(gantt-today-line)的left样式延伸时间轴
挂载时预先以今日为中心向前后各延伸了10周,同时在年月轴两边各放置了一个交叉观察器,当用户滚到它那里时自动继续延伸4周。$v.甘特.inited用来避免还未初始化就触发滚动。hover
鼠标挪到左边固定栏的任务上时(mouseOver)会把对应的任务条滚入可视范围(scrollIntoViewIfNeeded)。
鼠标挪到任务条上时展示任务概要信息和前后拖拽手柄。拖拽
拖动任务条本体和前后拖拽手柄都能够更改任务的开始时间和(或)截止时间,同时动态显示拖拽层(gantt-drag-mask)的可视化效果。
鼠标按下时开始监听鼠标移动事件,鼠标松开时根据移动距离/偏移量修改开始时间和(或)截止时间。
拖拽任务条本体时如果未发生位移应该认为是点击动作从而弹窗展示任务详情
任务
新建任务
A. 在看板、表格、列表类型的组件的右上角都有个“新建”按钮用来新建无预设任务;
B. 每个分组都有个加号按钮用来添加同组新任务:在$f.创建任务
的挂载事件中将把$v.modal.分组
赋给用来分组的字段中,如果该字段是状态则进一步过滤出可能的状态选项,有父任务类型时也同理;
C. 在任务弹窗中,如果允许有子任务类型,则可以在其某个子任务标签栏里新建子任务,此时预设了父任务。本组任务进度占比
每组任务上方都有个进度条,分别表示本组已完成、进行中、未开始的任务数及其占比。任务弹窗顶部
由项目名、任务类型、父任务arr数组、当前任务标题及各自图标和颜色组成。
每节父任务可点击展示其详情。分享到聊天
固定到任务栏
$v.任务栏
是当前项目固定的任务列表,可展开收缩,可移除可点击弹出详情。归档任务
删除任务
同时也要删除其关联的一个任务活动和多个资源。
把所有关联资源搜索出来,把返回来的arr克隆一下再依次删除:
$r.arr.clone().forEach('$resource.delete($x._id)')标题
点击切换到编辑模式并聚焦,失去焦点或按下回车时保存修改并切换回展示模式。当前状态
展示名称、图标和颜色
点击弹出当前状态下一个可流转的状态选项负责人、开始时间、截止时间
基本信息
展示的是当前任务类型的自定义属性及其在此任务中的值。
分别根据各自的属性类型来展示,通常分3列展示,但富文本和多行文本独占一行。子任务标签栏及其列表
可点击展示,可解除关联、删除任务,可变更负责人和截至日期关联任务
相关又非直接子任务附件
任务弹窗的顶层是个挂载组件,会把关联资源搜索出来,虽然一开始不展示附件列表但也是要展示附件数量的。
添加,重命名,删除
附件图标可能是上去前的本地文件预览图thumb,也可能是资源上传后的缩略图,还可能是根据其文件类型预设的文件图标。评论
活动
因为用户点击的时候才展示,所以可以延缓加载,单独存放于关联xtk中,没必要像评论一样存于任务中。
日期选择器
日期选择器是比较常用的功能,也有点小复杂,本来可以选一个开源的js库直接使用的,刚开始的时候为了验证表达式的能力就直接写了。
弹出日期选择器前先初始化
$v.pop
:
keep:为保持点击弹框本身不关框,参考$c.exp.login里添加的一个全局click事件
date:当前已设日期/时间,如果没有就会是今天
time:是否需要设置时间,如果上面的date里包含了分钟/秒钟则默认为真
F5:目的是再次触发日期选择器里的挂载事件从而重新初始化$v.日期
cb:选中/清除日期是调用的回调函数,它会做“截止时间不能早于开始时间”的验证后保存/清除对应日期。日期选择器顶层是个挂载组件用以构造
$v.日期
:
F5函数:根据$v.日期里的年和月构造那个月份的日期数组$v.日期.arr,每次挂载或年份、月份发生变化时都要调用。
日期数要比一个月的天数多,用的是6周,也即6 * 7的二维数组,所以有前头有部分日期是上个月的,后头有部分是下个月的。我们以本月第一个星期几作为起点,小于它的是上个月的,月份 - 1,日期 + 上月天数;大于本月天数的是下个月的,月份 + 1,日期 - 本月天数。
另外13月份是明年的1月,0月份是去年12月。返回日期
如果需要设置时间则必须点“确定”按钮才返回由年月日时分构造的date,点选日期只是更新$v.日期里的月和日。
如果不需要设置时间点选日期则会立即返回由选中年月日构造的date。清除
如果需要设置时间“清除”按钮只是关闭时间设置表单
如果不需要设置时间则会立即返回空以便调用者清除此日期快速输入时间
除了直接在输入框直接输入小时分钟数外可以点击输入框上方或下方的箭头进行加减,甚至直接按键盘上的上下箭头键进行快速加减。为提高效率,分钟数是以5为单位进行加减的。
注意在加减过程中小时分钟数超出范围时的特殊处理。加减年份/月份
顶部有双箭头加减年份,单箭头图标可加减月份。注意月份可能会翻到去年或明年时的检查。快速选择跨度大的年份/月份
当需要选择较远的年份时用上面的箭头一步步加减有点慢,比如选择一个老人的出生年月日,此时点击顶部中央的年月可以直接选择月份,再点击顶部中央的年份可以以20年为单位快速定位年份。
审批
设计审批表单、分条件设置流程、实时计算明细统计和审批人、审批流程展示、同意/拒绝/转交/撤销审批、评论和查阅记录、即时消息通知审批人、发送到聊天。
表单设计
分组管理
模板管理
审批表单设计
表单设计器是个拖拉拽的可视化编辑器,本身就是个简易版的零代码应用开发工具,包括三部分:可选控件、表单主体及表单和控件属性设置。
属性包括:属性类型,属性名,提示文字,是否必填等。
单选框、复选框、下拉列表有选项列表。
明细是个特殊控件容器,可包含其他控件。明细里面的数字类型的控件还可以有“是否参与明细统计”,“是否条件流程因素项”两项属性。
重点、难点:
拖拽使用的是Sortable.js。
列表之间拖拽的要点有相同的组名(group name),可选控件列表的组的拉取选项(group pull)用的是clone,表示被拽出列表时是克隆一份给表单主体而不是被拽离走了,同时组不可添加新控件(put: false)。group: { name: "group", pull: "clone", put: false }。
嵌套列表推荐配置:{ fallbackOnBody: true, swapThreshold: 0.65 }。
onSort函数要分是表单主体里控件间排序还是从可选控件里拉进来一个控件,拉进来时参数里有个pullMode是clone(对应前面的拉取选项clone),把控件从一个位置拖到新的位置时,是先把它从原来的位置(oldIndex)移除,在插入到新位置(newIndex)
$f.x.属性.splice($l.e.newIndex, 0, $f.x.属性.splice($l.e.oldIndex, 1)[0])单击选中一个表单控件时配置控件属性路径:$v.审批表单.属性 = "属性." + $i;而明细里的控件属性路径则是:$v.审批表单.属性 = "属性." + $p.$i + ".属性." + $i,它们是被嵌套在明细控件内的,为了不触发外层事件用$ev.stopPropagation()阻止事件传播
编辑控件各属性时添加change事件,即使是空事件也会随着输入或勾选而实时更新/(渲染)表单主体对应控件从而有更好的编辑体验。控件有多种类型,每种类型的表现形式和属性都不同,所以要根据不同的类型用渲染条件来控制显示哪种类型的控件。大家可根据实际需求进一步丰富控件列表。
重新渲染
表单主体是用一个挂载组件包装的,当添加新控件或控件排序时给$v.F5属性
一个新值使表单重新渲染。
明细里的属性列表也如此,但使用另一个变量$v.F5明细
仅重新渲染明细的属性。
定义审批流程
不同类型审批,审批流程及每个流程的审批人不同
自由流程
提交人可以自由选择审批由几步组成,每个阶段需要谁来审批,灵活性比较强,适合复杂程度不高且审批流程不定的审批,但不利于审批流程的规范性。固定流程
无论申请条件如何,审批流程都是固定,适合复杂程度不高但流程固定的审批。分条件设置流程
最常用的审批流程,当申请条件不同时,需要不同的审批流程和审批人,如:报销金额低于5000时只需要1级主管审批即可,报销金额在5000至10000时,需要添加2级主管审批,大于10000时则需要在添加boss审批。审批知会人
审批中不需要某些成员参与审批流程,但需要让TA知晓这个审批。
重点、难点:
部门栏是公司的组织架构,可按部门分级添加一个或多个审批人。“部门主管”是个未确定的变量,会在发起审批时得到替换成具体人员。
分条件设置流程中可通过表达式定义某种条件下的审批流,如招聘人数或报销金额越多需要参与审批的人也越多层级越高。注意条件的连续性,不要留下空挡。
发起审批
填写申请表单
实时计算明细统计
实时计算审批人
获取部门主管提交审批
验证表单必填项。
生成审批编号
重点、难点:
明细统计分为合计与总计
合计是某个待统计字段之和,总计是各待统计字段合计之和。
累加用:reduce("$acc + $x", 0)条件流程中根据前面编好的条件表达式($x.条件)从审批流程列表中找到符合条件的流程。一个要点是执行条件表达式是要把申请表单内容($f.申请)传进去作为执行环境;另一个要点是找到流程的审批人列表需要克隆一下赋给申请表单的待审批人,因为接下来的审批改成会更改此待审批人(如替换部门主管,转交审批人)而我们并不想影响到原流程中的审批人。
$f.申请.待审批人 = 审批流程.find('exc($x.条件, $f.申请)').审批人.clone()页面ready里定义了“找部门主管”函数,申请表单挂载时执行,条件流程中输入数字时也会执行。
难点是递归调用“$exp.部门主管”,部门从上往下一级级递归寻找直到到达申请人所在部门时被stopIf()停止。每次递归都把当前部门传给表达式执行环境。审批编号是申请年月日+4位数的序列号。序列号是每天从1开始的唯一数,每次申请都从数据库增加($inc)1得到。
提交审批时从待审批人列表把第一个审批人移出来作为当前审批人,并发送即时消息给TA。
审批展示
当前状态
申请人
审批详情
各类型数据展示
明细及其合计、总计审批流程
知会人
评论
查阅记录
重点、难点:
分类报销可以把审批详情较全面地展示,它外层的是个table元素的数据组件,里面由4个tbody,但它控制的是第二个用来展示明细的数据组件,第一个tbody是空的,用来占位,第三个用来展示属性列表,第四个放在最下面用来显示总计。
值得玩味的是第二个用来展示明细的数据组件没有配置HTML元素,即它本身并不会渲染成HTML元素,而是为了控制它第一个子元素的渲染,即循环展示明细列表(第二个子元素用来展示合计),而每个明细又有多项属性,再次使用数据组件,而明细里还有数组型属性要用到数据组件(比如上传多个附件)。
这是个嵌套使用数据组件极致展示复杂数据的例子。审批流程中的数据组件控制的是第二个li,由审批流、当前审批人、待审批人合并得来,第一个li固定放申请人,不受数据组件控制。
这里的【审批流】是已经参与审批动作的人,可能包含撤销人(申请人自己)和被转交人。每次动作都往审批流中推入相关信息:$push: { "x.审批流": { 谁: $c.me._id, 时间: date(), 动作, 意见 } }评论的input并不是用来输入的,而是点击时(focus时让$v.modal.评论 = true)弹出含有textarea的评论表单,点击其他地方能收回评论表单是因为在更高层的地方(modal-detail-main)把$v.modal.评论置空。
审批模态窗里有个挂载组件,非申请人每次打开审批模态窗时都往【审批查阅】添加一条查阅记录,
为了减少审批的信息量,把审批评论和审批查阅都保存到关联xtk表中,以审批._id作为key。在各类审批列表场景中都不读取评论和审批查阅,打开审批模态窗时在上面的挂载组件里默认读取评论,但依旧不读取审批查阅。
审批流程
同意审批
拒绝审批
转交审批
撤销审批
评论审批
查阅审批
审批通知
我申请的
我审批的
知会我的
即时消息通知
发送到聊天
重点、难点:
获取最新数据/重新渲染
待审在不同的人之间流转,信息变化快速。除了希望等待“我”审批的申请用即时消息通知到“我“以外,还希望在审批页面时能收到跟“我”相关的【待审批】申请。
待审批数:在根节点有个挂载组件,用$v.F5
作为key表达式,每当$v.F5变化时(提交、撤销、同意、拒绝、转交审批)都会重新拉取“我”相关的待审批数量
审批列表:审批列表的table用一个以$v.tab + $v.F6
为key表达式挂载组件包装。当在左侧导航栏(我申请的、我审批的,知会我的)或右上角标签栏(待审批、已审批)切换时都会重新拉取最新审批列表。