Compare commits

...

9 Commits

  1. 2
      .env.development
  2. 2
      .env.production
  3. 256
      CHANGELOG.md
  4. 4
      package.json
  5. 5
      public/index.html
  6. 30
      src/App.vue
  7. 20
      src/apis/plugin.js
  8. 16
      src/apis/project.js
  9. 9
      src/apis/role.js
  10. 3
      src/apis/tall.js
  11. 17
      src/apis/task.js
  12. 154
      src/common/styles/app.scss
  13. 91
      src/common/styles/user.scss
  14. 101
      src/components/InputSearch/InputSearch.vue
  15. 20
      src/components/PrettyExchange/PrettyExchange.vue
  16. 453
      src/components/ProjectDetail/ProjectDetail.vue
  17. 37
      src/components/Projects/ProjectItem.vue
  18. 252
      src/components/Roles/Roles.vue
  19. 84
      src/components/Skeleton/READ_ME.md
  20. 187
      src/components/Skeleton/Skeleton.vue
  21. 251
      src/components/Title/Title.vue
  22. 479
      src/components/Title/components/CreateTask.vue
  23. 210
      src/components/Title/components/ShareProject.vue
  24. 9
      src/components/Upload/Upload.vue
  25. 143
      src/components/login/login.vue
  26. 22
      src/components/uni-popup/message.js
  27. 23
      src/components/uni-popup/popup.js
  28. 246
      src/components/uni-popup/uni-popup-dialog.vue
  29. 115
      src/components/uni-popup/uni-popup-message.vue
  30. 171
      src/components/uni-popup/uni-popup-share.vue
  31. 289
      src/components/uni-popup/uni-popup.vue
  32. 276
      src/components/uni-transition/uni-transition.vue
  33. 3
      src/config/db.js
  34. 97
      src/config/plugin.js
  35. 2
      src/config/task.js
  36. 17
      src/config/time.js
  37. 11
      src/main.js
  38. 2
      src/manifest.json
  39. 21
      src/pages.json
  40. 18
      src/pages/index/index.vue
  41. 144
      src/pages/left-window/left-window.vue
  42. 60
      src/pages/phone-bind/phone-bind.vue
  43. 54
      src/pages/project-webview/project-webview.vue
  44. 78
      src/pages/right-window/right-window.vue
  45. 44
      src/pages/top-window/top-window.vue
  46. 3
      src/store/db/actions.js
  47. 3
      src/store/db/getters.js
  48. 12
      src/store/db/index.js
  49. 3
      src/store/db/mutations.js
  50. 7
      src/store/db/state.js
  51. 4
      src/store/index.js
  52. 9
      src/store/project/mutations.js
  53. 1
      src/store/project/state.js
  54. 17
      src/store/role/actions.js
  55. 13
      src/store/role/getters.js
  56. 12
      src/store/role/index.js
  57. 39
      src/store/role/mutations.js
  58. 8
      src/store/role/state.js
  59. 33
      src/store/task/actions.js
  60. 23
      src/store/task/getters.js
  61. 12
      src/store/task/index.js
  62. 238
      src/store/task/mutations.js
  63. 25
      src/store/task/state.js
  64. 18
      src/store/user/mutations.js
  65. 2
      src/store/user/state.js
  66. 1
      src/uni.scss
  67. 163
      src/utils/indexedDB.js
  68. 53
      src/utils/task.js

2
.env.development

@ -3,4 +3,4 @@ VUE_APP_BASE_URL=https://test.tall.wiki
VUE_APP_API_URL=https://test.tall.wiki/gateway
VUE_APP_MSG_URL=wss://test.tall.wiki/websocket/message/v4.0/ws
VUE_APP_PROJECT_PATH=https://test.tall.wiki/tall-project
VUE_APP_VERSION=v3.1.0
VUE_APP_VERSION=v3.2.0

2
.env.production

@ -3,4 +3,4 @@ VUE_APP_BASE_URL=https://www.tall.wiki
VUE_APP_API_URL=https://www.tall.wiki/gateway
VUE_APP_MSG_URL=wss://www.tall.wiki/websocket/message/v4.0/ws
VUE_APP_PROJECT_PATH=https://www.tall.wiki/tall-project
VUE_APP_VERSION=v3.1.0
VUE_APP_VERSION=v3.2.0

256
CHANGELOG.md

