声明
本课程于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.toggleRole里的限制,再在”成员管理“中点击某个成员的”设置“,在”修改成员账 “标签页中修改此成员的密码后就有密码以此人身份登录了。详细指导请移步教学视频。
准备深入研究的同学请注册登录众触官 后,点击克隆按钮,把整个应用复制一份随意调试更改吧。
全局$V.s
全局可持久化状态,是status首字母。应用开始时从localStorage读取,应用结束时(onbeforeunload)存回localStorage,这样可以立即恢复回上次离开时的状态。
- $V.s.左导航:最新点开的左侧导航栏标签
- $V.s.消息:最新点开的私聊联系人或群组的_id
- $V.s.未读:各私聊或群组的最新未读消息的个数
- $V.s.已读:各私聊或群组的最新已读消息的时间
- $V.s.折叠:各种被折叠的菜单
- $V.s.项目:最新点开的项目_id及其组件下标和视图下标
- $V.s.任务栏:以项目为单位固定到任务栏的任务列表
全局事件
- click:当有弹窗时关闭弹窗,但为真时点击弹窗内部或者zpage以外(confirm或alert)不关闭。
- keydown:当有弹窗或模态窗时按下退出键可关闭弹窗或模态窗。
组织架构
公司的组织架构是其他功能的基础。 可以以树状结构管理各部门及其成员,也可以从通讯录里查看同事正在经手的任务状态。
- 职位列表
可添加、修改、上移、下移、删除。 - 用户登录时($c.exp.onLogin)拉取公司全员工信息和公司组织架构,即$c.exp.部门。
- 对员工和组织架构整合到全局变量中,方便应用的很多地方使用。
是组织架构的克隆,除了增加了个”未分配部门“
是把组织架构拍平后把各部门成员加进去
是在$V.部门成员基础上再把所有子孙部门成员都加进去 - 成员管理
基本信息设置,修改成员账 ,角色设置 - 调整部门
可以选中成员(单选或全选)后可批量调整他们的部门 - 成员搜索(过滤)
重点、难点:
- 移动就是交换位置,常用原生函数,先从旧下标删除一个元素,删除得到的是数组,所以后面加个取得元素本身,再把被删除元素插入到新下标中。
也就容易理解$l.arr.splice($index – 1, 0, $l.arr.splice($index, 1)[0])是上移,$l.arr.splice($index + 1, 0, $l.arr.splice($index, 1)[0])是下移了。
等学习了后面课程掌握了如何使用Sortable.js,回过头来把这里的上移和下移按钮改成拖动排序将使操作更简单和直观。 - 在数据库中删除数组的某个元素常用$pull,即从数组中拉走某个元素:
$xtk.modify(“公司”, “职位”, {$pull: {“x.arr”: $x}}) - 数据结构
部门是以树形结构存储的,部门有名称、主管,zchildren数组放子部门。
数据组件的数据源包含zchildren时会递归渲染成树形结构。 - 子部门缩进
为了体现部门层次结构在动态样式里根据层次深度($indexes)计算左缩进(paddingLeft) - 部门调整或成员更新后通过让挂载组件获取最新数据重新渲染
- 删除成员的部门用的是。留意“未分配部门”这个特殊部门。
- 删除子部门用的是,从数组中“拉走”。
通讯录
- 同事列表
- 同事详情
- 同事任务列表及状态数
- 发消息
- 常用联系人
- 收藏
- 按部门查找同事
盘
类似百度 盘、阿里 盘,以虚拟文件夹的形式分层分类来管理云端存储的文件,可批量上传整个文件夹。
盘展示
- 读取文件夹及文件信息。
- 文件夹树
- 文件路径
- 搜索
- 排序
重点、难点:
- 树形数据结构
文件夹只是存储于product表中的简单记录,包含、、(即父文件夹)信息,而非直接的树形结构,但我们要根据“父”构造出类似于上节课的树形组织架构来,作为数据组件的数据源。
文件夹可以有非常深的层次结构,不像组织架构那么单一稳固,不能一次性把所有的文件夹都读取出来。我们在onReady先读取顶层文件和顶层文件夹(其父文件夹是”无”),并依次读取各自直接子文件夹(根据有无子文件夹决定是否显示“展开”小三角图标)。
读取文件或文件夹的一个要点是把父文件夹_id传入执行环境:。
文件是上传到对象存储服务器的普通资源,并不无文件结构树信息,所以我们每次上传完文件后要给资源表添加当前文件夹作为其父文件夹:
$resource.modify(_id, { “x.类型”: “ 盘”, “x.父”: 当前文件夹_id }) - 几个关键变量
是文件夹结构树的根(名称唤作“ 盘”吧,文件路径的第一个节点),是最大的一棵树。它的“zchildren”包含上面读取顶层文件夹,”文件“包含顶层文件。特别地,它的_id为”无“。
是以父文件夹_id作为key的各级文件夹起始的大大小小的树。所以也即。
是树根到当前文件夹各级文件夹树的数组
是当前文件夹_id。用户点击时(无论是左侧文件结构树还是主体文件夹列表)会以它作为父文件夹读取它里面的文件夹和文件信息。另外,点击”展开“小三角图标时只需读写孙辈文件夹信息,一方面树里不展示文件信息,另一方面子文件夹已经读取,孙辈文件夹信息是为了决定是否展示子文件夹的小三角。 - 文件夹(自定义组件)
动态颜色 - 重新渲染
当变化时有三个挂载组件会重新组装数据并再次渲染。
A. 左侧文件结构树里的挂载组件就是用来组装的,从根节点开始向下递归遍历所有已经搜索出来的文件夹:一方面把文件夹直接挂载上,另一方面把它推入父文件夹的zchildren数组中。
B. 顶部的文件路径里的挂载组件就是用来组装的,从当前文件夹开始向上递归直至根节点,把每层文件夹推入路径数组头部(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()。
把上传成功后才要用到的在刚开始选择文件(onChange时)就设置了是为了防止上传过程中受到用户点击了其他文件夹导致$v.文件夹发生变化的影响,而刷新用户当前文件列表($v.exp.文件.exc({父: $v.文件夹}))则没有这个忧虑。 - 上传文件夹
为了避免受$v.文件夹变化的影响先赋值给临时变量:$l.文件夹 = $v.文件夹
把整个文件夹递归上传的关键是每个待上传文件有个相对路径,循环每个路径节点,创建未创建的文件夹节点,直到最里层的文件夹赋给$l.当前文件夹,上传完成后把它作为父文件夹。 - 上传列表
文件上传前把信息推入数组,标识为”正在上传“,如果是图片或视频给它创建一个缩略图:URL.createObjectURL(file),上传过程中及时改变它的进度样式,上传完成后删除”正在上传“标识。
盘管理
- 重命名
- 移动
- 删除
- 修改颜色
- 发送到聊天
- 回收站
重点、难点:
- 我们删除文件夹或文件是让它脱离原来的位置,但随后有还原操作,所以还要保留原来位置的信息。
我们通过把父文件夹改成原父文件夹的形式来实现
$rename: {“x.父”: “x.原父”}
这样没有父文件夹的资源就是被逻辑删除的。
“x.原父”: {$exists: true}
即时消息
类似企业微信/飞书/钉钉的即时消息,支持私聊、群聊、历史消息、置顶,可以发送表情、图片、 盘文件、项目和审批到会话,还可以收藏文件、固定消息。 是学习连接的好课程。
消息初始化
- 获取私有群(群成员包含我)和公开群的列表信息,并把它们的_id组成列表。
- 打开连接:
群组_id列表作为,这样可收到相关群组的消息;
设为true,把信息都保存到数据库,这样通过$socket.hist()可以查看历史消息,即时不在线是别人发送的未读消息也能收到;
设为true,这样打开的多个设备/浏览器都可以同时收到消息。 - onConnect 已连上
通过获取所有私聊的历史消息。
通过获取所有私有群和公开群的历史消息。
给各消息添加发送消息日期,为了方便在消息列表中显示分割用的日期和星期。
获取置顶会话的_id列表。
根据私聊对象_id或群聊_id:
把各消息列表放入中;
把各更多消息的列表放入中,每当聊天窗口滚到顶部时就从中拿出一个_id去获取更早的历史消息;
把各消息_id放入中,删除历史消息时用;
把各消息的发送时间大于其已读时间的消息条数放入其中; - onData 收到新消息
根据聊天对象(私聊对象_id或群聊_id)把消息放入对应的消息列表中。
如果此消息是当前正在打开的聊天会话中,则渲染后把新消息滚入视图,否则给其未读消息数加 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 !== $array[$index – 1].date - 把自己发送的消息向右对齐,以区别于别人发送的消息
$c.me._id === from ” message-item–me” : “” - 消息弹出框
固定消息,删除自己发送的消息 - 用渲染消息体
- 交叉观察器
如果还有更多消息未读取出来时($V.消息.more[$V.s.消息].length),当滚到消息框顶部是会触发交叉观察器从里拿出一项交给$socket.more()来获取更多消息,并放入消息列表头部,渲染后滚到第一个消息但交叉观察器却在窗口外的位置,这样避免连续触发,而是等用户再往上滚的时候又会触发一次。
一个注意点是引入临时变量$v.l.more来阻止每次新渲染交叉观察器时导致的首次执行,因为此时并不是由有户主动往上滚引起的。 - 未读消息
展示未读消息数,点击时往上滚到上次的已读消息或者消息框顶部以触发获取更多消息的交叉观察器,然后触发“全部标记为已读” - 发送文本消息
把替换成对应的img:.replace($c.reg.表情, $c.fun.表情);
@某人:在群组会话中按下@时(即Shift + 2,2的keyCode是50)弹出排除自己的群组成员列表,但在已弹窗时按下非Shift键(keyCode为16)则关闭弹窗;
把替换成对应的姓名:.replace($c.reg.提及, $c.fun.提及);点击时还有弹窗是因为添加了onClick事件:window.提及click();
$socket.send($V.s.消息, “text”, $f.消息.txt); - 发送图片/文件消息
点击文件图标是弹出上传对话框,选择一个或多个文件后上传至服务器;
然后根据不同的文件类型转化成对应的HTML文本:为图片,直接显示图片缩略图;为视频,显示对应的视频截图;为其它文件类型,会根据其文件后缀format来显示不同的图标);
消息发送后把文件信息添加到此会话的文件列表中。
值得注意的是消息发送完了并没有立即操作数据库而是先把内容放在变量$v.待保存文件消息中,等待消息返回后在$c.exp.onData再执行,因为考虑到以后删除文件时也要把它从消息历史中删除,删除时要用到消息发送时间d:$socket.pull(_id, from, d),而这个时间是在服务器端生成的,只有收到消息后才知道。 - 发送项目任务/审批/ 盘消息
其它
- 文件列表
从 盘发送到聊天的文件不在此列表中
删除一个文件要:
A. 删除文件本身
B. 从关联会话的文件列表中移除
C. 消息历史中移除对应消息。 - 固定消息列表
删除时从关联会话的固定消息列表中移除 - 点击成员图标
群组时显示群组成员,可进一步显示成员资料或移除成员
私聊时显示成员资料 - 点击设置图标
置顶会话和取消置顶会话
群组设置
退出群组
删除群组
项目管理
项目协作管理是Worktile的核心功能,用来计划、组织和跟进一些具有明确目的且相互关联的任务。 支持高度灵活的个性化配置以适应各种场景的项目需求。
Worktile以项目化的方式计划、管控和跟进多种工作领域和场景中的任务。
项目管理概念
项目
任务集合中心,用来计划、组织和跟进一些具有明确目的且相互关联的任务。项目可以是长期的也可以是短期的。
组件
任务的展示方式,将任务信息以不同的视觉形式呈现给用户,有多种组件类型:看板、列表、表格、甘特图、日历、 表。
视图
组件可以有多种视图来对任务进行分组、筛选、排序。
任务类型
承载不同业务场景的模型,由高度结构化的任务属性构成,可规定任务类型之间的关系和状态流转方式,从而形成信息存放和团队协作的规范。
属性:任务类型可以指定此类型的任务可包含哪些自定义信息,有多种属性类型:文本、富文本、数字、日期、成员、多个成员、下拉单选、下拉多选、继承。
任务
某种任务类型的实例。每个任务都有标题、状态,还有负责人、参与人、起止时间、评论等,主体部分是任务类型里定义的各种属性。
建议大家克隆本课程后尝试扩展更多属性类型,甚至扩展组件类型。
数据模型
项目:
任务类型:
任务:
其中任务属性是存储它所属任务类型属性指定的具体数据,比如招聘任务的属性:
安装数据库管理插件可以更直观地理解数据模型。
初始化
加载项目
- 加载我参与的私有项目
- 加载公开项目
- 把以上两种项目合并后抽取简要信息组成列表
- 从过滤出列表
- 如果有就继续加载此项目信息
- 加载此项目的任务类型
- 加载此项目的所有任务
- 恢复此项目的数组
任务整理
把上面加载的原始数据整理到。
- 如果任务类型里有指定父任务类型,就把它添加到其父任务类型的里
- 把各任务类型里自定义的属性赋给对应的列表中
- 为了方便读取,继续把上述属性列表根据它里面的字段赋给对应的对象中
- 不更改原$c常量,克隆所有任务数据赋给数组,并根据其_id组成对象
- 如果一个任务有父任务,那就把该任务根据任务类型添加到其父任务的子任务数组里,此外递归往上寻找父任务链条组成任务链数组,作为导航栏展示在任务弹窗顶部
- 对于任务类型里定义的继承类型的属性,给其下所有任务里的此属性添加继承字段
分组整理
把上面加载的原始数据整理到
- 把当前活跃的项目组件(即$V.s.项目.组件)赋给
- 把当前活跃组件的任务类型下的任务,经过当前活跃视图筛选要求过滤后,根据当前活跃视图规定的分组字段进行分组,把每个任务添加到数组中,不同的分组属性组成数组
- 根据任务状态统计各分组下的任务数,并合计分组总数
- 根据当前活跃视图的排序要求给各分组里的任务列表进行排序
- 把当前任务类型的赋给
- 对甘特图组件做额外处理
任务活动
把任务活动封装成函数,以方便在多个地方任务发生变化时调用:把传入的事件参数添加上变更人和变更时间后插入到任务对应的xtk数组头里。
任务弹窗
从其他页面跳转过来带有“任务”参数时($query.任务)弹出任务模态框。
onResize
当窗体不足以容纳所有组件时把后面的组件/视图收到“更多”里面。 表示可以容纳的组件数。
项目
- 项目列表
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!