@ -1,8 +1,58 @@
# 0.1.0 (2021-09-09)
# 0.1.0 (2021-11-19)
### 🌟 新功能
范围|描述|commitId
--|--|--
- | 绑定手机号 | 52e0352
- | 标题栏变化 | 3898cfe
- | 标题栏变化 | c0fcd9d
- | 标题栏角色栏全局任务组件新建 | 0500cb4
- | 插件参数处理调整 | a3e68d3
- | 插件数据获取 | 5b91bdc
- | 存token | b8a178d
- | 导入项目,更新项目 | 5e06adf
- | 导入项目后提示并打开项目详情页 | 410f527
- | 导入wbs | 1224fcb
- | 登录时提示是否合并账号样式修改 | d82bf1a
- | 登录提示是否合并账号 | 6a9b054
- | 点击日历日期查询项目列表 | c458385
- | 定期任务面板骨架屏添加 | b2698c0
富文本插件 | 富文本插件demo测试 | ed3d644
- | 缓存修改 | 63e1f0d
- | 获取用户收取那,提交用户信息 | a3c54f1
- | 角色栏实现 | 94cd671
- | 距调整pc端 | 5069aa1
- | 面变化首页变化 | 5e860f1
- | 模拟接口测试 | 69e7931
- | 配置默认插件接口 | f0c177d
- | 全局插件及默认插件位置修改 | 6c80d08
- | 任务进行中状态数字 | 27b7326
- | 任务状态时间显示 | 56f5183
- | 日常任务插件调整 | c1881f9
- | 日历定位;合并 | ea3f937
- | 删除项目 | 00b886c
- | 上传逻辑变化 | 3ff1dc2
- | 设置小红点 | 9316bcb
- | 升级版本v3.1.0;tailwindcss添加class | 9ef05e1
- | 时间基准线,默认插件 | a33ba1e
- | 时间轴界面 | 33927e9
- | 时间轴修改状态时提示框增加 | e841392
- | 适配小程序;小程序登录 | cefc0eb
- | 适应PC端 | 8851e78
- | 首页项目样式改变 | 8514c85
- | 提交到本地 | 9cbe411
- | 添加 环境变量,动态控制webview project的path | 8a40481
- | 添加时间轴上下滚动 | 2b81bbc
- | 添加项目排序 | a0b491b
- | 添加子任务插件 子项目插件 | 7bda7e2
- | 细节调整,添加project-webview | 4d9050b
- | 向右箭头图标变化 | 8e9ca55
- | 项目列表, 项目url | 32e005b
- | 项目列表排序 | 224c58b
- | 项目api url设置 | 6cd5245
- | 修改小程序id | 4206bf2
- | 引入dayjs | 29b8b93
- | 字体大小更改 | 82cfdd4
- | api封装 | 7d4edfc
bind phone | 图形验证码;短信验证码;绑定手机号 | 93ffea2
- | cache indexedDB处理 | 3388967
@ -20,138 +70,95 @@
- | tall插件封装 | 1bcb920
task status | 任务状态切换未完 | 7ffd135
- | ws storage | 21b3a06
- | 上传逻辑变化 | 3ff1dc2
- | 任务状态时间显示 | 56f5183
- | 任务进行中状态数字 | 27b7326
- | 全局插件及默认插件位置修改 | 6c80d08
- | 删除项目 | 00b886c
- | 升级版本v3.1.0;tailwindcss添加class | 9ef05e1
- | 向右箭头图标变化 | 8e9ca55
- | 字体大小更改 | 82cfdd4
- | 存token | b8a178d
- | 定期任务面板骨架屏添加 | b2698c0
富文本插件 | 富文本插件demo测试 | ed3d644
- | 导入wbs | 1224fcb
- | 导入项目,更新项目 | 5e06adf
- | 导入项目后提示并打开项目详情页 | 410f527
- | 引入dayjs | 29b8b93
- | 提交到本地 | 9cbe411
- | 插件参数处理调整 | a3e68d3
- | 插件数据获取 | 5b91bdc
- | 日历定位;合并 | ea3f937
- | 日常任务插件调整 | c1881f9
- | 时间基准线,默认插件 | a33ba1e
- | 时间轴修改状态时提示框增加 | e841392
- | 时间轴界面 | 33927e9
- | 标题栏变化 | 3898cfe
- | 标题栏变化 | c0fcd9d
- | 标题栏角色栏全局任务组件新建 | 0500cb4
- | 模拟接口测试 | 69e7931
- | 添加 环境变量,动态控制webview project的path | 8a40481
- | 添加子任务插件 子项目插件 | 7bda7e2
- | 添加时间轴上下滚动 | 2b81bbc
- | 添加项目排序 | a0b491b
- | 点击日历日期查询项目列表 | c458385
- | 细节调整,添加project-webview | 4d9050b
- | 绑定手机号 | 52e0352
- | 缓存修改 | 63e1f0d
- | 角色栏实现 | 94cd671
- | 设置小红点 | 9316bcb
- | 距调整pc端 | 5069aa1
- | 适配小程序;小程序登录 | cefc0eb
- | 配置默认插件接口 | f0c177d
- | 面变化首页变化 | 5e860f1
- | 项目api url设置 | 6cd5245
- | 项目列表, 项目url | 32e005b
- | 项目列表排序 | 224c58b
- | 首页项目样式改变 | 8514c85
### 🎨 代码样式
范围|描述|commitId
--|--|--
- | calendar注释 | a2ec112
- | indexedDB.js格式整理 | b0d3a36
- | 代码审查 | d75134c
- | 代码格式细节调整 | cb2532b
- | 代码审查 | d75134c
- | 格式细节调整 | b907a03
- | 更新代码 | 8c27e68
- | 更新代码 | 1f40a76
- | 任务快捷方式图标增加 | 4aba872
- | 修改角色样式 | 73e268e
- | 日常任务修改 | dfa7ee2
- | 删除插件携带的多余文件 | 0f392bb
- | 删除多余字段 | 5ae3973
- | 删除没用代码 | 34b20e1
- | 删除calendar中多余的console | e339eec
- | 删除console.log | 5064a38
- | 删除index中没用的alert代码 | 9c9eec7
- | 删除mock,console;upload添加loading | 99d42e2
- | 删除多余字段 | 5ae3973
- | 删除插件携带的多余文件 | 0f392bb
- | 删除没用代码 | 34b20e1
- | 添加插件数据 | 2f11b42
- | 图标修改 | 54bca09
- | 无基本变化 | 21ac4bb
- | 日常任务修改 | dfa7ee2
- | 更新代码 | 8c27e68
- | 更新代码 | 1f40a76
- | 格式细节调整 | b907a03
- | 添加插件数据 | 2f11b42
- | 组件新建 | 89c0035
- | 细节调整 | 2cfc09a
- | 修改角色样式 | 73e268e
- | 组件新建 | 89c0035
- | calendar注释 | a2ec112
- | indexedDB.js格式整理 | b0d3a36
### 🐛 Bug 修复
范围|描述|commitId
--|--|--
- | 1.时间轴数据渲染 2.时间基准线 | d643af2
- | api 存storage | 81032ba
ID1000343 | 解决向下预加载查询参数时间没+1颗粒度;以及滚动加载颗粒度写死的问题 | 940603a, closes #ID1000343
plugin | 插件解析机制完善 | 0f5a27d
project title | 项目标题修改; 切换角色移除script | 5c20017
roles | 修复默认显示不是我的角色的问题 | b69f94f
role | 切换角色的逻辑修正完善 | 4ae534f
task任务逻辑完善 | 减少初始global及regular的不必要请求 | bd4bd38
- | title.vue根据页面栈显示返回按钮;标题文本超出显示... | 0cbacf4
- | 上下滑动加载定期任务 | 4090d89
- | 上下滚动时间轴 | d533a01
- | 下拉加载定期任务传参,时间格式化修改 | 0b95a0e
- | 任务开始时间延迟插件 | 992a313
- | 修改main | 749ae9a
- | 修改定期任务状态0和4时不加载圆圈 | 30e352f
- | 修改小红点传参 | 87b20fd
- | 修改报错 | 531c14d
- | 修改接口路径 | df6acf2
- | 修改样式 | f0ddc90
- | 修改角色栏组件 | a54c601
- | 切换到默认项目角色没有激活状态的bug | 438d448
- | 切换日历时查询小红点 | 7091789
- | 插件bug解决 | 41257eb
- | 初始展示角色修改 | 2ac4053
- | 定期任务key值修改 | c6688db
- | 定期任务接口 | aa4981c
- | 定期任务插件 | 92b3254
- | 定期任务未加载时,显示空的时间轴并能上下滑动 | ce38093
- | 定期任务本地缓存和api赋值,未完成 | 5a10856
定期任务本地缓存和api赋值,未完成 | 定期任务本地缓存和api赋值,未完成 | b22a366
- | 定期任务插件 | 92b3254
- | 定期任务骨架屏修改 | 8ff72dd
- | 定期任务接口 | aa4981c
- | 定期任务未加载时,显示空的时间轴并能上下滑动 | ce38093
- | 定期任务key值修改 | c6688db
- | 骨架屏替换 | e9fdd71
- | 监听时间基本点 | 033fca0
- | 角色栏修改 | 19228d6
- | 角色显示状态修改 | 7d3b906
- | 解决时间轴报错 | da1eece
- | 平车演示临时去掉项目快捷方式的toast提示 | e0b2c23
- | 手动展开日常任务 | 0a4a622
- | 提示信息显示bug及日常任务收缩问题 | f2f06c5
- | 插件bug解决 | 41257eb
- | 收到消息修改任务状态 | c378063
- | 日历无任务时添加小绿点,时间轴刻度无任务不显示时分 | 0f90868
- | 日常任务html数据查验 | 880ce5c
- | 切换到默认项目角色没有激活状态的bug | 438d448
- | 切换日历时查询小红点 | 7091789
- | 任务开始时间延迟插件 | 992a313
- | 日常任务插件遍历时的key值修改 | cd26285
- | 日常任务插件面板高度修改 | 249f9e4
- | 时间轴上下滑动 | 4d0ae46
- | 时间轴上下滚动数据加载bug修改 | e82ede4
- | 日常任务html数据查验 | 880ce5c
- | 日历无任务时添加小绿点,时间轴刻度无任务不显示时分 | 0f90868
- | 上下滚动时间轴 | d533a01
- | 上下滑动加载定期任务 | 4090d89
- | 设置时间轴自动滚动到当前位置 | a3474f8
- | 时间轴插件 | 225d3cc
- | 时间轴无任务时时间刻度加载修改 | 4921672
- | 时间轴滚动位置修改 | 551da63
- | 时间轴骨架屏修改 | ca78d02
- | 监听时间基本点 | 033fca0
- | 角色显示状态修改 | 7d3b906
- | 角色栏修改 | 19228d6
- | 解决时间轴报错 | da1eece
- | 设置时间轴自动滚动到当前位置 | a3474f8
- | 时间轴滚动位置修改 | 551da63
- | 时间轴上下滚动数据加载bug修改 | e82ede4
- | 时间轴上下滑动 | 4d0ae46
- | 时间轴无任务时时间刻度加载修改 | 4921672
- | 收到消息修改任务状态 | c378063
- | 手动展开日常任务 | 0a4a622
- | 提示信息显示bug及日常任务收缩问题 | f2f06c5
- | 跳转详情页返回路径修改 | c5e17c0
- | 下拉加载定期任务传参,时间格式化修改 | 0b95a0e
- | 项目操作按钮弹框显示问题 | 1d8d73e
- | 项目操作弹框显示不对 | b55a915
项目列表排序 | 项目列表排序 | 402c563
- | 项目列表排序修改 | fd3c3ac
- | 项目列表排序修改 | 59f4c21
项目列表排序 | 项目列表排序 | 402c563
- | 骨架屏替换 | e9fdd71
- | 修改报错 | 531c14d
- | 修改定期任务状态0和4时不加载圆圈 | 30e352f
- | 修改角色栏组件 | a54c601
- | 修改接口路径 | df6acf2
- | 修改小红点传参 | 87b20fd
- | 修改样式 | f0ddc90
- | 修改main | 749ae9a
- | api 存storage | 81032ba
ID1000343 | 解决向下预加载查询参数时间没+1颗粒度;以及滚动加载颗粒度写死的问题 | 940603a, closes #ID1000343
plugin | 插件解析机制完善 | 0f5a27d
project title | 项目标题修改; 切换角色移除script | 5c20017
role | 切换角色的逻辑修正完善 | 4ae534f
roles | 修复默认显示不是我的角色的问题 | b69f94f
task任务逻辑完善 | 减少初始global及regular的不必要请求 | bd4bd38
- | title.vue根据页面栈显示返回按钮;标题文本超出显示... | 0cbacf4
### 📝 文档
@ -163,53 +170,62 @@
### 🔧 测试
范围|描述|commitId
--|--|--
- | 暂时移除了jest浏览器配置 | 5088d01
- | 添加测试,测试utils/time.js的computeDurationText | e758010
- | 禁用任务开始操作 | b5425db
- | 添加测试,测试utils/time.js的computeDurationText | e758010
- | 暂时移除了jest浏览器配置 | 5088d01
### 🔨 代码重构
范围|描述|commitId
--|--|--
- | 更改版本号 | 5f1d9f4
- | 界面样式调整 | 4367249
- | 去掉tailwindcss | 4bed47e
- | 任务状态重构 | 4693655
- | 删除多余的技术验证界面 | 542ae5b
- | 删除多余的weekmode store里的东西 | 0841fe0
- | 下滑时间轴添加备注 | 4fd20e3
- | 修改utils/upload 兼容小程序选择客户端文件上传WBS | 8f49129
- | 重构store分层 | 5f6fff8
calendar | 日历细节调整 | 1a8d6bf
- | H5自定义导航 | 04ae8f2
- | project 代码健壮性完善 | a3202c5
store/home | 删除store/home | db8a3b4
task beginTime | 格式化任务开始时间 | fbc0301
template | eslint prettier sass uview tailwindcss | 9c966a1
tips | 修改任务状态方法重构 | b57d3ac
tip | 任务状态显示及tip组件数据的重构 | 78a5750
tips | 修改任务状态方法重构 | b57d3ac
title.vue | 移除测试的repeat; 样式细节调整 | c32d2bd
- | 下滑时间轴添加备注 | 4fd20e3
- | 任务状态重构 | 4693655
- | 删除多余的weekmode store里的东西 | 0841fe0
- | 删除多余的技术验证界面 | 542ae5b
- | 去掉tailwindcss | 4bed47e
- | 界面样式调整 | 4367249
- | 重构store分层 | 5f6fff8
### 🚀 性能优化
范围|描述|commitId
--|--|--
- | 1.时间轴筛选相同的时间替换数据 2.整理代码 | e082ccb
- | 修改代码格式 | 14123d7
- | 修改定期任务骨架屏高度 | 909a734
- | 小红点api缓存修改 | e992343
- | 测试接口 | 215e074
- | 插件查询及展示 | 4dba770
- | 登[C录时提示登录时提示是否合并账号样式修改是否合并账号样式修改 | 4f75696
- | 角色栏文字颜色修改 | 215c6b3
- | 解决警告 | c932b09
- | 日历的更改 | 7353ac8
- | 数据存储,避免重复调用接口 | d22308a
- | 提交本地代码 | e0cf2ed
- | 小红点api缓存修改 | e992343
- | 修改代码格式 | 14123d7
- | 修改定期任务骨架屏高度 | 909a734
- | 整理代码 | 7a55315
- | 日历的更改 | 7353ac8
- | 测试接口 | 215e074
- | 组件文件夹新建 | 22bfe7b
- | 组件文件夹新建 | 17bb8c9
- | 组件文件夹新建 | 1421504
- | 角色栏文字颜色修改 | 215c6b3
- | 解决警告 | c932b09
### chore
范围|描述|commitId
--|--|--
- | 删除多余的构建的命令 | 3f4eb2f
- | 添加mp-weixin的构建命令 | 3776a67
信息配置 | 配置eslint等配置 | 7421443
- | 修复不能build的问题 | 0b7b91e
- | api 封装 | 8dcb8a2
- | dart-sass替换node-sass;删除多余的uni平台包 | 519f28b
- | env host修改 | a79a4a5
@ -220,10 +236,6 @@
pwa 小程序 | 移除了pwa,alloyFinger添加平台判断 | 875fab4
- | uview-ui | a9ea34b
v3.0.1 | tall api 地址从1.0改成了3.0 | db5afd5
信息配置 | 配置eslint等配置 | 7421443
- | 修复不能build的问题 | 0b7b91e
- | 删除多余的构建的命令 | 3f4eb2f
- | 添加mp-weixin的构建命令 | 3776a67
范围|描述|commitId

4
package.json

@ -28,7 +28,7 @@
"dependencies": {
"@dcloudio/uni-h5": "^2.0.0-32220210818002",
"@dcloudio/uni-helper-json": "*",
"@dcloudio/uni-i18n": "^2.0.0-32220210818002",
"@dcloudio/uni-i18n": "^2.0.0-32920211029004",
"@dcloudio/uni-mp-vue": "^2.0.0-32220210818002",
"@dcloudio/uni-mp-weixin": "^2.0.0-32220210818002",
"@dcloudio/uni-stat": "^2.0.0-32220210818002",
@ -39,12 +39,14 @@
"regenerator-runtime": "^0.12.1",
"uview-ui": "^1.8.4",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.3",
"vuex": "^3.2.0"
},
"devDependencies": {
"@babel/runtime": "~7.12.0",
"@dcloudio/types": "^2.5.1",
"@dcloudio/uni-automator": "^2.0.0-32220210818002",
"@dcloudio/uni-cli-i18n": "^2.0.0-32920211029004",
"@dcloudio/uni-cli-shared": "^2.0.0-32220210818002",
"@dcloudio/uni-migration": "^2.0.0-32220210818002",
"@dcloudio/uni-template-compiler": "^2.0.0-32220210818002",

5
public/index.html

@ -14,6 +14,7 @@
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<link rel="stylesheet" href="https://unpkg.com/tailwindcss@2.2.7/dist/tailwind.min.css">
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
</head>
@ -24,5 +25,7 @@
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<script src="https://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<!-- uni 的 SDK -->
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
</html>

30
src/App.vue

@ -9,6 +9,7 @@ export default {
/* #ifdef MP-WEIXIN */
await this.signin();
this.initSocket();
/* #endif */
/* #ifdef H5 */
@ -19,6 +20,7 @@ export default {
if (!options.query || !options.query.u) {
// u (userId)
this.$t.ui.showToast('缺少用户信息参数');
// this.$t.ui.navigateTo({ url: '/pages/user/login' });
} else {
const data = await this.getToken(options.query.u);
this.noPhone(data.phone);
@ -26,6 +28,21 @@ export default {
}
/* #endif */
this.initSocket();
// #ifdef H5
if (navigator.userAgent.indexOf('Mobile') === -1) {
this.setIsPC(true);
} else {
this.setIsPC(false);
}
// #endif
// pc
if (navigator.userAgent.match(/(iPhone|Android)/i)) {
this.setIsPC(false);
} else {
this.setIsPC(true);
}
},
computed: {
@ -37,7 +54,7 @@ export default {
...mapActions('user', ['getToken']),
...mapActions('socket', ['initSocket']),
...mapMutations(['setNetworkConnected', 'setSystemInfo']),
...mapMutations('user', ['setToken', 'setUser']),
...mapMutations('user', ['setToken', 'setUser', 'setIsPC']),
// store
// 2g 3g ;
@ -100,3 +117,14 @@ export default {
@import 'uview-ui/index.scss';
@import './common/styles/app.scss';
</style>
<style>
.uni-top-window {
border-bottom: 1px solid #cccccc;
}
.uni-right-window {
height: 100%;
border-left: 1px solid #cccccc;
}
</style>

20
src/apis/plugin.js

@ -0,0 +1,20 @@
// 插件的地址是固定的
const url = process.env.VUE_APP_API_URL;
const install = (Vue, vm) => {
vm.$u.api = { ...vm.$u.api } || {};
// 获取插件信息
vm.$u.api.getOtherPlugin = param => vm.$u.post(`${url}/pluginshop/plugin/query?pluginId=${param.pluginId}&styleType=${param.styleType}`);
// 查询子任务
vm.$u.api.findSonTask = param => vm.$u.post(`${uni.$t.domain}/task/findSonTask`, param);
// 查询子项目
vm.$u.api.findSonProject = param => vm.$u.post(`${uni.$t.domain}/project/findSonProject`, param);
// 提交交付物
vm.$u.api.saveDeliver = param => vm.$u.post(`${uni.$t.domain}/deliver/save`, param);
// 查询任务的交付物历史记录
vm.$u.api.queryDeliverOfTask = param => vm.$u.post(`${uni.$t.domain}/deliver/queryDeliverOfTask`, param);
// 检查交付物
vm.$u.api.checkDeliver = param => vm.$u.post(`${uni.$t.domain}/deliver/checkDeliver`, param);
};
export default { install };

16
src/apis/project.js

@ -0,0 +1,16 @@
const install = (Vue, vm) => {
vm.$u.api = { ...vm.$u.api } || {};
//根据id获取项目信息
vm.$u.api.findProjectById = param => vm.$u.post(`${uni.$t.domain}/project/findProjectById`, param);
//创建分享连接
vm.$u.api.createShare = param => vm.$u.post(`${uni.$t.domain}/share/create`, param);
//点击分享连接
vm.$u.api.clickShare = param => vm.$u.post(`${uni.$t.domain}/share/click`, param);
//查询医院是否填写了调查问卷
// vm.$u.api.queryNotWrite = param => vm.$u.post(`${uni.$t.domain}/questionnaire/queryNotWrite`, param);
};
export default { install };

9
src/apis/role.js

@ -0,0 +1,9 @@
const install = (Vue, vm) => {
vm.$u.api = { ...vm.$u.api } || {};
//根据项目id查找角色
vm.$u.api.findShowRole = param => vm.$u.post(`${uni.$t.domain}/role/show`, param);
//根据项目id查找所有成员
vm.$u.api.queryChecker = param => vm.$u.post(`${uni.$t.domain}/deliver/queryChecker`, param);
};
export default { install };

3
src/apis/tall.js

@ -30,6 +30,9 @@ const install = (Vue, vm) => {
vm.$u.api.getToken = userId => vm.$u.get(`${tall}/users/userId`, { userId });
// 绑定手机号
vm.$u.api.phoneBind = (phone, smsCode) => vm.$u.http.post(`${tall}/users/binding`, { phone, smsCode });
// 是否合并账号
vm.$u.api.phoneMerge = (phone, isMerge) => vm.$u.http.post(`${tall}/users/merge`, { phone, isMerge });
// 修改用户信息
vm.$u.api.updateUserInfo = params => vm.$u.http.post(`${tall}/users/userInfo`, params);

17
src/apis/task.js

@ -0,0 +1,17 @@
const install = (Vue, vm) => {
vm.$u.api = { ...vm.$u.api } || {};
vm.$u.api.getGlobal = param => vm.$u.post(`${uni.$t.domain}/task/global`, param);
vm.$u.api.getPermanent = param => vm.$u.post(`${uni.$t.domain}/task/permanent`, param);
//根据时间基准点和角色查找定期任务
vm.$u.api.getRegularTask = param => vm.$u.post(`${uni.$t.domain}/task/regular`, param);
//修改任务状态
vm.$u.api.updateTaskType = param => vm.$u.post(`${uni.$t.domain}/task/type`, param);
//新建任务
vm.$u.api.saveTask = param => vm.$u.post(`${uni.$t.domain}/task/save`, param);
//克隆任务
vm.$u.api.cloneTask = param => vm.$u.post(`${uni.$t.domain}/task/clone`, param);
//模糊查询 查找项目下的任务
vm.$u.api.queryTaskOfProject = param => vm.$u.post(`${uni.$t.domain}/task/queryTaskOfProject`, param);
};
export default { install };

154
src/common/styles/app.scss

@ -1,7 +1,155 @@
.min-0 {
min-width: 0;
}
.border-b-1 {
border-bottom-width: 1px;
}
.flex-shrink-0 {
flex-shrink: 0;
}
/* 列表 */
.uni-list {
background-color: #ffffff;
position: relative;
width: 100%;
display: flex;
flex-direction: column;
border: 1px solid #afbed1;
height: 80rpx;
line-height: 80rpx;
}
.uni-list-cell {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.uni-list-cell-hover {
background-color: #eee;
}
.uni-list-cell-pd {
padding: 22rpx 30rpx;
}
.uni-list-cell-left {
white-space: nowrap;
font-size: 28rpx;
padding: 0 30rpx;
}
.uni-list-cell-db,
.uni-list-cell-right {
flex: 1;
}
.uni-list-cell::after {
position: absolute;
z-index: 3;
right: 0;
bottom: 0;
left: 30rpx;
height: 1px;
content: '';
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: #c8c7cc;
}
.uni-list .uni-list-cell:last-child::after {
height: 0rpx;
}
.uni-list-cell-last.uni-list-cell::after {
height: 0rpx;
}
.uni-list-cell-divider {
position: relative;
display: flex;
color: #999;
background-color: #f7f7f7;
padding: 15rpx 20rpx;
}
.uni-list-cell-divider::before {
position: absolute;
right: 0;
top: 0;
left: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: #c8c7cc;
}
.uni-list-cell-divider::after {
position: absolute;
right: 0;
bottom: 0;
left: 0rpx;
height: 1px;
content: '';
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
background-color: #c8c7cc;
}
.uni-list-cell-navigate {
font-size: 30rpx;
padding: 22rpx 30rpx;
line-height: 48rpx;
position: relative;
display: flex;
box-sizing: border-box;
width: 100%;
flex: 1;
justify-content: space-between;
align-items: center;
}
.uni-list-cell-navigate {
padding-right: 36rpx;
}
.uni-navigate-badge {
padding-right: 50rpx;
}
.uni-list-cell-navigate.uni-navigate-right:after {
font-family: uniicons;
content: '\e583';
position: absolute;
right: 24rpx;
top: 50%;
color: #bbb;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.uni-list-cell-navigate.uni-navigate-bottom:after {
font-family: uniicons;
content: '\e581';
position: absolute;
right: 24rpx;
top: 50%;
color: #bbb;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.uni-list-cell-navigate.uni-navigate-bottom.uni-active::after {
font-family: uniicons;
content: '\e580';
position: absolute;
right: 24rpx;
top: 50%;
color: #bbb;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.uni-collapse.uni-list-cell {
flex-direction: column;
}
.uni-list-cell-navigate.uni-active {
background: #eee;
}
.uni-list.uni-collapse {
box-sizing: border-box;
height: 0;
overflow: hidden;
}
.uni-collapse .uni-list-cell {
padding-left: 20rpx;
}
.uni-collapse .uni-list-cell::after {
left: 52rpx;
}
.uni-list.uni-active {
height: auto;
}

91
src/common/styles/user.scss

@ -0,0 +1,91 @@
.login-box {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ffffff;
z-index: 99999;
}
.code-wrap {
display: flex;
.code-field {
flex: 1;
}
.code-btn {
margin-left: 10px;
}
i {
position: absolute;
top: 26px;
right: 15px;
cursor: pointer;
}
}
.back-login {
float: right;
cursor: pointer;
color: rgba(0, 0, 0, 0.54);
&:hover {
text-decoration: underline;
color: rgba(0, 0, 0, 0.87);
}
}
.active {
font-weight: bold;
color: rgba(0, 0, 0, 0.87) !important;
}
.forget-password {
cursor: pointer;
color: rgba(0, 0, 0, 0.54);
transition: all 300ms ease-in;
&:hover {
text-decoration: underline;
color: rgba(0, 0, 0, 0.87);
}
}
.code-img {
width: 112px !important;
height: 40px !important;
background-size: contain;
}
.button-style {
background-color: #e0e0e0;
color: #a6a6a6;
}
::v-deep input:-internal-autofill-selected {
background-color: #fff !important;
}
.wx-icon {
border-radius: 50%;
height: 50px;
width: 50px;
margin-top: 60px;
&:hover {
cursor: pointer;
background: #ddd;
}
}
.wechat-style {
color: #388e3c;
font-size: 40px;
}
.cursor-hover:hover {
cursor: pointer;
}

101
src/components/InputSearch/InputSearch.vue

@ -0,0 +1,101 @@
<template>
<view class="input-group flex-1">
<input :placeholder="placeholder" @input="search" @blur="hideList" v-model="backName" />
<view class="ul">
<view class="li" v-for="(item, index) in dataSource" :key="index" @tap="select(item)">{{ item.name }}</view>
</view>
</view>
</template>
<script>
export default {
props: {
placeholder: String, //
searchKey: String, //key
dataSource: {
type: Array,
default: function () {
//
return [];
},
},
},
data() {
return {
list: [],
name: '',
backName: '',
};
},
destroyed() {
clearTimeout(this.t);
},
methods: {
search(e) {
let val = e.detail.value;
console.log('val: ', val);
this.$emit('searchPrevTask', val);
// let arr = [];
// for (let i = 0; i < dataSource.length; i++) {
// if (dataSource[i].name.indexOf(val) !== -1) {
// arr.push(dataSource[i]);
// }
// }
// if (!val) {
// this.list = [];
// } else {
// this.list = arr;
// }
},
select(item) {
console.log('item: ', item);
this.backName = item.name;
this.$emit('select', item);
},
hideList() {
setTimeout(() => {
this.$emit('clearAllTasks');
}, 200);
},
},
};
</script>
<style lang="scss" scoped>
.input-group {
position: relative;
input {
border-bottom: 1upx solid #dcdfe6;
height: 90upx;
padding-left: 10upx;
font-size: 30upx;
box-sizing: border-box;
}
.uni-input-placeholder {
color: rgb(192, 196, 204);
font-size: 28upx;
}
.ul {
position: absolute;
left: 0;
top: 100%;
width: 100%;
z-index: 999;
background: #fff;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
max-height: 100px;
overflow-y: auto;
.li {
border-bottom: 1upx solid #f1f1f1;
padding: 16upx;
}
}
}
</style>

20
src/components/PrettyExchange/PrettyExchange.vue

@ -8,7 +8,7 @@
:index="index"
:item="item"
:menuList="menuList"
@chooseAction="chooseAction"
@setData="setData"
@openSubProject="openSubProject"
/>
</view>
@ -119,6 +119,9 @@
</view>
</view>
<!-- 移动悬浮 end -->
<!-- 项目操作面板 -->
<u-action-sheet :list="menuList" :tips="tips" @click="chooseAction" v-model="showMenu"></u-action-sheet>
</view>
</template>
@ -156,6 +159,7 @@ export default {
color: '#909399',
fontSize: 28,
},
projectId: 0,
menuList: [{ text: '复制' }, { text: '编辑' }, { text: '删除' }, { text: '置顶' }, { text: '排序' }],
// show: false,
border: 'border border-blue-500 shadow rounded-md',
@ -223,8 +227,20 @@ export default {
.exec();
},
setData(flag, projectId, tips) {
this.showMenu = flag;
this.projectId = projectId;
this.tips = tips;
},
chooseAction(e) {
let data = { index: e, projectId: this.projectId };
// this.$emit('chooseAction', data);
this.actionFun(data);
},
//
chooseAction(obj) {
actionFun(obj) {
let action = this.menuList[obj.index].text;
if (action === '排序') {
this.changeEvent = true;

453
src/components/ProjectDetail/ProjectDetail.vue

@ -0,0 +1,453 @@
<template>
<view :style="{ height: height }" class="flex flex-col overflow-hidden u-font-14">
<!-- 标题栏 -->
<Title />
<view class="container flex flex-col flex-1 mx-auto overflow-hidden bg-gray-100">
<!-- 角色栏 -->
<Roles />
<!-- 日常任务面板 -->
<!-- <Globals /> -->
<!-- 定期任务面板 -->
<!-- <TimeLine @getTasks="getTasks" class="flex-1 overflow-hidden" ref="timeLine" /> -->
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
import { setPlaceholderTasks, computeFillPlaceholderTaskCount } from '@/utils/task';
import { flatten } from 'lodash';
export default {
data() {
return { height: '' };
},
computed: {
...mapState('user', ['user', 'token']),
...mapState('role', ['visibleRoles', 'roleId']),
...mapState('task', ['timeNode', 'timeUnit', 'tasks', 'regularTask', 'newProjectInfo', 'showSkeleton', 'showScrollTo']),
...mapState('project', ['project']),
...mapGetters('task', ['timeGranularity']),
...mapGetters('project', ['projectId']),
...mapGetters('user', ['userId']),
},
onLoad(options) {
if (options.share && options.share === '1') {
this.shareInit(options);
} else {
this.init(options);
}
},
watch: {
/**
* 当时间基准点发生变化时
* 重新根据时间和角色查询普通日常任务
* 永久日常任务不发生改变
*/
timeNode(val) {
if (val && this.roleId) {
this.clearTasksData();
this.getGlobalData(); //
this.initPlanTasks(); //
}
},
/**
* 当角色发生变化时
* 重新查询永久日常任务和普通日常任务
* 注意: 切换角色后 重新设置了时间基准点 时间基准点一定会变
* 所以监听时间基准点获取 可变日常任务即可 这里不用获取 避免重复获取
*/
roleId(val) {
if (val) {
this.setTimeNode(Date.now());
//
const params = { roleId: val, projectId: this.projectId };
this.getPermanent(params);
}
},
//
newProjectInfo(val) {
if (val && val.projectId && val.url) {
this.$u.route('/', { u: this.userId, p: val.projectId, url: val.url });
this.clearTasksData();
this.setRoleId('');
const options = this.$route.query;
this.init(options);
}
},
},
mounted() {
const system = uni.getSystemInfoSync();
this.height = system.windowHeight + 'px';
},
onUnload() {
this.clearTasksData();
this.setRoleId('');
},
methods: {
...mapActions('user', ['getToken']),
...mapActions('task', ['getRegulars', 'getPermanent', 'getGlobal']),
...mapMutations('user', ['setToken']),
...mapMutations('project', ['setProject', 'setProjectName']),
...mapMutations('role', ['setInvisibleRoles', 'setVisibleRoles', 'setRoleId']),
...mapMutations('task', [
'setPermanents',
'setUpTasks',
'setDownTasks',
'setDailyTasks',
'setTimeNode',
'clearTasks',
'clearEndFlag',
'setShowSkeleton',
'setTopEnd',
'setBottomEnd',
'setShowScrollTo',
]),
//
async initPlanTasks() {
this.setPrevPlaceholderTasks(); //
this.setNextPlaceholderTasks(); //
// // this.$nextTick(() => this.$refs.timeLine.setScrollPosition()); //
await this.getInitTasks(); //
//
let timer = null;
timer = setInterval(() => {
if (this.showScrollTo) {
clearInterval(timer);
this.$nextTick(() => this.$refs.timeLine.setScrollPosition());
}
}, 500);
},
// ||
getInitTasks() {
//
function preloadFn(that) {
const detailId = that.tasks.findIndex(task => task.detailId);
const arr = [];
that.tasks.forEach(task => {
if (task.detailId) {
arr.push(task);
}
});
if (detailId !== -1) {
// 1
const { pageCount } = that.$t.task;
that.$nextTick(() => {
//
const { tasks, timeGranularity } = that;
that.getTasks({ timeNode: +tasks[detailId].planStart, queryType: 0, queryNum: pageCount });
//
const nextQueryTime = +that.$t.time.add(+arr[arr.length - 1].planStart, 1, timeGranularity);
that.getTasks({ timeNode: nextQueryTime, queryType: 1, queryNum: pageCount });
});
} else {
//
//
// that.setPrevPlaceholderTasks();
// //
// that.setNextPlaceholderTasks();
}
}
//
this.getTasks({ queryType: 0 }); //
// id
this.getTasks({ queryType: 1 }, preloadFn); //
},
/**
* 根据时间基准点和角色查找定期任务
* @param {object} query
* @param {string} query.roleId 角色id
* @param {string} query.timeNode 时间基准点 默认当前
* @param {string} query.timeUnit 时间颗粒度 默认天
* @param {string} query.queryNum 查找颗粒度数量 默认3个
* @param {number} query.queryType 0向上查找 1向下查找(默认) 下查包含自己上查不包含
*/
getTasks(query, fn) {
this.setShowSkeleton(false);
const params = this.generateGetTaskParam(query);
this.$t.$q.getRegularTask(params, (err, data) => {
this.setShowSkeleton(false);
if (err) {
// TODO:
console.error('err: ', err);
} else {
this.setShowScrollTo(true);
//
//
if (data && data.length) {
this.replacePrevData(data, params.queryType);
params.queryType === 0 ? this.setTopEnd(false) : this.setBottomEnd(false);
} else {
// TODO: 0 -> 1 ->
params.queryType === 0 ? this.setPrevPlaceholderTasks() : this.setNextPlaceholderTasks();
}
if (this.tasks.length && fn) {
fn(this);
}
}
});
},
/**
* 生成getTasks所用的参数
* @param {object} query getTasks传递的参数
*/
generateGetTaskParam(query) {
const { roleId, timeNode, timeUnit, projectId } = this;
return {
roleId,
timeNode: query.timeNode || timeNode,
timeUnit: query.timeUnit || timeUnit,
queryNum: query.queryNum || 3,
queryType: query.queryType,
projectId,
};
},
//
setPrevPlaceholderTasks() {
this.setTopEnd(true);
let startTime = '';
const { tasks } = this;
if (!tasks || !tasks.length) {
startTime = Date.now(); //
} else {
startTime = tasks[0].planStart - 0; //
}
const placeholderTasks = setPlaceholderTasks(startTime, true, this.timeGranularity);
this.setUpTasks(placeholderTasks);
},
//
setNextPlaceholderTasks() {
this.setBottomEnd(true);
let startTime = '';
if (!this.tasks || !this.tasks.length) {
startTime = Date.now();
} else {
startTime = +this.tasks[this.tasks.length - 1].planStart;
}
const initData = setPlaceholderTasks(startTime, false, this.timeGranularity);
this.setDownTasks(initData);
},
/**
* 用拿到的新数据 替换 时间刻度/旧数据
* 先对比 新旧数据的 始末时间 补齐刻度
* 再遍历对比 用任务替换刻度
* @param {array} data 服务端返回的新数据 上边已经处理过空值
* @param {number} type 0 -> 向上 1->向下
*/
replacePrevData(data, type) {
const { timeGranularity } = this;
let oldTasks = this.fillPlaceholderTask({ tasks: this.tasks, data, timeGranularity }); //
//
// TODO: tasks
oldTasks.forEach((taskItem, index) => {
const arr = data.filter(dataItem => this.$moment(+dataItem.planStart).isSame(+taskItem.planStart, timeGranularity));
if (arr && arr.length) {
oldTasks.splice(index, 1, [...arr]); // array, [{},{},[],[],{}]
}
});
oldTasks = flatten(oldTasks); // 1
this.clearTasks(); // setUpTasks setUpTasks
type === 0 ? this.setUpTasks(oldTasks) : this.setDownTasks(oldTasks);
},
/**
* 超出旧数据上下限 补齐时间刻度到新数据的起始时间颗粒度
*/
fillPlaceholderTask({ tasks, data, timeGranularity }) {
const { prev, next } = computeFillPlaceholderTaskCount({ tasks, data, timeGranularity });
if (prev) {
const newTasks = setPlaceholderTasks(+tasks[0].planStart, true, timeGranularity, prev);
this.setUpTasks(newTasks);
}
if (next) {
const newTasks = setPlaceholderTasks(+tasks[tasks.length - 1].planStart, false, timeGranularity, next);
this.setDownTasks(newTasks);
}
return this.tasks;
},
/**
* 初始化
* @param {object | null} options
*/
init(options) {
if (!this.token) {
// tokenuserIdtoken
// token userId
if (!options || !options.u) {
this.$t.ui.showToast('缺少用户信息参数'); // u (userId)
} else {
this.getToken(options.u);
}
}
//
options && options.pname && this.setProjectName(options.pname);
if (!options || !options.p) {
this.$t.ui.showToast('缺少项目信息参数'); // id
} else {
if (options.p !== this.$t.storage.getStorageSync('projectId')) {
console.log('切项目了');
this.$t.storage.setStorageSync('roleId', '');
}
// TODO
this.getProjectById({ projectId: options.p, num: 0 }); // id
}
},
//
async shareInit(options) {
const storageUser = this.$t.storage.getStorageSync('user');
const user = storageUser ? JSON.parse(storageUser) : null;
if (user && user.id) {
await this.getToken(user.id);
const res = await this.clickShare({ code: options.shareId });
if (res && res.projectId) {
let query = { ...this.$route.query };
query = {
u: user.id,
p: res.projectId,
};
this.$router.push({ path: this.$route.path, query });
this.init(query);
}
} else {
this.$t.ui.showToast('缺少用户信息参数,请登录');
}
},
/**
* 点击分享连接
* @param {any} commit
* @param {object} param 请求参数
*/
async clickShare(param) {
try {
const data = await this.$u.api.clickShare(param);
return data;
} catch (error) {
this.$t.ui.showToast(error.msg || '获取失败');
}
},
/**
* 通过项目id获取项目信息
* @param {object} params 提交的参数
*/
async getProjectById(params) {
try {
const data = await uni.$u.api.findProjectById(params);
this.setProject(data);
// id
this.getRoles(params);
} catch (error) {
console.log('error: ', error || '获取项目信息失败');
}
},
/**
* 通过项目id获取角色信息
* @param {string} projectId
* @param {object} params 提交的参数
*/
getRoles(params) {
this.$t.$q.findShowRole(params, (err, data) => {
if (err) {
console.error('err: ', err || '获取角色信息失败');
} else {
this.setInvisibleRoles(data ? data.invisibleList : []);
this.setVisibleRoles(data ? data.visibleList : []);
this.setInitialRoleId(data ? data.visibleList : []);
}
});
},
//
setInitialRoleId(visibleList) {
if (!visibleList || !visibleList.length) return;
const index = visibleList.findIndex(item => +item.mine === 1);
const currentRole = index > 0 ? visibleList[index] : visibleList[0];
const storageRoleId = this.$t.storage.getStorageSync('roleId');
const currentRoleId = storageRoleId ? storageRoleId : currentRole ? currentRole.id : '';
this.setRoleId(currentRoleId);
// storage
this.$t.storage.setStorageSync('roleId', '');
},
//
getGlobalData() {
const { roleId, timeNode, timeUnit, projectId } = this;
const param = { roleId, timeNode, timeUnit, projectId };
this.getGlobal(param);
},
//
clearTasksData() {
//
this.setPermanents([]);
this.setDailyTasks([]);
//
this.clearTasks();
//
//
this.clearEndFlag();
},
/**
* 查询医院是否填写了调查问卷
* @param {string} projectId 项目id
*/
// async handleQueryNotWrite(projectId) {
// try {
// const param = { projectId };
// const data = await this.$u.api.queryNotWrite(param);
// console.log('data: ', data);
// this.questionnaires = data;
// } catch (error) {
// console.error('error: ', error);
// }
// },
//
// openQuestionnaire(value) {
// this.chooseItem = value;
// if (this.count === 1 || this.chooseItem) {
// window.location.href = 'https://www.baidu.com/';
// } else {
// this.showQuestionList = true;
// }
// },
},
};
</script>
<style lang="scss" scoped>
.border-b {
border-bottom: 1px solid #e4e7ed;
}
</style>

37
src/components/Projects/ProjectItem.vue

@ -77,12 +77,12 @@
</view>
<!-- 项目操作面板 -->
<!-- <u-action-sheet :list="menuList" :tips="tips" @click="$emit('chooseAction', $event)" v-model="showMenu"></u-action-sheet> -->
<u-action-sheet :list="menuList" :tips="tips" @click="chooseAction" v-model="showMenu"></u-action-sheet>
<!-- <u-action-sheet :list="menuList" :tips="tips" @click="chooseAction" v-model="showMenu"></u-action-sheet> -->
</view>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapMutations } from 'vuex';
export default {
props: {
@ -117,16 +117,37 @@ export default {
computed: mapGetters('user', ['userId']),
methods: {
...mapMutations('project', ['setProject']),
//
openProject(project) {
const { name, id, url } = project;
url && (uni.$t.domain = url);
this.$u.route('pages/project-webview/project-webview', {
// this.$u.route('pages/project-webview/project-webview', {
// u: this.userId,
// p: id,
// pname: name,
// url: encodeURIComponent(url),
// });
let projectInfo = {
u: this.userId,
id: id,
name: name,
url: encodeURIComponent(url),
};
this.setProject(projectInfo);
this.$t.storage.setStorageSync('projectInfo', projectInfo);
this.$u.route('pages/right-window/right-window', {
u: this.userId,
p: id,
pname: name,
url: encodeURIComponent(url),
});
uni.$emit('updateDetail', projectInfo);
},
/**
@ -136,12 +157,14 @@ export default {
this.showMenu = true;
this.projectId = project.id;
this.tips.text = project.name;
},
chooseAction(e) {
let data = { index: e, projectId: this.projectId };
this.$emit('chooseAction', data);
this.$emit('setData', this.showMenu, this.projectId, this.tips);
},
// chooseAction(e) {
// let data = { index: e, projectId: this.projectId };
// this.$emit('chooseAction', data);
// },
},
};
</script>

252
src/components/Roles/Roles.vue

@ -0,0 +1,252 @@
<template>
<view class="px-2 bg-white wrap">
<view class="home-box u-skeleton">
<scroll-view :enable-flex="true" :scroll-left="scrollLeft" :throttle="false" scroll-with-animation scroll-x @scroll="scroll">
<view class="tab-box">
<!-- 角色项
default-tab-choice 我的角色 && 当前展示
default-tab-item 我的角色 && 当前不展示
tab-choice 不是我的 && 当前展示
-->
<view
:class="{
'default-tab-choice': item.mine == 1 && roleId === item.id,
'default-tab-item': item.mine == 1 && roleId !== item.id,
'tab-choice': item.mine == 0 && roleId === item.id,
}"
:key="index"
@click="changeRole(item.id, index)"
class="tab-item"
v-for="(item, index) in roles"
>
<view class="tab-children u-skeleton-fillet u-font-14">
{{ item.name }}
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 骨架屏 -->
<!-- <u-skeleton :animation="true" :loading="loading" bg-color="#fff"></u-skeleton> -->
</view>
</template>
<script>
import { mapState, mapMutations, mapActions } from 'vuex';
export default {
name: 'Roles',
data() {
return {
tabIndex: 0, // 访 index 0
tabList: [], // tab dom
scrollLeft: 0, // scrollview
loading: false, //
roles: [
{ id: 1, name: '项目经理', mine: 0, pm: 1, sequence: 1 },
{ id: 2, name: '运维', mine: 0, pm: 0, sequence: 2 },
],
roleLeft: 0,
};
},
computed: {
...mapState('role', ['visibleRoles', 'roleId']),
...mapState('task', ['tasks']),
},
watch: {
visibleRoles(val) {
if (val && val.length) {
this.roles = [...this.visibleRoles];
this.loading = false;
}
},
},
mounted() {
if (!this.visibleRoles || !this.visibleRoles.length) {
this.loading = true;
} else {
this.loading = false;
this.roles = [...this.visibleRoles];
}
setTimeout(() => {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.selectAll('.tab-children')
.boundingClientRect(data => {
this.roleLeft = data[0].left;
})
.exec();
});
}, 300);
},
methods: {
...mapActions('task', ['handleRegularTask']),
...mapMutations('role', ['setRoleId']),
...mapMutations('task', ['setPermanents', 'clearEndFlag']),
scroll(e) {
this.scrollLeft = e.detail.scrollLeft;
},
//
setCurrentRole(index) {
const query = uni.createSelectorQuery().in(this);
query
.selectAll('.tab-children')
.boundingClientRect(data => {
data.forEach(item => {
this.tabList.push({ width: item.width });
});
})
.exec();
const system = uni.getSystemInfoSync(); //
//
let screenWidth = system.windowWidth;
//
let left = 0;
for (let i = 0; i < index; i++) {
left += this.tabList[i].width + (this.roleLeft - 8) * 2;
}
left += (this.tabList[index].width + (this.roleLeft - 8) * 2) / 2;
if (left > screenWidth) {
this.scrollLeft = left - screenWidth + screenWidth / 2;
} else if (left > screenWidth / 2) {
this.scrollLeft = left - screenWidth / 2;
} else if (left < screenWidth / 2) {
this.scrollLeft = 0;
}
},
//
// projectroleId
// projectroleId
changeRole(id, index) {
try {
// script
this.clearPluginScript();
this.$nextTick(() => {
this.setRoleId(id);
// index
this.setCurrentRole(index);
});
} catch (error) {
console.error('role.vue changeRole error: ', error);
}
},
// script
clearPluginScript() {
try {
const scripts = document.querySelectorAll('script[data-type=plugin]');
for (let i = 0; i < scripts.length; i++) {
document.body.removeChild(scripts[i]);
}
} catch (error) {
console.error('clearPluginScript error: ', error);
}
},
},
};
</script>
<style lang="scss" scoped>
.home-box {
// sticky
position: sticky;
top: 0;
background: #fff; //
/* #ifdef H5 */
// h5 44px
top: 88rpx;
/* #endif */
.tab-box {
position: relative;
white-space: nowrap;
// height: 84rpx;
.tab-item {
// width: 20%;
// height: 84rpx;
padding: 20rpx 24rpx;
position: relative;
display: inline-block;
text-align: center;
font-size: 30rpx;
transition-property: background-color, width;
}
.default-tab-item {
color: $roleChoiceColor;
}
.default-tab-choice {
//
position: relative;
color: $roleChoiceColor;
font-weight: 600;
}
.default-tab-choice:before {
content: '';
position: absolute;
left: 0;
bottom: -20rpx;
width: 100%;
height: 6rpx;
border-radius: 2rpx;
background: $roleChoiceColor;
}
.tab-choice {
//
position: relative;
color: $uni-color-primary;
font-weight: 600;
}
.tab-choice:before {
content: '';
position: absolute;
left: 0;
bottom: -20rpx;
width: 100%;
height: 6rpx;
border-radius: 2rpx;
background: $uni-color-primary;
}
}
}
// //
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
/* #ifdef H5 */
// 穿H5scroll-view
scroll-view ::v-deep ::-webkit-scrollbar {
display: none;
}
/* #endif */
.skeleton {
height: 44rpx;
}
</style>

84
src/components/Skeleton/READ_ME.md

@ -0,0 +1,84 @@
# skeleton组件
### 1.描述
> 此组件用于加载数据时占位图显示,跟vant-ui骨架屏用法相似,但比vant-ui更灵活
### 2.用法
- 基本用法
代码:
```vue
//基本用法
<skeleton :row="3" animate :loading="loading" >
<view>
content
</view>
</skeleton>
```
- **显示 title ——通过 **title 属性显示title占位图
代码:
```vue
//显示 title——通过 title 属性显示title占位图
<skeleton :row="3" title animate :loading="loading">
<view>
content
</view>
</skeleton>
```
- 显示头像(上面)——通过avatar=‘top’让头像的占位图上面显示
代码:
```vue
<skeleton :avatar="top" avatarAlign="left" :row="3" animate :loading="loading" style="margin-top:24rpx;">
<view>
content
</view>
</skeleton>
```
- 显示头像(左边)——通过avatar=‘left’让头像的占位图左边显示
代码:
```vue
<skeleton title :avatar="left" :row="3" animate :loading="loading" style="margin-top:24rpx;">
<view>
content
</view>
</skeleton>
```
- 显示banner**——通过 **banner属性显示banner占位图(只显示banner,不显示内容占位图时设置row="0")
代码:
```vue
<skeleton banner :row="0" animate :loading="loading" style="margin-top:24rpx;">
<view>
content
</view>
</skeleton>
```
###
### 3. API
### Props
| **属性名** | **说明** | **类型** | **默认值** | 可取值 |
| --- | --- | --- | --- | --- |
| loading | 是否显示骨架屏 | Boolean | true | true/false |
| row | 段落行数 | Number | String | 3 | 0表示不展现 |
| rowWidth | 段落行宽度 | Boolean &#124; Number | '100%' | |
| title | 是否显示标题 | Boolean &#124; String | false | |
| banner | 是否显示banner | Boolean &#124; String | false | |
| animate | 是否开启动画 | Boolean &#124; String | false | |
| avatar | 头像位置 | Boolean &#124; String | ''空 | left/top |
| avatarSize | 头像大小 | String | - | |
| avatarShape | 头像形状 | String | circle | circle/round |

187
src/components/Skeleton/Skeleton.vue

@ -0,0 +1,187 @@
<template>
<view>
<view :class="[avatarClass, animationClass]" class="lx-skeleton" v-show="loading">
<view :class="[avatarShapeClass, bannerClass]" :style="{ width: avatarSize, height: avatarSize }" class="avatar-class"></view>
<view :style="{ width: rowWidth }" class="row">
<view class="row-class lx-skeleton_title" v-if="title"></view>
<view :key="index" class="row-class" v-for="(item, index) in row"></view>
</view>
</view>
<slot v-if="!loading"></slot>
</view>
</template>
<script>
/**
* skeleton 骨架屏
* @description 用于加载数据时占位图显示跟Vant-UI用法相似但比Vant-UI更灵活
* @property {Boolean} loading 是否显示骨架屏默认为true
* @property {Number | String} row 段落行数默认为3
* @property {Boolean | Number} rowWidth 段落行宽度默认为100%
* @property {Boolean | String} title 是否显示标题默认为false
* @property {Boolean | String} banner 是否显示banner默认为false
* @property {Boolean | String} animate 是否开启动画默认为false
* @property {Boolean | String} avatar 头像位置
* @property {String} avatarSize 头像大小
* @property {String} avatarShape 头像形状默认为circle
*
* */
export default {
props: {
loading: {
type: Boolean,
default: true,
},
row: {
type: Number,
default: 3,
},
title: {
type: String,
default: '',
},
avatar: {
type: String,
default: '',
},
animate: {
type: Boolean,
default: false,
},
avatarSize: { type: String },
rowWidth: {
type: String,
default: '100%',
},
avatarShape: {
type: String,
default: 'circle',
},
banner: {
type: Boolean,
default: false,
},
// avator-size:{
// type: String,
// defualt: '32px'
// }
},
computed: {
avatarClass() {
if (this.avatar == 'top') {
return ['lx-skeleton_avator__top'];
} else if (this.avatar == 'left') {
return ['lx-skeleton_avator__left'];
} else {
return '';
}
},
animationClass() {
return [this.animate ? 'lx-skeleton_animation' : ''];
},
slotClass() {
return [!this.loading ? 'show' : 'hide'];
},
avatarShapeClass() {
return [this.avatarShape == 'round' ? 'lx-skeleton_avator__round' : ''];
},
bannerClass() {
return [this.banner ? 'lx-skeleton_banner' : ''];
},
},
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.lx-skeleton {
background-color: #fff;
padding: 12px;
}
.lx-skeleton_avator__left {
display: flex;
width: 100%;
}
.lx-skeleton_avator__left .avatar-class,
.lx-skeleton_avator__top .avatar-class {
background-color: #f2f3f5;
border-radius: 50%;
width: 32px;
height: 32px;
}
.lx-skeleton_avator__left .avatar-class.lx-skeleton_avator__round,
.lx-skeleton_avator__top .avatar-class.lx-skeleton_avator__round {
border-radius: 0;
width: 32px;
height: 32px;
}
.lx-skeleton_avator__left .avatar-class {
margin-right: 16px;
}
.lx-skeleton_avator__top .avatar-class {
margin: 0 auto 12px auto;
}
.row-class {
width: 100%;
height: 16px;
background-color: #f2f3f5;
margin-top: 12px;
}
.row-class:first-child {
margin-top: 0;
}
.row {
flex: 1;
}
.lx-skeleton_avator__left .row {
width: calc(100% - 48px);
}
.row-class:last-child {
width: 60%;
}
.lx-skeleton_animation .row-class {
animation-duration: 1.5s;
animation-name: blink;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
@keyframes blink {
50% {
opacity: 0.6;
}
}
.lx-skeleton_title {
width: 40%;
}
.show {
display: block;
}
.hide {
display: none;
}
.lx-skeleton .lx-skeleton_banner {
width: 92%;
margin: 10px auto;
height: 64px;
border-radius: 0;
background-color: #f2f3f5;
}
</style>

251
src/components/Title/Title.vue

@ -0,0 +1,251 @@
<template>
<view class="pro-title relative">
<view class="pro-navbar flex items-center justify-between px-4">
<view class="pro-name font-bold">
<view class="truncate">{{ projectInfo.name }}</view>
</view>
<view class="">
<!-- <u-icon class="m-1" name="xuanzhong2" custom-prefix="custom-icon" size="20px" @click="lwbs"></u-icon> -->
<u-icon class="" name="shuaxin1" custom-prefix="custom-icon" size="20px" @click="projectOverview"></u-icon>
<u-icon class="ml-4" name="home" custom-prefix="custom-icon" size="20px" @click="openIndex"></u-icon>
<u-icon class="ml-4" name="xuanxiang" custom-prefix="custom-icon" size="20px" @click="operation"></u-icon>
</view>
</view>
<view
class="mask absolute"
v-if="maskShow"
@click="closeMask"
style="width: 400px; height: 100vh; z-index: 21; position: fixed; background: rgba(0, 0, 0, 0.3)"
></view>
<!-- 右上角 ... 弹窗 -->
<view class="popup border shadow-sm" v-if="show">
<view class="flex pb-3 border-b-1">
<u-icon name="plus-circle" size="36" style="margin: 0 15px 3px 0"></u-icon>
<view @click="createTask">新建任务</view>
</view>
<view class="flex pt-3">
<u-icon name="share" size="32" style="margin: 0 15px 3px 0"></u-icon>
<view @click="share">分享项目</view>
</view>
</view>
<!-- 分享项目弹窗 -->
<ShareProject v-if="secondShow" class="second-popup" />
<!-- 新建任务弹窗 -->
<CreateTask
:startTime="startTime"
:endTime="endTime"
@showTime="showTime"
@closeMask="closeMask"
class="third-popup flex transition-transform"
v-if="createTaskShow"
/>
</view>
</template>
<script>
import { mapGetters } from 'vuex';
import CreateTask from './components/CreateTask.vue';
import ShareProject from './components/ShareProject.vue';
export default {
name: 'ProjectTitle',
components: { CreateTask, ShareProject },
data() {
return {
projectInfo: {}, //
show: false, // ...
createTaskShow: false, //
secondShow: false, //
maskShow: false, //
showStart: false,
showEnd: false,
startTime: '', //
endTime: '', //
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true,
},
};
},
created() {
let info = this.$t.storage.getStorageSync('projectInfo');
this.projectInfo = JSON.parse(info);
uni.$on('updateDetail', e => {
console.log(e);
this.projectInfo = e;
});
},
computed: { ...mapGetters('user', ['userId']) },
methods: {
showTime() {
this.showStart = !this.showStart;
},
//
confirmStartTime(e) {
this.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
this.showEnd = true;
},
//
confirmEndTime(e) {
this.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
},
//
onBack() {
// eslint-disable-next-line no-undef
const pages = getCurrentPages(); //
console.log('历史pages: ', pages.length);
if (pages.length > 1) {
// uni.webView.navigateBack();
uni.navigateBack();
} else {
// this.$u.route('/', { u: this.userId });
// uni.webView.reLaunch({ url: `/pages/index/index?u=${this.userId}` });
uni.reLaunch({ url: `/pages/index/index?u=${this.userId}` });
}
},
// LWBS
lwbs() {
// this.$t.ui.showToast('LWBS');
},
//
projectOverview() {
// this.$t.ui.showToast('');
},
//
openIndex() {
console.log(111);
// uni.webView.reLaunch({ url: `/pages/index/index?u=${this.userId}` });
uni.reLaunch({ url: `/pages/index/index?u=${this.userId}` });
},
//
operation() {
// this.$t.ui.showToast('');
this.show = !this.show;
},
//
createTask() {
// ...
this.show = false;
//
this.maskShow = true;
//
this.createTaskShow = true;
},
//
share() {
// ...
this.show = false;
//
this.maskShow = true;
//
this.secondShow = true;
},
//
closeMask() {
//
this.maskShow = false;
//
this.secondShow = false;
//
this.createTaskShow = false;
},
},
};
</script>
<style lang="scss" scoped>
.pro-navbar {
height: 44px;
}
.pro-name {
width: calc(100% - 120px);
}
.second-popup {
background: #ffffff;
position: absolute;
left: 20px;
top: calc((100vh - 210px) / 2);
z-index: 33;
border-radius: 5px;
width: 360px;
}
.third-popup {
background: #ffffff;
position: absolute;
left: 20px;
top: calc((100vh - 430px) / 2);
z-index: 33;
border-radius: 5px;
width: 360px;
}
.popup {
width: 40%;
background: #fff;
position: absolute;
right: 0;
z-index: 99;
padding: 15px;
color: black;
animation: opacity 0.5s ease-in;
}
@keyframes opacity {
0% {
opacity: 0;
}
50% {
opacity: 0.8;
}
100% {
opacity: 1;
}
}
::v-deep .u-slot-content {
min-width: 0;
}
::v-deep .u-dropdown__content {
min-height: 120px !important;
height: auto !important;
overflow-y: auto;
background: #fff !important;
transition: none !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
::v-deep .u-dropdown__menu__item .u-flex {
justify-content: space-between;
width: 100%;
height: 100%;
flex-wrap: nowrap;
border: 1px solid #afbed1;
padding: 0 8px;
}
::v-deep .u-dropdown__content__mask {
display: none;
}
</style>

479
src/components/Title/components/CreateTask.vue

@ -0,0 +1,479 @@
<template>
<div class="new-projects-box">
<div class="form">
<!-- 项目名称 -->
<view class="mb-3 font-bold text-base flex justify-center">新建任务</view>
<div class="flex items-center mb-2">
<div>名称<span class="text-red-500">*</span></div>
<u-input max-length="5" v-model="name" :type="type" :border="border" />
</div>
<!-- 起止时间 -->
<div class="mb-2">
<div>起止时间</div>
<u-input placeholder="请选择起止时间" v-model="timeValue" :type="type" :border="border" @click="$emit('showTime')" />
</div>
<!-- 多选框 -->
<div class="flex justify-between items-center">
<div>负责人<span class="text-red-500">*</span></div>
<div class="flex-1" v-if="hasRole">{{ roleName }}</div>
<div label="负责人" class="flex-1" v-else>
<u-dropdown disabled ref="uDropdown" placeholder="请选择负责人">
<u-dropdown-item :title="roleList">
<view class="slot-content bg-white">
<div
class="flex flex-row justify-between mb-1 drop-item"
v-for="(role, roleIndex) in roleOptions"
:key="roleIndex"
@click="change(roleIndex)"
>
<view v-model="role.id">{{ role.name }}</view>
<u-icon v-if="role.dropdownShow" name="checkbox-mark" color="#2979ff" size="28"></u-icon>
</div>
</view>
</u-dropdown-item>
</u-dropdown>
</div>
</div>
<!-- 下拉图标 -->
<div class="flex justify-center my-6">
<u-icon v-if="arrow" name="arrow-down" size="28" @click="openDropdown"></u-icon>
<u-icon v-else name="arrow-up" size="28" @click="closeSecondDropdown"></u-icon>
</div>
<!-- 下拉框的内容 -->
<div v-if="show" class="mb-6">
<!-- 描述 -->
<div class="flex items-center mb-2">
<div>描述</div>
<u-input v-model="description" max-length="48" type="textarea" height="36" auto-height :border="border" />
</div>
<!-- 所属项目 -->
<div class="w flex items-center mb-2">
<div>所属项目<span class="text-red-500">*</span></div>
<div>{{ project.name }}</div>
</div>
<!-- 所属任务 -->
<div class="w flex items-center mb-2" v-if="task && task.id">
<div>所属任务</div>
<div>{{ task.name }}</div>
</div>
<!-- 上道工序 -->
<div class="flex items-center mb-2">
<div>上道工序</div>
<InputSearch
@searchPrevTask="searchPrevTask"
:dataSource="allTasks"
@select="handleChange"
@clearAllTasks="clearAllTasks"
placeholder="请输入上道工序"
/>
</div>
<!-- 检查人多选框 -->
<div class="flex justify-between items-center">
<div>检查人<span class="text-red-500">*</span></div>
<div label="检查人" class="flex-1">
<u-dropdown ref="dropdown">
<u-dropdown-item :title="checkerList">
<view class="slot-content bg-white">
<div
class="flex flex-row justify-between mb-1 drop-item"
v-for="(checkoutOption, Index) in checkoutOptions"
:key="Index"
@click="choose(Index)"
>
<view v-model="checkoutOption.value">{{ checkoutOption.name }}</view>
<u-icon v-if="checkoutOption.dropdownShow" name="checkbox-mark" color="#2979ff" size="28"></u-icon>
</div>
</view>
</u-dropdown-item>
</u-dropdown>
</div>
</div>
<!-- 是否是日常任务 -->
<div class="flex justify-between items-center mt-6">
是否是日常任务
<u-switch v-model="isGlobal" size="28"></u-switch>
</div>
<div class="mt-6">
<div>交付物</div>
<div v-for="(sort, sortIndex) in deliverSort" :key="sortIndex">
<u-input
@blur="addDeliverInput"
v-model="sort.name"
:placeholder="`交付物名称${sortIndex + 1}`"
:type="type"
:border="border"
/>
</div>
</div>
</div>
<div class="flex items-center mb-6">
<u-button type="primary" size="medium" @click="setParameters">提交</u-button>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
props: {
startTime: {
type: String,
default: '',
},
endTime: {
type: String,
default: '',
},
task: {
type: Object,
default: null,
},
},
data() {
return {
arrow: true,
show: false,
isGlobal: false, //
name: '', //
showChooseTime: false,
timeValue: '', //
description: '', //
projectShow: false, //
processTaskId: '', //
type: 'text',
border: true,
roleList: undefined, //
checkerList: undefined, //
roleOptions: [], //
checkoutOptions: [], //
roleIdList: [], // id
checkerIdList: [], // id
deliverables: [], //
deliverSort: [{ name: '' }], //
allTasks: [],
roleName: '', //
hasRole: false, //
};
},
computed: {
...mapState('role', ['visibleRoles', 'roleId']),
...mapState('project', ['project']),
...mapState('task', ['tasks']),
...mapGetters('project', ['projectId']),
},
watch: {
endTime(val) {
if (val) {
this.timeValue = this.startTime + ' 至 ' + val;
}
},
},
mounted() {
//
if (this.visibleRoles.length) {
this.visibleRoles.forEach(role => {
role.dropdownShow = false;
role.status = false;
});
}
this.roleOptions = this.$u.deepClone(this.visibleRoles);
this.checkoutOptions = this.$u.deepClone(this.visibleRoles);
//
if (this.roleId) {
const item = this.visibleRoles.find(r => r.id === this.roleId);
if (item) {
this.roleName = item.name;
this.hasRole = true;
}
}
},
methods: {
...mapMutations('task', ['updateTasks']),
...mapActions('task', ['getPermanent']),
//
change(index) {
let arr = [...this.roleOptions];
//
arr[index].dropdownShow = !arr[index].dropdownShow;
//
this.roleList = arr[index].name;
let shows = '';
// arr
arr.map(val => {
if (val.dropdownShow === true) {
shows += val.name + ',';
this.roleIdList.push(val.id);
}
});
this.roleOptions = [...arr];
// ','
this.roleList = shows.slice(0, shows.length - 1);
},
//
choose(index) {
let arr = [...this.checkoutOptions];
//
arr[index].dropdownShow = !arr[index].dropdownShow;
//
this.checkerList = arr[index].name;
let shows = '';
// arr
arr.map(val => {
if (val.dropdownShow === true) {
shows += val.name + ',';
this.checkerIdList.push(val.id);
}
});
this.checkoutOptions = [...arr];
// ','
this.checkerList = shows.slice(0, shows.length - 1);
// this.roleList = arr[value - 1].name;
},
//
openDropdown() {
this.arrow = !this.arrow;
this.show = true;
},
//
closeSecondDropdown() {
this.arrow = !this.arrow;
this.show = false;
},
/**
* 模糊查询 查找项目下的任务
* @param name 任务名
* @param projectId 项目id
*/
async searchPrevTask(val) {
try {
const params = { name: val, projectId: this.projectId };
const data = await this.$u.api.queryTaskOfProject(params);
this.allTasks = data;
return data;
} catch (error) {
console.error('error: ', error);
}
},
//
handleChange(data) {
console.log('data', data);
this.processTaskId = data.detailId;
},
//
clearAllTasks() {
this.allTasks = [];
},
//
addDeliverInput() {
if (this.deliverSort[this.deliverSort.length - 1].name) {
this.deliverSort.push({ name: '' });
}
},
//
async setParameters() {
const {
projectId,
task,
name,
startTime,
endTime,
hasRole,
roleIdList,
roleId,
description,
processTaskId,
checkerIdList,
isGlobal,
} = this;
if (!name) {
this.$t.ui.showToast('请输入名称');
return;
}
if ((!roleIdList || !roleIdList.length) && !hasRole) {
this.$t.ui.showToast('请选择负责人');
return;
}
if (!checkerIdList || !checkerIdList.length) {
this.$t.ui.showToast('请选择检查人');
return;
}
const deliverList = [];
this.deliverSort.forEach(item => {
if (item.name) {
deliverList.push(item.name);
}
});
const params = {
name,
startTime: startTime ? this.$moment(startTime).format('x') - 0 : '',
endTime: endTime ? this.$moment(endTime).format('x') - 0 : '',
roleIdList: hasRole ? [roleId] : roleIdList,
description,
projectId,
parentTaskId: task && task.id ? task.id : '', //
processTaskId, // TODO
checkerIdList,
global: isGlobal ? 1 : 0,
deliverList,
};
await this.handleSubmit(params);
},
/**
* 新建任务
* @param name 任务名
* @param startTime 开始时间
* @param endTime 结束时间
* @param roleIdList 负责人id数组
* @param description 描述
* @param projectId 所属项目id
* @param parentTaskId 所属任务id
* @param processTaskId 上道工序任务id
* @param checkerIdList 检查人id数组
* @param global 是否日常任务 0 1
* @param deliverList 交付物名字数组
*/
async handleSubmit(params) {
try {
const data = await this.$u.api.saveTask(params);
// TODO or
this.$emit('closeMask');
const newTasks = {
data: data[0],
processTaskId: params.processTaskId,
};
// store
//
if (!this.task || !this.task.id) {
this.addNewTasks(newTasks);
}
} catch (error) {
this.$emit('closeMask');
console.error('error: ', error);
}
},
// tasks
addNewTasks(data) {
const oldTasks = this.$u.deepClone(this.tasks);
let res = data.data;
console.log('添加任务后更新tasks', data);
//
if (data.processTaskId) {
const index = oldTasks.find(item => item.detailId === data.processTaskId);
if (index) {
oldTasks.splice(index + 1, 0, res);
}
} else {
this.setAddPosition(res, oldTasks);
}
},
//
setAddPosition(res, oldTasks) {
console.log('设置添加位置', res, oldTasks);
if (res.planStart - 0 < oldTasks[0].planStart - 0) {
//
oldTasks.splice(0, 0, res);
} else if (res.planStart - 0 === oldTasks[0].planStart - 0) {
//
oldTasks.splice(1, 0, res);
} else if (res.planStart - 0 >= oldTasks[oldTasks.length - 1].planStart - 0) {
//
oldTasks.splice(-1, 0, res);
} else {
//
for (let i = 0; i < oldTasks.length; i++) {
const item = oldTasks[i];
if (res.planStart - 0 > item.planStart - 0) {
if (res.planStart - 0 <= oldTasks[i + 1].planStart - 0) {
oldTasks.splice(i + 1, 0, res);
// console.log('res: ', res);
// return;
}
}
}
}
// TODO:
console.log('oldTasks: ', oldTasks);
this.updateTasks([...oldTasks]);
const params = { roleId: this.roleId, projectId: this.projectId };
this.getPermanent(params);
},
},
};
</script>
<style lang="scss" scoped>
.form {
display: flex;
flex-direction: column;
width: 100%;
max-height: 400px;
overflow-y: scroll;
}
.drop-item {
border-bottom: 1px solid #f1f1f1;
padding: 16rpx;
}
::v-deep.u-input--border {
border: none;
border-radius: 0;
}
::v-deep.u-dropdown__menu__item > uni-view {
border: none !important;
padding: 5px;
}
.u-input {
border-bottom: 1px solid #dcdfe6;
}
::v-deep input,
::v-deep textarea {
font-size: 14px;
}
.new-projects-box {
padding: 15px;
width: 100%;
font-size: 14px;
overflow: hidden;
}
.w {
width: 300px;
height: 39px;
}
::v-deep .u-dropdown__menu__item .u-flex {
border: 0 !important;
border-bottom: 1px solid #dcdfe6 !important;
padding: 0 20rpx;
}
::v-deep .u-size-medium {
width: auto;
font-size: 13px;
height: 35px;
line-height: 35px;
padding: 0 40px;
}
</style>

210
src/components/Title/components/ShareProject.vue

@ -0,0 +1,210 @@
<template>
<view class="flex justify-center">
<view class="content p-3 pb-8">
<view class="mb-3 font-bold text-base flex justify-center">创建分享链接</view>
<view class="flex flex-col">
<view class="mb-1">用户以什么角色加入项目</view>
<!-- 下拉多选 -->
<view class="uni-list">
<view class="uni-list-cell">
<view class="uni-list-cell-db ml-2" v-if="rolesArray.length">
<picker @change="changeRole" :value="index" :range="rolesArray">
<view class="uni-input">{{ allRolesName[index].name }}</view>
</picker>
</view>
</view>
</view>
<!-- 复制链接 -->
<view class="link flex items-center mt-4">
<view class="link-url">{{ links }}</view>
<u-button
style="border-radius: 0; height: 100%"
type="primary"
v-clipboard:copy="copyText"
v-clipboard:success="copySuccess"
v-clipboard:error="copyError"
>
复制链接
</u-button>
</view>
<view @click="select">
<!-- 全选按钮 -->
<!-- <view class="flex mt-4">
<view>
<u-checkbox-group>
<u-checkbox v-model="checked" @change="checkedAll"></u-checkbox>
</u-checkbox-group>
</view>
<view>已选择({{ this.quantity }})</view>
<view style="color: #f37378; margin-left: 20px">批量删除</view>
</view> -->
<!-- 多选框 -->
<!-- <view>
<u-checkbox-group class="checkboxs flex flex-1 items-center mt-4" v-for="(item, index) in list" :key="index">
<div class="flex-1 flex items-center">
<u-checkbox v-model="item.checked"></u-checkbox>
<u-avatar :src="item.src" size="55" style="background: #d8dce0; margin-right: 10px"></u-avatar>
<div style="width: 60%; font-size: 12px">
<div style="color: gray">{{ item.name }}</div>
<div style="color: #c4d0e1">{{ item.joinMethod }}</div>
</div>
</div>
</u-checkbox-group>
</view> -->
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
export default {
data() {
return {
rolesArray: [],
allRolesName: [],
index: 0,
links: '', //
copyText: '',
checked: false, //
roleName: '观众',
//
list: [
{
name: '冯老师',
src: '',
joinMethod: '文件创建者',
role: '观众',
checked: false,
disabled: false,
},
{
name: '马壮',
src: '',
joinMethod: '通过链接加入',
role: '干系人',
checked: false,
disabled: false,
},
{
name: '张野',
src: '',
joinMethod: '通过链接加入',
role: '观众',
checked: false,
disabled: false,
},
],
quantity: 0, //
path: '',
};
},
computed: {
...mapState('role', ['visibleRoles', 'invisibleRoles']),
...mapState('project', ['project']),
...mapGetters('project', ['projectId']),
},
mounted() {
this.$nextTick(() => {
this.path = window.location.href.split('?')[0];
const { path, projectId } = this;
const params = { path: `${path}`, projectId, roleId: '0' };
this.creatShare(params);
});
if (this.visibleRoles.length || this.invisibleRoles.length) {
const arr = this.visibleRoles.concat(this.invisibleRoles);
arr.forEach(role => {
let item = { id: '', name: '' };
item.id = role.id;
item.name = role.name;
this.allRolesName.push(item);
this.rolesArray.push(role.name);
});
const firstItem = { id: '0', name: '观众' };
this.allRolesName.unshift(firstItem);
this.rolesArray.unshift('观众');
}
},
methods: {
//
async changeRole(e) {
this.index = e.target.value;
this.roleName = this.allRolesName[this.index].name;
const { path, projectId } = this;
const params = { path, projectId, roleId: this.allRolesName[this.index].id };
await this.creatShare(params);
},
//
copySuccess() {
this.$t.ui.showToast('复制成功');
},
//
copyError() {
this.$t.ui.showToast('复制失败,请稍后重试');
},
/**
* 创建分享链接
* @param path 路径前缀
* @param projectId 项目id
* @param roleId 角色id
*/
async creatShare(params) {
try {
const data = await this.$u.api.createShare(params);
this.links = data.path;
this.copyText = `邀请您加入${this.project.name}的项目,角色为${this.roleName},链接为${data.path}&url=${this.$t.domain}`;
} catch (error) {
console.error('error: ', error);
}
},
//
select() {
this.quantity = 0;
this.list.forEach(val => {
if (val.checked == true) {
this.quantity++;
}
});
},
//
checkedAll() {
this.list.map(val => {
val.checked = !this.checked;
});
},
},
};
</script>
<style lang="scss" scoped>
.content {
width: 100%;
max-height: 400px;
}
.link {
height: 40px;
border: 1px solid #afbed1;
}
.link-url {
color: #afbed1;
width: 80%;
line-height: 40px;
margin-left: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

9
src/components/Upload/Upload.vue

@ -1,6 +1,6 @@
<template>
<view class="upload">
<u-icon name="plus" size="24px" class="flex justify-center w-12 h-12 bg-blue-100 rounded-full shadow-md" @click="handleUpload"></u-icon>
<u-icon name="plus" size="24px" class="btn flex justify-center w-12 h-12 rounded-full shadow-md" @click="handleUpload"></u-icon>
</view>
</template>
@ -41,6 +41,11 @@ export default {
right: 10px;
bottom: 0;
transform: translate3d(0, 50%, 0);
color: $uni-color-primary !important;
color: $uni-color-btn !important;
.btn {
background: $uni-color-btn;
color: $uni-text-color-inverse;
}
}
</style>

143
src/components/login/login.vue

@ -0,0 +1,143 @@
<template>
<view class="wrap flex justify-center items-center">
<view class="wrap-con">
<h1 class="text-3xl text-center mb-10">{{ project && project.id ? project.name : '时物链条' }}</h1>
<u-form :model="form" ref="uForm" :errorType="errorType">
<!-- 手机号 -->
<u-form-item :rightIconStyle="{ color: '#888', fontSize: '32rpx' }" prop="account" label-width="160" class="bg-white">
<u-input type="text" placeholder="账户" :border="border" v-model="form.account" />
</u-form-item>
<view class="mt-1 text-sm text-red-500" v-if="showAccountTips">{{ accountTips }}</view>
<u-form-item :rightIconStyle="{ color: '#888', fontSize: '32rpx' }" prop="password" label-width="160" class="bg-white mt-6">
<u-input type="password" placeholder="密码" :border="border" v-model="form.password" />
</u-form-item>
<view class="mt-1 text-sm text-red-500" v-if="showPasswordTips">{{ passwordTips }}</view>
</u-form>
<view class="flex justify-between my-8 text-sm">
<u-checkbox-group>
<u-checkbox v-model="checked"><text class="pl-2">自动登录</text></u-checkbox>
</u-checkbox-group>
<text class="text-blue cursor-pointer" @click="$emit('changeShow')">修改密码</text>
</view>
<!-- TODO: 报错 -->
<u-button @click="submit" type="primary" class="btn"> </u-button>
</view>
</view>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
data() {
return {
border: true,
form: {
account: '', //
password: '', //
},
accountTips: '',
passwordTips: '',
showAccountTips: false,
showPasswordTips: false,
checked: false,
errorType: ['message'],
};
},
computed: mapState('project', ['project']),
methods: {
...mapMutations('user', ['setToken', 'setUser', 'setFirstInter']),
async submit() {
//
if (this.verifyAccount() && this.verifyPassword()) {
try {
const { account, password } = this.form;
const params = {
client: this.$t.user.clients['h5'],
type: this.$t.user.types['username'],
data: { identifier: account, credential: password },
};
const data = await this.$u.api.signin(params);
if (data && data.token) {
this.setUser(data);
this.setToken(data.token);
this.noPhone(data.phone);
this.setFirstInter(true);
this.$t.ui.showToast('登录成功');
} else {
this.$t.ui.showToast('返回数据异常');
}
} catch (error) {
console.error('error: ', error);
this.$t.ui.showToast(error || '登录失败');
}
}
},
//
verifyAccount() {
if (!this.form.account) {
this.showAccountTips = true;
this.accountTips = '请输入账号';
return false;
} else {
this.showAccountTips = false;
return true;
}
},
//
verifyPassword() {
if (!this.form.password) {
this.showPasswordTips = true;
this.passwordTips = '请输入密码';
return false;
}
const reg = /^[a-zA-Z0-9._-]{6,20}$/;
if (!reg.test(this.form.password)) {
this.passwordTips = '请输入6-20位字母、数字、汉字或字符"_ - ."';
return false;
}
this.showPasswordTips = false;
return true;
},
/**
* 没有手机号 跳转绑定手机号的界面
* @param {string} phone
*/
async noPhone(phone) {
if (!phone) {
this.$u.route('/pages/phone-bind/phone-bind');
}
},
},
};
</script>
<style lang="scss" scoped>
.wrap {
width: 100%;
height: 100%;
}
.wrap-con {
width: 400px;
}
.text-blue {
color: #2979ff;
}
.btn {
line-height: 35px;
font-size: 1rem;
}
</style>

22
src/components/uni-popup/message.js

@ -0,0 +1,22 @@
export default {
created() {
if (this.type === 'message') {
// 不显示遮罩
this.maskShow = false;
// 获取子组件对象
this.childrenMsg = null;
}
},
methods: {
customOpen() {
if (this.childrenMsg) {
this.childrenMsg.open();
}
},
customClose() {
if (this.childrenMsg) {
this.childrenMsg.close();
}
},
},
};

23
src/components/uni-popup/popup.js

@ -0,0 +1,23 @@
import message from './message.js';
// 定义 type 类型:弹出类型:top/bottom/center
const config = {
// 顶部弹出
top: 'top',
// 底部弹出
bottom: 'bottom',
// 居中弹出
center: 'center',
// 消息提示
message: 'top',
// 对话框
dialog: 'center',
// 分享
share: 'bottom',
};
export default {
data() {
return { config: config };
},
mixins: [message],
};

246
src/components/uni-popup/uni-popup-dialog.vue

@ -0,0 +1,246 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__' + dialogType]">{{ title }}</text>
</view>
<view class="uni-dialog-content">
<text class="uni-dialog-content-text" v-if="mode === 'base'">{{ content }}</text>
<input v-else class="uni-dialog-input" v-model="val" type="text" :placeholder="placeholder" :focus="focus" />
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" @click="close">
<text class="uni-dialog-button-text">取消</text>
</view>
<view class="uni-dialog-button uni-border-left" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">确定</text>
</view>
</view>
</view>
</template>
<script>
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式
* @value base 基础对话框
* @value input 可输入对话框
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: 'uniPopupDialog',
props: {
value: {
type: [String, Number],
default: '',
},
placeholder: {
type: [String, Number],
default: '请输入内容',
},
/**
* 对话框主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'error',
},
/**
* 对话框模式 base/input
*/
mode: {
type: String,
default: 'base',
},
/**
* 对话框标题
*/
title: {
type: String,
default: '提示',
},
/**
* 对话框内容
*/
content: {
type: String,
default: '',
},
/**
* 拦截取消事件 如果拦截取消事件必须监听close事件执行 done()
*/
beforeClose: {
type: Boolean,
default: false,
},
},
data() {
return {
dialogType: 'error',
focus: false,
val: '',
};
},
inject: ['popup'],
watch: {
type(val) {
this.dialogType = val;
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info';
}
},
value(val) {
this.val = val;
},
},
created() {
//
this.popup.mkclick = false;
if (this.mode === 'input') {
this.dialogType = 'info';
this.val = this.value;
} else {
this.dialogType = this.type;
}
},
mounted() {
this.focus = true;
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
this.$emit(
'confirm',
() => {
this.popup.close();
if (this.mode === 'input') this.val = this.value;
},
this.mode === 'input' ? this.val : '',
);
},
/**
* 点击取消按钮
*/
close() {
if (this.beforeClose) {
this.$emit('close', () => {
this.popup.close();
});
return;
}
this.popup.close();
},
},
};
</script>
<style lang="scss" scoped>
.uni-popup-dialog {
width: 300px;
border-radius: 15px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 15px;
padding-bottom: 5px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 5px 15px 15px 15px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6e6e6e;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 14px;
}
.uni-button-color {
color: $uni-color-primary;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
}
.uni-popup__success {
color: $uni-color-success;
}
.uni-popup__warn {
color: $uni-color-warning;
}
.uni-popup__error {
color: $uni-color-error;
}
.uni-popup__info {
color: #909399;
}
</style>

115
src/components/uni-popup/uni-popup-message.vue

@ -0,0 +1,115 @@
<template>
<view class="uni-popup-message" :class="'uni-popup__' + [type]">
<text class="uni-popup-message-text" :class="'uni-popup__' + [type] + '-text'">{{ message }}</text>
</view>
</template>
<script>
/**
* PopUp 弹出层-消息提示
* @description 弹出层-消息提示
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} message 消息提示文字
* @property {String} duration 显示时间设置为 0 则不会自动关闭
*/
export default {
name: 'UniPopupMessage',
props: {
/**
* 主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'success',
},
/**
* 消息文字
*/
message: {
type: String,
default: '',
},
/**
* 显示时间设置为 0 则不会自动关闭
*/
duration: {
type: Number,
default: 3000,
},
},
inject: ['popup'],
data() {
return {};
},
created() {
this.popup.childrenMsg = this;
},
methods: {
open() {
if (this.duration === 0) return;
clearTimeout(this.popuptimer);
this.popuptimer = setTimeout(() => {
this.popup.close();
}, this.duration);
},
close() {
clearTimeout(this.popuptimer);
},
},
};
</script>
<style lang="scss" scoped>
.uni-popup-message {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
background-color: #e1f3d8;
padding: 10px 15px;
border-color: #eee;
border-style: solid;
border-width: 1px;
}
.uni-popup-message-text {
font-size: 14px;
padding: 0;
}
.uni-popup__success {
background-color: #e1f3d8;
}
.uni-popup__success-text {
color: #67c23a;
}
.uni-popup__warn {
background-color: #faecd8;
}
.uni-popup__warn-text {
color: #e6a23c;
}
.uni-popup__error {
background-color: #fde2e2;
}
.uni-popup__error-text {
color: #f56c6c;
}
.uni-popup__info {
background-color: #f2f6fc;
}
.uni-popup__info-text {
color: #909399;
}
</style>

171
src/components/uni-popup/uni-popup-share.vue

@ -0,0 +1,171 @@
<template>
<view class="uni-popup-share">
<view class="uni-share-title">
<text class="uni-share-title-text">{{ title }}</text>
</view>
<view class="uni-share-content">
<view class="uni-share-content-box">
<view class="uni-share-content-item" v-for="(item, index) in bottomData" :key="index" @click.stop="select(item, index)">
<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
<text class="uni-share-text">{{ item.text }}</text>
</view>
</view>
</view>
<view class="uni-share-button-box">
<button class="uni-share-button" @click="close">取消</button>
</view>
</view>
</template>
<script>
export default {
name: 'UniPopupShare',
props: {
title: {
type: String,
default: '分享到',
},
},
inject: ['popup'],
data() {
return {
bottomData: [
{
text: '微信',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/grid-2.png',
name: 'wx',
},
{
text: '支付宝',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/grid-8.png',
name: 'wx',
},
{
text: 'QQ',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/gird-3.png',
name: 'qq',
},
{
text: '新浪',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/grid-1.png',
name: 'sina',
},
{
text: '百度',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/grid-7.png',
name: 'copy',
},
{
text: '其他',
icon: 'https://img-cdn-qiniu.dcloud.net.cn/uni-ui/grid-5.png',
name: 'more',
},
],
};
},
created() {},
methods: {
/**
* 选择内容
*/
select(item, index) {
this.$emit(
'select',
{
item,
index,
},
() => {
this.popup.close();
},
);
},
/**
* 关闭窗口
*/
close() {
this.popup.close();
},
},
};
</script>
<style lang="scss" scoped>
.uni-popup-share {
background-color: #fff;
}
.uni-share-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
}
.uni-share-title-text {
font-size: 14px;
color: #666;
}
.uni-share-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 10px;
}
.uni-share-content-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: wrap;
width: 360px;
}
.uni-share-content-item {
width: 90px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
padding: 10px 0;
align-items: center;
}
.uni-share-content-item:active {
background-color: #f5f5f5;
}
.uni-share-image {
width: 30px;
height: 30px;
}
.uni-share-text {
margin-top: 10px;
font-size: 14px;
color: #3b4144;
}
.uni-share-button-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 10px 15px;
}
.uni-share-button {
flex: 1;
border-radius: 50px;
color: #666;
font-size: 16px;
}
.uni-share-button::after {
border-radius: 50px;
}
</style>

289
src/components/uni-popup/uni-popup.vue

@ -0,0 +1,289 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle]" @touchmove.stop.prevent="clear">
<uni-transition v-if="maskShow" :mode-class="['fade']" :styles="maskClass" :duration="duration" :show="showTrans" @click="onTap" />
<uni-transition :mode-class="ani" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
<view class="uni-popup__wrapper-box" @click.stop="clear">
<slot />
</view>
</uni-transition>
</view>
</template>
<script>
import uniTransition from '../uni-transition/uni-transition.vue';
import popup from './popup.js';
/**
* PopUp 弹出层
* @description 弹出层组件为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [ture|false] 是否开启动画
* @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
* @event {Function} change 打开关闭弹窗触发e={show: false}
*/
export default {
name: 'UniPopup',
components: { uniTransition },
props: {
//
animation: {
type: Boolean,
default: true,
},
// top: bottomcenter
// message: ; dialog :
type: {
type: String,
default: 'center',
},
// maskClick
maskClick: {
type: Boolean,
default: true,
},
},
provide() {
return { popup: this };
},
mixins: [popup],
watch: {
/**
* 监听type类型
*/
type: {
handler: function (newVal) {
this[this.config[newVal]]();
},
immediate: true,
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick(val) {
this.mkclick = val;
},
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
},
transClass: {
position: 'fixed',
left: 0,
right: 0,
},
maskShow: true,
mkclick: true,
popupstyle: 'top',
};
},
created() {
this.mkclick = this.maskClick;
if (this.animation) {
this.duration = 300;
} else {
this.duration = 0;
}
},
methods: {
clear(e) {
// TODO nvue
e.stopPropagation();
},
open() {
this.showPopup = true;
this.$nextTick(() => {
new Promise(resolve => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.showTrans = true;
// fixed by mehaotian app
this.$nextTick(() => {
resolve();
});
}, 50);
}).then(res => {
console.log('res: ', res);
//
clearTimeout(this.msgtimer);
this.msgtimer = setTimeout(() => {
this.customOpen && this.customOpen();
}, 100);
this.$emit('change', {
show: true,
type: this.type,
});
});
});
},
close(type) {
this.showTrans = false;
this.$nextTick(() => {
this.$emit('change', {
show: false,
type,
});
clearTimeout(this.timer);
//
this.customOpen && this.customClose();
this.timer = setTimeout(() => {
this.showPopup = false;
}, 300);
});
},
onTap() {
if (!this.mkclick) return;
this.close();
},
/**
* 顶部弹出样式处理
*/
top() {
this.popupstyle = 'top';
this.ani = ['slide-top'];
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
};
},
/**
* 底部弹出样式处理
*/
bottom() {
this.popupstyle = 'bottom';
this.ani = ['slide-bottom'];
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
};
},
/**
* 中间弹出样式处理
*/
center() {
this.popupstyle = 'center';
this.ani = ['zoom-out', 'fade'];
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center',
};
},
},
};
</script>
<style lang="scss" scoped>
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-popup__mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: $uni-bg-color-mask;
opacity: 0;
}
.mask-ani {
transition-property: opacity;
transition-duration: 0.2s;
}
.uni-top-mask {
opacity: 1;
}
.uni-bottom-mask {
opacity: 1;
}
.uni-center-mask {
opacity: 1;
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
}
.top {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.bottom {
bottom: 0;
}
.uni-popup__wrapper-box {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
}
.content-ani {
// transition: transform 0.3s;
transition-property: transform, opacity;
transition-duration: 0.2s;
}
.uni-top-content {
transform: translateY(0);
}
.uni-bottom-content {
transform: translateY(0);
}
.uni-center-content {
transform: scale(1);
opacity: 1;
}
</style>

276
src/components/uni-transition/uni-transition.vue

@ -0,0 +1,276 @@
<template>
<view
v-if="isShow"
ref="ani"
class="uni-transition"
:class="[ani.in]"
:style="'transform:' + transform + ';' + stylesObject"
@click="change"
>
<slot></slot>
</view>
</template>
<script>
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation');
// #endif
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式 css 样式注意带-连接符的属性需要使用小驼峰写法如`backgroundColor:red`
*/
export default {
name: 'uniTransition',
props: {
show: {
type: Boolean,
default: false,
},
modeClass: {
type: Array,
default() {
return [];
},
},
duration: {
type: Number,
default: 300,
},
styles: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
isShow: false,
transform: '',
ani: { in: '', active: '' },
};
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open();
} else {
this.close();
}
},
immediate: true,
},
},
computed: {
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's',
};
let transfrom = '';
for (let i in styles) {
let line = this.toLine(i);
transfrom += line + ':' + styles[i] + ';';
}
return transfrom;
},
},
created() {
// this.timer = null
// this.nextTick = (time = 50) => new Promise(resolve => {
// clearTimeout(this.timer)
// this.timer = setTimeout(resolve, time)
// return this.timer
// });
},
methods: {
change() {
this.$emit('click', { detail: this.isShow });
},
open() {
clearTimeout(this.timer);
this.isShow = true;
this.transform = '';
this.ani.in = '';
for (let i in this.getTranfrom(false)) {
if (i === 'opacity') {
this.ani.in = 'fade-in';
} else {
this.transform += `${this.getTranfrom(false)[i]} `;
}
}
this.$nextTick(() => {
setTimeout(() => {
this._animation(true);
}, 50);
});
},
close() {
clearTimeout(this.timer);
this._animation(false);
},
_animation(type) {
let styles = this.getTranfrom(type);
// #ifdef APP-NVUE
if (!this.$refs['ani']) return;
animation.transition(
this.$refs['ani'].ref,
{
styles,
duration: this.duration, //ms
timingFunction: 'ease',
needLayout: false,
delay: 0, //ms
},
() => {
if (!type) {
this.isShow = false;
}
this.$emit('change', { detail: this.isShow });
},
);
// #endif
// #ifndef APP-NVUE
this.transform = '';
for (let i in styles) {
if (i === 'opacity') {
this.ani.in = `fade-${type ? 'out' : 'in'}`;
} else {
this.transform += `${styles[i]} `;
}
}
this.timer = setTimeout(() => {
if (!type) {
this.isShow = false;
}
this.$emit('change', { detail: this.isShow });
}, this.duration);
// #endif
},
getTranfrom(type) {
let styles = { transform: '' };
this.modeClass.forEach(mode => {
switch (mode) {
case 'fade':
styles.opacity = type ? 1 : 0;
break;
case 'slide-top':
styles.transform += `translateY(${type ? '0' : '-100%'}) `;
break;
case 'slide-right':
styles.transform += `translateX(${type ? '0' : '100%'}) `;
break;
case 'slide-bottom':
styles.transform += `translateY(${type ? '0' : '100%'}) `;
break;
case 'slide-left':
styles.transform += `translateX(${type ? '0' : '-100%'}) `;
break;
case 'zoom-in':
styles.transform += `scale(${type ? 1 : 0.8}) `;
break;
case 'zoom-out':
styles.transform += `scale(${type ? 1 : 1.2}) `;
break;
}
});
return styles;
},
_modeClassArr(type) {
let mode = this.modeClass;
if (typeof mode !== 'string') {
let modestr = '';
mode.forEach(item => {
modestr += item + '-' + type + ',';
});
return modestr.substr(0, modestr.length - 1);
} else {
return mode + '-' + type;
}
},
// getEl(el) {
// console.log(el || el.ref || null);
// return el || el.ref || null
// },
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
},
},
};
</script>
<style>
.uni-transition {
transition-timing-function: ease;
transition-duration: 0.3s;
transition-property: transform, opacity;
}
.fade-in {
opacity: 0;
}
.fade-active {
opacity: 1;
}
.slide-top-in {
/* transition-property: transform, opacity; */
transform: translateY(-100%);
}
.slide-top-active {
transform: translateY(0);
/* opacity: 1; */
}
.slide-right-in {
transform: translateX(100%);
}
.slide-right-active {
transform: translateX(0);
}
.slide-bottom-in {
transform: translateY(100%);
}
.slide-bottom-active {
transform: translateY(0);
}
.slide-left-in {
transform: translateX(-100%);
}
.slide-left-active {
transform: translateX(0);
opacity: 1;
}
.zoom-in-in {
transform: scale(0.8);
}
.zoom-out-active {
transform: scale(1);
}
.zoom-out-in {
transform: scale(1.2);
}
</style>

3
src/config/db.js

@ -0,0 +1,3 @@
export const db = null; // indexedDB 对象
export const name = 'TALL_indexedDB'; // indexDB name
export const version = 1; // indexDB version

97
src/config/plugin.js

@ -0,0 +1,97 @@
// 定义插件相关信息
/* eslint-disable */
export default {
defaults: [
{
id: 1,
name: 'TASK_NAME',
description: '任务名插件',
component: 'p-task-title',
},
{
id: 2,
name: 'TASK_DESCRIPTION',
description: '任务描述插件',
component: 'p-task-description',
},
{
id: 3,
name: 'TASK_DURATION_DELAY',
description: '任务时长延迟插件(+-1min)时间格式可设置',
component: 'p-task-duration-delay',
},
{
id: 4,
name: 'TASK_START_TIME_DELAY',
description: '任务开始时间延迟插件(+-1hour)',
component: 'p-task-start-time-delay',
},
{
id: 5,
name: 'DELIVERABLE',
description: '交付物插件(人 + 交付物)可配置【仅人】 or 【仅交付物】 or 【人+交付物】',
component: 'p-deliverable',
},
{
id: 6,
name: 'SUBTASKS',
description: '子任务插件:显示子任务',
component: 'p-subtasks',
},
{
id: 7,
name: 'SUB_PROJECT',
description: '子项目插件:显示子项目',
component: 'p-sub-project',
},
{
id: 8,
name: 'TASK_COUNTDOWN',
description: '任务倒计时插件',
component: 'p-task-countdown',
},
{
id: 9,
name: 'MANAGE_PROJECT',
description: '项目信息管理插件',
component: 'p-manage-project',
},
{
id: 10,
name: 'MANAGE_ROLE',
description: '角色信息管理插件',
component: 'p-manage-role',
},
{
id: 11,
name: 'MANAGE_MEMBER',
description: '成员信息管理插件',
component: 'p-manage-member',
},
{
id: 12,
name: 'MANAGE_TASK',
description: '任务信息管理插件',
component: 'p-manage-task',
},
{
id: 13,
name: 'WBS_IMPORT',
description: '导入WBS新建项目',
component: 'p-wbs-import',
},
{
id: 14,
name: 'WBS_IMPORT_UPDATE',
description: '导入WBS更新项目',
component: 'p-wbs-update',
},
{
id: 15,
name: 'DELIVER_CHECK',
description: '交付物检查',
component: 'p-deliver-check',
},
], // 默认插件id列表
};

2
src/config/task.js

@ -0,0 +1,2 @@
// 每页加载颗粒度的个数
export default { pageCount: 10 };

17
src/config/time.js

@ -0,0 +1,17 @@
export default {
timeUnits: [
// 时间颗粒度
{ id: 0, value: '毫秒', format: 'x', cycle: 'YY-M-D HH:mm:ss', granularity: 'millisecond' },
{ id: 1, value: '秒', format: 'x', cycle: 'YY-M-D HH:mm:ss', granularity: 'second' },
{ id: 2, value: '分', format: 'ss', cycle: 'YY-M-D HH:mm', granularity: 'minute' },
{ id: 3, value: '时', format: 'mm', cycle: 'YY-M-D HH时', granularity: 'hour' },
{ id: 4, value: '天', format: 'D日 HH:mm', cycle: 'YY-M-D', granularity: 'day' },
{ id: 5, value: '周', format: 'D日 HH:mm', cycle: '', granularity: 'week' },
{ id: 6, value: '月', format: 'D日 H:m', cycle: 'YYYY年', granularity: 'month' },
{ id: 7, value: '季度', format: '', cycle: 'YYYY年', granularity: 'quarter' },
{ id: 8, value: '年', format: 'YYYY', cycle: '', granularity: 'year' },
{ id: 9, value: '年代', format: '', cycle: '', granularity: '' },
{ id: 10, value: '世纪', format: '', cycle: '', granularity: '' },
{ id: 11, value: '千年', format: '', cycle: '', granularity: '' },
],
};

11
src/main.js

@ -1,10 +1,15 @@
import App from './App';
import Tall from '@/utils/tall';
import Vue from 'vue';
import VueClipboard from 'vue-clipboard2';
import dayjs from 'dayjs';
import plugin from '@/apis/plugin.js';
import project from '@/apis/project.js';
import request from '@/utils/request.js';
import role from '@/apis/role.js';
import store from './store';
import tall from '@/apis/tall.js';
import task from '@/apis/task.js';
import uView from 'uview-ui';
import wbs from '@/apis/wbs.js';
@ -18,6 +23,8 @@ import wbs from '@/apis/wbs.js';
// Vue.use(indexedDB);
//#endif
Vue.use(VueClipboard);
Vue.config.productionTip = false;
Vue.prototype.$moment = dayjs;
Vue.use(uView);
@ -33,6 +40,10 @@ const app = new Vue({ ...App, store });
Vue.use(request, app);
Vue.use(tall, app);
Vue.use(project, app);
Vue.use(task, app);
Vue.use(plugin, app);
Vue.use(role, app);
Vue.use(wbs, app);
app.$mount();

2
src/manifest.json

@ -66,7 +66,7 @@
"h5": {
"router": {
"base": "/tall/v3.1.0"
"base": "/tall/v3.2.0"
},
"title": "时物链条",
"sdkConfigs": {

21
src/pages.json

@ -3,22 +3,37 @@
{
"path": "pages/index/index",
"style": {
"navigationBarText": "TALL"
"navigationBarText": "TALL",
"navigationStyle": "custom"
}
},
{
"path": "pages/phone-bind/phone-bind",
"style": {
"navigationBarTitleText": "绑定手机号"
"navigationBarTitleText": "绑定手机号",
"navigationStyle": "custom"
}
},
{
"path": "pages/project-webview/project-webview",
"style": {
"navigationBarTitleText": "项目详情页"
"navigationBarTitleText": "项目详情页",
"navigationStyle": "custom"
}
}
],
"topWindow": {
"path": "pages/top-window/top-window.vue",
"style": {
"height": "48px"
}
},
"rightWindow": {
"path": "pages/right-window/right-window",
"style": {
"width": "calc(100vw - 300px)"
}
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "TALL",

18
src/pages/index/index.vue

@ -1,5 +1,5 @@
<template>
<view class="flex flex-col h-full bg-gray-50" @click="openAuth">
<view class="main-box flex flex-col h-full" @click="openAuth">
<view class="relative" @touchmove="onMove">
<!-- 日历 -->
<Calendar @selected-change="onDateChange" :show-back="true" ref="calendar" @handleFindPoint="handleFindPoint" />
@ -10,7 +10,7 @@
</view>
<!-- 项目列表 -->
<Projects @getProjects="getProjects" class="flex-1 overflow-y-auto" />
<Projects ref="projects" @getProjects="getProjects" class="flex-1 overflow-y-auto" />
<!-- 全局提示框 -->
<u-top-tips ref="uTips"></u-top-tips>
@ -32,7 +32,7 @@ export default {
};
},
computed: mapState('user', ['token', 'user']),
computed: mapState('user', ['token', 'user', 'firstInter']),
watch: {
token(value) {
@ -46,6 +46,7 @@ export default {
if (!this.token) return;
this.getProjects();
this.handleFindPoint();
this.setTaskDetailUrl('');
},
onReady() {
@ -53,7 +54,7 @@ export default {
},
methods: {
...mapMutations('project', ['setProjects', 'setDotList']),
...mapMutations('project', ['setProjects', 'setDotList', 'setTaskDetailUrl']),
//
getProjects(start = this.$moment().startOf('day').valueOf(), end = this.$moment().endOf('day').valueOf()) {
@ -66,6 +67,11 @@ export default {
item.show = false;
});
this.setProjects(data);
if (this.firstInter) {
setTimeout(() => {
this.$refs.projects.openProject();
}, 1000);
}
}
});
},
@ -134,4 +140,8 @@ page {
height: 100%;
overflow: hidden;
}
.main-box {
background: #eeeeee;
}
</style>

144
src/pages/left-window/left-window.vue

@ -0,0 +1,144 @@
<template>
<view class="project-list flex flex-col h-full" @click="openAuth">
<view class="relative" @touchmove="onMove">
<!-- 日历 -->
<Calendar @selected-change="onDateChange" :show-back="true" ref="calendar" @handleFindPoint="handleFindPoint" />
<!-- 上传 导入wbs -->
<Upload @success="onUploadSuccess" @error="onUploadError" />
<!-- #ifdef H5 -->
<!-- #endif -->
</view>
<!-- 项目列表 -->
<Projects @getProjects="getProjects" class="flex-1 overflow-y-auto" />
<!-- 全局提示框 -->
<u-top-tips ref="uTips"></u-top-tips>
</view>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import UserAuthMixin from '@/mixins/userAuth';
let prevY = 0;
export default {
mixins: [UserAuthMixin],
data() {
return {
calendar: null,
days: [],
};
},
computed: mapState('user', ['token', 'user']),
watch: {
token(value) {
if (!value) return;
this.getProjects();
this.handleFindPoint();
},
},
onShow() {
if (!this.token) return;
this.getProjects();
this.handleFindPoint();
this.setTaskDetailUrl('');
},
onReady() {
this.calendar = this.$refs.calendar;
},
methods: {
...mapMutations('project', ['setProjects', 'setDotList', 'setTaskDetailUrl']),
//
getProjects(start = this.$moment().startOf('day').valueOf(), end = this.$moment().endOf('day').valueOf()) {
// const data = await this.$u.api.getProjects(start, end);
this.$t.$q.getProjects(start, end, (err, data) => {
if (err) {
console.error('err: ', err);
} else {
data.forEach(item => {
item.show = false;
});
this.setProjects(data);
}
});
},
/**
* 查询小红点
* @param { string } endTime 结束时间
* @param { string } startTime 开始时间
*/
async handleFindPoint(start, end) {
try {
const startTime = start || this.$moment().startOf('month').valueOf();
const endTime = end || this.$moment().endOf('month').valueOf();
const data = await this.$u.api.findRedPoint(startTime, endTime);
this.setDotList(data);
} catch (error) {
console.log('error: ', error);
}
},
//
onDateChange(event) {
const day = this.$moment(event.fullDate);
const start = day.startOf('date').valueOf();
const end = day.endOf('date').valueOf();
this.getProjects(start, end);
},
// /
onMove(event) {
const y = event.changedTouches[0].pageY;
if (y - prevY > 0) {
// weekMode=true weekMode=false
this.calendar.weekMode && (this.calendar.weekMode = false);
} else if (y - prevY < 0) {
// weekMode=false weekMode=true
!this.calendar.weekMode && (this.calendar.weekMode = true);
}
prevY = y;
this.calendar.initDate();
},
//
onUploadSuccess() {
this.$refs.uTips.show({
title: '导入成功,即将打开新项目',
type: 'success',
duration: '3000',
});
},
//
onUploadError(error) {
this.$refs.uTips.show({
title: error || '导入失败',
type: 'error',
duration: '6000',
});
},
},
};
</script>
<style lang="scss" scoped>
page {
height: 100%;
overflow: hidden;
}
.project-list {
border-right: 1px solid #cccccc;
width: 375px;
background: #eee;
}
</style>

60
src/pages/phone-bind/phone-bind.vue

@ -23,6 +23,26 @@
<u-verification-code :seconds="seconds" ref="uCode" @change="codeChange"></u-verification-code>
<u-top-tips ref="uTips"></u-top-tips>
<u-popup v-model="bindingPhone" mode="center" closeable class="p-4 ml-7 mr-7" border-radius="14">
<view class="p-4 pt-10"> 该手机号已经注册过账号是否将账号合并? </view>
<div class="flex justify-between pb-3 mt-3">
<u-button size="mini" @click="noMerge()">取消</u-button>
<u-button type="primary" size="mini" @click="yesMerge()">确定</u-button>
</div>
</u-popup>
<u-popup v-model="mergePop" mode="center" closeable class="p-4 ml-7 mr-7" border-radius="14">
<view class="pt-10 p-4">
<p>1您可以选择合并账号完成手机号的绑定系统会为您自动合并两个账号的数据信息</p>
<br />
<p>2如果选择不合并已注册手机号的已有数据可能会被清空</p>
<br />
<p class="text-red-500">注意合并账号可能会带来不确定的数据丢失</p>
</view>
<div class="flex justify-between mt-3 pb-3">
<u-button size="mini" @click="isMerge(1)">取消合并</u-button>
<u-button type="primary" size="mini" @click="isMerge(0)">确定合并</u-button>
</div>
</u-popup>
</view>
</template>
@ -73,6 +93,10 @@ export default {
seconds: 120, //
codeTips: '',
errorType: ['message'],
//
bindingPhone: false,
//
mergePop: false,
};
},
@ -94,11 +118,15 @@ export default {
});
setTimeout(() => uni.navigateBack(), 2000);
} catch (error) {
this.$refs.uTips.show({
/* this.$refs.uTips.show({
title: error.msg || '手机号绑定失败',
type: 'error',
duration: '3000',
});
}); */
if (error.code === 75) {
this.bindingPhone = true;
}
console.log(error.code);
}
} else {
console.log('验证失败');
@ -170,6 +198,34 @@ export default {
}
return true;
},
//
noMerge() {
this.bindingPhone = false;
},
//
yesMerge() {
this.mergePop = true;
},
//
async isMerge(merge) {
try {
const data = await this.$u.api.phoneMerge(this.model.phone, merge);
console.log(data);
this.$refs.uTips.show({
title: '手机号合并成功, 即将跳转上一页',
type: 'success',
duration: '3000',
});
setTimeout(() => uni.navigateBack(), 2000);
} catch (error) {
this.$refs.uTips.show({
title: error.msg || '手机号合并失败',
type: 'error',
duration: '3000',
});
}
this.mergePop = false;
},
},
};
</script>

54
src/pages/project-webview/project-webview.vue

@ -3,16 +3,44 @@
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
data() {
return { src: '' };
return {
src: '',
urlOptions: '',
};
},
onLoad(options) {
console.log('options: ', options);
uni.$on('updateDetail', e => {
options = e;
});
if (options.p) {
this.setIsShowTimeBox(true);
} else {
this.setIsShowTimeBox(false);
}
if (!options) {
this.$t.ui.showModal('缺少参数, 请返回重试');
} else {
this.serUrl(options);
}
this.getPostMessage();
},
computed: mapState('user', ['isPC']),
methods: {
...mapMutations('project', ['setTaskDetailUrl']),
...mapMutations('user', ['setIsShowTimeBox']),
serUrl(options) {
this.urlOptions = options;
const { p, u, pname, url } = options;
if (pname) {
uni.setNavigationBarTitle({ title: pname });
@ -29,7 +57,27 @@ export default {
const baseUrl = process.env.VUE_APP_PROJECT_PATH;
this.src = `${baseUrl}/${this.$t.app.version}/#/?u=${u}&p=${p}&url=${url}&pname=${pname}`;
}
},
//
getPostMessage() {
const that = this;
window.addEventListener(
'message',
function (e) {
if (e.data.event === 'openDetail') {
if (that.isPC) {
that.setTaskDetailUrl(e.data.data);
} else {
const { p, u, pname, url } = that.urlOptions;
that.src = `${e.data.data}/${that.$t.app.version}/#/?u=${u}&p=${p}&url=${url}&pname=${pname}`;
}
}
},
false,
);
},
},
};
</script>

78
src/pages/right-window/right-window.vue

@ -0,0 +1,78 @@
<template>
<view class="h-full detail-con">
<!-- 没有项目id -->
<view class="null-project" v-if="!projectId"></view>
<!-- 有项目id -->
<view class="have-project flex" v-if="projectId">
<view class="time-box">
<ProjectDetail />
</view>
<view class="container">2222222</view>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
show: false,
src: '',
projectId: '',
};
},
created() {
let projectInfo = this.$t.storage.getStorageSync('projectInfo');
let project_info = JSON.parse(projectInfo);
if (project_info.id) {
this.projectId = project_info.id;
}
uni.$on('updateDetail', e => {
this.projectId = e.id;
});
},
computed: {
...mapState('project', ['project', 'taskDetailUrl']),
...mapState('user', ['user', 'token', 'isShowTimeBox']),
},
watch: {
taskDetailUrl(val) {
if (val && val.split('?')[0] === 'change-password') {
this.show = true;
} else {
this.src = val;
this.show = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.detail-con {
background: #eeeeee;
}
.have-project {
width: 100%;
height: 100%;
}
.time-box {
width: 400px;
height: 100%;
background: #fff;
border-right: 1px solid #cccccc;
}
.container {
width: calc(100% - 400px);
}
</style>

44
src/pages/top-window/top-window.vue

@ -0,0 +1,44 @@
<template>
<view class="w-full h-full bg-black flex justify-between items-center">
<view class="project-name">{{ project && project.id ? project.name : '时物链条' }}</view>
<view class="user-box flex items-center">
<u-avatar :src="user && user.wxInfo && user.wxInfo.headImgUrl ? user.wxInfo.headImgUrl : src" :size="56"></u-avatar>
<text class="user-name ml-3 mr-5 text-sm">{{ user && user.account ? user.account : '未登录' }}</text>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return { src: '../../../public/img/avatar.jpg' };
},
computed: {
...mapState('project', ['project']),
...mapState('user', ['user']),
},
onLoad(options) {
console.log('options: ', options);
},
};
</script>
<style lang="scss" scoped>
.project-name {
font-size: 16px;
color: $uni-text-color;
padding-left: 16px;
}
.user-box {
cursor: pointer;
}
.user-name {
color: $uni-text-color;
}
</style>

3
src/store/db/actions.js

@ -0,0 +1,3 @@
const actions = {};
export default actions;

3
src/store/db/getters.js

@ -0,0 +1,3 @@
const getters = {};
export default getters;

12
src/store/db/index.js

@ -0,0 +1,12 @@
import state from './state';
import getters from './getters';
import mutations from './mutations';
import actions from './actions';
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};

3
src/store/db/mutations.js

@ -0,0 +1,3 @@
const mutations = {};
export default mutations;

7
src/store/db/state.js

@ -0,0 +1,7 @@
const state = {
db: null, // indexedDB对象
name: 'TALL_indexedDB',
version: 1,
};
export default state;

4
src/store/index.js

@ -2,7 +2,9 @@ import Vue from 'vue';
import Vuex from 'vuex';
import messages from './messages/index';
import project from './project/index';
import role from './role/index';
import socket from './socket/index';
import task from './task/index';
import user from './user/index';
// 不属于具体模块的 应用级的 store内容
@ -46,4 +48,4 @@ const mutations = {
};
Vue.use(Vuex);
export default new Vuex.Store({ state, getters, mutations, modules: { user, messages, socket, project } });
export default new Vuex.Store({ state, getters, mutations, modules: { user, messages, socket, project, role, task } });

9
src/store/project/mutations.js

@ -57,6 +57,15 @@ const mutations = {
setDotList(state, data) {
state.dotList = data;
},
/**
* 设置任务详情页路径
* @param { object } state
* @param { string } data
*/
setTaskDetailUrl(state, data) {
state.taskDetailUrl = data;
},
};
export default mutations;

1
src/store/project/state.js

@ -3,6 +3,7 @@ const state = {
project: { name: '加载中...' }, // 当前项目信息
projects: [], // 项目列表
dotList: [], // 小红点
taskDetailUrl: '', // 任务详情页路径
};
export default state;

17
src/store/role/actions.js

@ -0,0 +1,17 @@
const actions = {
/**
* 根据项目id查找所有成员信息
* @param {*} commit
* @param {object} params
*/
async getAllMembers({ commit }, params) {
try {
const data = await uni.$u.api.queryChecker(params);
commit('setMembers', data);
} catch (error) {
uni.$t.ui.showToast(error.msg || '成员查询失败');
}
},
};
export default actions;

13
src/store/role/getters.js

@ -0,0 +1,13 @@
const getters = {
// 是不是负责人
isMine({ roleId, invisibleRoles, visibleRoles }) {
if (!visibleRoles || !visibleRoles.length) return false;
const visible = visibleRoles.find(visible => visible.id === roleId);
if (visible) return visible.mine;
const invisible = invisibleRoles.find(invisible => invisible.id === roleId);
if (invisible) return visible.mine;
return false;
},
};
export default getters;

12
src/store/role/index.js

@ -0,0 +1,12 @@
import state from './state';
import getters from './getters';
import mutations from './mutations';
import actions from './actions';
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};

39
src/store/role/mutations.js

@ -0,0 +1,39 @@
const mutations = {
/**
* 设置不展示的角色信息
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setInvisibleRoles(state, data) {
state.invisibleRoles = data || [];
},
/**
* 设置展示的角色信息
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setVisibleRoles(state, data) {
state.visibleRoles = data || [];
},
/**
* 设置当前角色信息
* @param {Object} state
* @param {string} roleId 当前正在展示的角色的id
*/
setRoleId(state, roleId) {
state.roleId = roleId;
},
/**
* 设置项目下所有成员信息
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setMembers(state, data) {
state.members = data || [];
},
};
export default mutations;

8
src/store/role/state.js

@ -0,0 +1,8 @@
const state = {
invisibleRoles: [], // 不展示的角色信息
visibleRoles: [], // 展示的角色信息
roleId: '', // 当前展示查看的角色id
members: [], // 项目下所有成员
};
export default state;

33
src/store/task/actions.js

@ -0,0 +1,33 @@
const actions = {
/**
* 根据角色查找永久的日常任务
* @param {*} commit
* @param {string} roleId 角色id
*/
getPermanent({ commit }, param) {
uni.$t.$q.getPermanent(param, (err, data) => {
if (err) {
console.error('err: ', err);
} else {
commit('setPermanents', data);
}
});
},
/**
* 根据时间和角色查找日常任务
* @param {*} commit
* @param {object} param 请求参数 roleId, timeNode, timeUnit
*/
getGlobal({ commit }, param) {
uni.$t.$q.getGlobal(param, (err, data) => {
if (err) {
console.error('err: ', err);
} else {
commit('setDailyTasks', data);
}
});
},
};
export default actions;

23
src/store/task/getters.js

@ -0,0 +1,23 @@
const getters = {
// 所有的日常任务 永久 + 可变 日常任务
globals({ dailyTasks, permanents }) {
return [...permanents, ...dailyTasks];
},
unitConfig({ timeUnit }) {
const target = uni.$t.timeConfig.timeUnits.find(item => item.id === timeUnit);
return target;
},
// 计算任务开始时间的格式
startTimeFormat(state, { unitConfig }) {
return unitConfig.format || 'D日 HH:mm';
},
// 计算颗粒度 对应的 dayjs add 的单位
timeGranularity(state, { unitConfig }) {
return unitConfig.granularity;
},
};
export default getters;

12
src/store/task/index.js

@ -0,0 +1,12 @@
import state from './state';
import getters from './getters';
import mutations from './mutations';
import actions from './actions';
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};

238
src/store/task/mutations.js

@ -0,0 +1,238 @@
const mutations = {
/**
* 记录时间轴向上滚动的距离
* @param { object } state
* @param { number } num
*/
setScrollTop(state, num) {
state.scrollTop = num;
},
/**
* 记录时间轴向上滚动的距离
* @param { object } state
* @param {string} taskId
*/
setScrollToTaskId(state, taskId) {
state.scrollToTaskId = taskId;
},
/**
* 设置日常任务当前是否应该处于收缩状态
* @param { object } state
* @param { boolean } data
*/
setShrink(state, data) {
state.isShrink = data;
},
/**
* 设置tip的值
* @param {object} state
* @param {object} data
*/
setTip(state, data) {
if (!data) return;
state.tip = { ...data };
},
/**
* 是否显示tips
* @param { object } state
* @param { boolean } show
*/
setTipShow(state, show) {
state.tip.show = show;
},
/**
* 是否显示tips
* @param { object } state
* @param { number } status
*/
setStatus(state, status) {
state.tip.status = status;
},
/**
* 设置时间基准点
* @param { object } state
* @param { number } data
*/
setTimeNode(state, data) {
state.timeNode = data;
},
/**
* 设置时间颗粒度
* @param { object } state
* @param { number } data
*/
setTimeUnit(state, data) {
state.timeUnit = data;
},
/**
* 设置向上查到的定期任务数据
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setUpTasks(state, data) {
if (!state.tasks.length) {
state.tasks = [...data]; // 原来没有数据
} else {
state.tasks = [...data, ...state.tasks];
let arr = [],
flag = false;
state.tasks.forEach(task => {
arr.forEach(item => {
if (task.id == item.id) {
flag = true;
}
});
if (!flag) {
arr.push(task);
}
});
state.tasks = [...arr];
// state.tasks = [...data.concat(state.tasks)];
}
},
/**
* 设置向下查到的定期任务数据
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setDownTasks(state, data) {
if (!state.tasks && !state.tasks.length) {
state.tasks = [...data];
} else {
state.tasks = [...state.tasks, ...data];
let arr = [],
flag = false;
state.tasks.forEach(task => {
arr.forEach(item => {
if (task.id == item.id) {
flag = true;
}
});
if (!flag) {
arr.push(task);
}
});
state.tasks = [...arr];
// state.tasks = [...state.tasks.concat(data)];
}
},
/**
* 添加任务后更新tasks
* @param {Object} state
* @param {Array} data 新添加的task
*/
updateTasks(state, data) {
state.tasks = [...data];
},
/**
* 设置添加任务的位置
* @param {*} state
* @param {*} data
*/
setAddPosition(state, data) {
console.log('data: ', data);
},
/**
* 设置日常任务数据
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setDailyTasks(state, data) {
state.dailyTasks = data || [];
},
/**
* 设置永久固定任务
* @param {object} state
* @param {array} tasks 服务端查询到的永久日常任务书籍
*/
setPermanents(state, tasks) {
state.permanents = tasks || [];
},
/**
* 设置时间轴是否继续向上查任务
* @param {Object} state
* @param {Boolean} show
*/
setTopEnd(state, show) {
state.topEnd = show;
},
/**
* 设置时间轴是否继续向下查任务
* @param {Object} state
* @param {Boolean} show
*/
setBottomEnd(state, show) {
state.bottomEnd = show;
},
// 清空标志位 如切换角色等使用
clearEndFlag(state) {
state.topEnd = false;
state.bottomEnd = false;
},
// 清空定期任务
clearTasks(state) {
state.tasks = [];
},
/**
* 收到消息设置任务状态
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setTaskStatus(state, data) {
const item = state.tasks.find(i => i.id === data.id);
item.process = data.taskStatus;
},
/**
* 收到打开新项目消息状态
* @param {Object} state
* @param {Array} data 服务端返回的模板数组
*/
setNewProjectInfo(state, data) {
state.newProjectInfo = data;
},
/**
* 设置骨架屏是否显示
* @param {Object} state
* @param {Boolean} show
*/
setShowSkeleton(state, show) {
state.showSkeleton = show;
},
/**
* 是否设置时间轴自动滚动的位置
* @param {Object} state
* @param {Boolean} show
*/
setShowScrollTo(state, show) {
state.showScrollTo = show;
},
};
export default mutations;

25
src/store/task/state.js

@ -0,0 +1,25 @@
const state = {
scrollTop: 0,
scrollToTaskId: '', // 时间轴自动滚动的位置
isShrink: false, // true: 收起, false:展开
tip: {
taskId: '', // 当前正在修改状态的任务的id
show: false,
status: 0, // 所点击任务的当前状态码
text: '',
left: 0, // 鼠标点击位置距离左边的距离
top: 0, // 鼠标点击位置距离上边的距离
},
timeNode: new Date().getTime(), // 时间基准点
timeUnit: 4, // 时间颗粒度
topEnd: false, // 时间轴向上查任务到顶了
bottomEnd: false, // 时间轴向下查任务到底了
permanents: [], // 永久日常任务
dailyTasks: [], // 日常任务
tasks: [], // 所有的定期任务
showSkeleton: false, // 定期任务骨架屏
newProjectInfo: {},
showScrollTo: false, // 是否可以设置时间轴自动滚动的位置
};
export default state;

18
src/store/user/mutations.js

@ -19,6 +19,24 @@ const mutations = {
state.user = { ...user };
uni.$t.storage.setStorageSync('user', JSON.stringify(user));
},
/**
* 设置是不是PC端
* @param {object} state
* @param {boolean} show 项目列表
*/
setIsPC(state, show) {
state.isPC = show;
},
/**
* 设置是否打开了时间轴
* @param {object} state
* @param {boolean} show /
*/
setIsShowTimeBox(state, show) {
state.isShowTimeBox = show;
},
};
export default mutations;

2
src/store/user/state.js

@ -1,5 +1,7 @@
const state = {
token: '',
user: null,
isPC: false, // 是不是PC端
isShowTimeBox: false, // 是否打开了时间轴
};
export default state;

1
src/uni.scss

@ -14,6 +14,7 @@
@import 'uview-ui/theme.scss';
@import './common/styles/tailwind.scss';
/* 颜色变量 */ /* 行为相关颜色 */
$uni-color-btn: #1890ff;
$uni-color-primary: #0284c7;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;

163
src/utils/indexedDB.js

@ -0,0 +1,163 @@
import { name } from '@/config/db';
import { curry } from 'lodash';
// 创建表
const createCollection = (Vue, db) => {
// projects项目表
!db.objectStoreNames.contains('projects') && db.createObjectStore('projects', { keyPath: 'id' });
// roles 角色表
!db.objectStoreNames.contains('roles') && db.createObjectStore('roles', { keyPath: 'id' });
// plan_tasks 定期任务
!db.objectStoreNames.contains('plan_tasks') && db.createObjectStore('plan_tasks', { keyPath: 'id' });
// fixed_tasks 固定全局任务
Vue.prototype.$db.fixed_tasks = !db.objectStoreNames.contains('fixed_tasks') && db.createObjectStore('fixed_tasks', { keyPath: 'id' });
// variable_tasks 可变全局任务
Vue.prototype.$db.variable_tasks =
!db.objectStoreNames.contains('variable_tasks') && db.createObjectStore('variable_tasks', { keyPath: 'id' });
// plugins 插件表
Vue.prototype.$db.plugins = !db.objectStoreNames.contains('plugins') && db.createObjectStore('plugins', { keyPath: 'id' });
};
/**
* 新增数据
*
* @param {object} db 数据库database
* @param {string} collection 集合/
* @param {object} data 数据
*/
const create = (db, collection, data) => {
return new Promise((resolve, reject) => {
const request = db.transaction([collection], 'readwrite').objectStore(collection).add(data);
request.onsuccess = () => resolve();
request.onerror = event => {
const { name, message } = event.target.error;
if (name === 'ConstraintError') {
reject('数据已存在');
} else {
reject(message);
}
};
});
};
/**
* 找到1条数据
*
* @param {object} db 数据库database
* @param {string} collection 集合/
* @param {string} key 索引关键字 一般是id
*/
const findOne = (db, collection, key) => {
return new Promise((resolve, reject) => {
const request = db.transaction([collection]).objectStore(collection).get(key);
request.onerror = event => reject(event.target.error.message);
request.onsuccess = event => resolve(event.target.result);
});
};
/**
* 找到所有数据
*
* @param {object} db 数据库database
* @param {string} collection 集合/
*/
const find = (db, collection) => {
return new Promise((resolve, reject) => {
const request = db.transaction(collection).objectStore(collection).openCursor();
let result = [];
request.onerror = event => reject(event.target.error.message);
request.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
result.push(cursor.value);
cursor.continue();
} else {
resolve(result);
}
};
});
};
/**
* 更新数据
*
* @param {object} db 数据库database
* @param {string} collection 集合/
* @param {object} newData 新数据
*/
const update = (db, collection, newData) => {
return new Promise((resolve, reject) => {
const request = db.transaction([collection], 'readwrite').objectStore(collection).put(newData);
request.onerror = event => reject(event.target.error.message);
request.onsuccess = () => resolve(newData);
});
};
/**
* 移除数据 通过关键字
*
* @param {object} db 数据库database
* @param {string} collection 集合/
* @param {string} key 关键字
*/
const remove = (db, collection, key) => {
return new Promise((resolve, reject) => {
const request = db.transaction([collection], 'readwrite').objectStore(collection).delete(key);
request.onerror = event => reject(event.target.error.message);
request.onsuccess = () => resolve();
});
};
/**
* 创建索引
*
* @param {object} db 数据库database
* @param {string} collection 集合/
* @param {string} field 创建索引的字段名称
* @param {string} key 关键字
*/
const createIndexAndFind = (db, collection, field, key) => {
return new Promise((resolve, reject) => {
const store = db.transaction([collection], 'readonly').objectStore(collection);
store.createIndex(field, field);
const index = store.index(field);
const request = index.get(key);
request.onerror = event => reject(event.target.error.message);
request.onsuccess = event => resolve(event.target.result);
});
};
const curriedCreate = curry(create);
export const curriedFindOne = curry(findOne);
export const curriedFind = curry(find);
export const curriedRemove = curry(remove);
export const curriedUpdate = curry(update);
export const curriedIndex = curry(createIndexAndFind);
const install = Vue => {
uni.$db = Vue.prototype.$db = {};
Vue.prototype.$db.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
const request = Vue.prototype.$db.indexedDB.open(name, Date.now()); // IDBRequest 对象
request.onerror = error => console.error('打开数据库失败', error);
request.onsuccess = event => {
console.log('INDEXED_DB OPEN SUCCESS');
Vue.prototype.$db.db = event.target.result;
};
request.onupgradeneeded = event => {
console.log('INDEXED_DB OPEN onupgradeneeded');
Vue.prototype.$db.db = event.target.result;
// 创建表
createCollection(Vue, Vue.prototype.$db.db);
Vue.prototype.$db.create = curriedCreate(Vue.prototype.$db.db); // create 新增数据,颗粒化以后就不用再传db数据了
Vue.prototype.$db.findOne = curriedFindOne(Vue.prototype.$db.db); // 查一条
Vue.prototype.$db.find = curriedFind(Vue.prototype.$db.db); // 查集合里的所有数据
Vue.prototype.$db.update = curriedUpdate(Vue.prototype.$db.db); // 更新某条数据
Vue.prototype.$db.remove = curriedRemove(Vue.prototype.$db.db); // 删除某条数据
// Vue.prototype.$db.createIndex = curriedIndex(Vue.prototype.$db.db); // 创建索引
};
};
export default { install };

53
src/utils/task.js

@ -0,0 +1,53 @@
import dayjs from 'dayjs';
/**
* 设置时间轴空数据
* @param {number} startTime
* @param {boolean} isUp true 向上加载,false 向下加载
* @param {string} timeGranularity 颗粒度
* @param {number} pageCount 加载的颗粒度数量 默认值是10
*/
export const setPlaceholderTasks = (startTime, isUp, timeGranularity, pageCount) => {
let result = [];
pageCount = pageCount || uni.$t.task.pageCount;
for (let i = 0; i < pageCount; i++) {
const delta = isUp ? `-${i + 1}` - 0 : i + 1;
let item = {
id: uni.$u.guid(20, false, 10),
panel: {},
plugins: [],
process: 4,
planStart: uni.$moment(startTime).add(delta, timeGranularity).valueOf(),
};
// console.log('isup: ', isUp, 'result:', new Date(item.planStart).toLocaleDateString());
isUp ? result.unshift(item) : result.push(item);
}
return result;
};
/**
* 超出旧数据上下限 补齐时间刻度到新数据的起始时间颗粒度
* @param {object} option
* @param {array} option.tasks 旧的已有的任务书籍
* @param {array} option.data 新拿到的任务数据 空值已经过滤过了
* @param {string} option.timeGranularity 颗粒度
*/
export const computeFillPlaceholderTaskCount = ({ tasks, data, timeGranularity }) => {
const result = { prev: 0, next: 0 };
// 新数据的开始时间 < 旧数据的开始时间
// 超出了上限 补上限的时间刻度
// 补上 新数据开始时间 到 旧数据开始时间 的刻度
if (+data[0].planStart < +tasks[0].planStart) {
// 找出来需要补几组颗粒度
result.prev = dayjs(+tasks[0].planStart).diff(+data[0].planStart, timeGranularity) + 1;
}
// 新数据的结束时间 > 旧数据的结束时间
// 超出了下线 补下限的时间刻度
// 补上 旧数据截止时间 到 新数据截止时间 的刻度
if (+data[data.length - 1].planStart > +tasks[tasks.length - 1].planStart) {
result.next = dayjs(+data[data.length - 1].planStart).diff(+tasks[tasks.length - 1].planStart, timeGranularity) + 1;
}
return result;
};
Loading…
Cancel
Save