Compare commits

...

61 Commits

Author SHA1 Message Date
wally a54d509c61 Merge pull request 'feat' (#3) from feat into develop 4 years ago
wally 342ac74628 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally 8cddc7b3a3 ci: drone 4 years ago
xuesinan 8f455da94c feat: 手机号登录 4 years ago
xuesinan 050b12a130 fix: 删除多余的引入 4 years ago
xuesinan 5374cdbb6d fix: 解决冲突 4 years ago
xuesinan 0486e98e9b refactor: 项目列表 4 years ago
wally d25f2a7ec8 ci: 测试ci' 4 years ago
wally 6ab95f8a81 ci: 测试ci 4 years ago
wally 63ec5a3091 ci: 更新drone.yml 4 years ago
wally dfa3ff8c30 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally a57d598b57 ci: 更新drone.yml 4 years ago
xuesinan e475a5150c fix: 解决冲突 4 years ago
xuesinan 88cf48d86d feat: 项目列表新 4 years ago
song 53c6b90990 fix: 插件接口修改 4 years ago
song dcb0079843 fix: 解决warning 4 years ago
song 31fed9bc61 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
song 98abdf6ae5 fix: 时间轴任务 4 years ago
xuesinan 565585b07a feat: 手机号登录 4 years ago
xuesinan 78aa50761f fix: defineExpose, defineEmits不需要引入 4 years ago
xuesinan 902caccfec fix: defineExpose, defineEmits不需要引入 4 years ago
wally b32599987c Merge pull request 'feat' (#2) from feat into develop 4 years ago
wally f5b52e355d ci: 修改.drone.yml 4 years ago
wally e6c3b92d4d Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally 9fbae8997f ci: 添加drone.yml 4 years ago
xuesinan 5f64204e03 feat: 表单验证 4 years ago
xuesinan 8f3bc1ea25 feat: 表单验证 4 years ago
song a88c9331ce fix: 解决冲突 4 years ago
song 8b1b380a6b feat: 时间轴展示 4 years ago
xuesinan 95fdeac461 feat: 用户名密码登录 4 years ago
xuesinan ebf456e47c feat: 账户名密码登录 4 years ago
wally 91757586d3 feat(theme): theme demo 4 years ago
wally b20d3f0300 fix(createTask): 修复createTask v-model的问题 4 years ago
wally b3f16ff1e2 feat(project): 日常任务面板添加 4 years ago
wally db256d1c62 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally 9120d541a0 fix(app.vue): 修复获取token报错的问题 4 years ago
xuesinan 1c785547b1 fix: 修复一些内容 4 years ago
xuesinan 3cdb1ce6c2 fix: 修复一些内容 4 years ago
wally 0b05767d92 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally bdd5f87c89 style: 细节调整 4 years ago
song 72dad2bed2 feat: 添加 timeline 4 years ago
wally 1792e1fe39 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
wally 2457a87c5c refactor: project init 重构 4 years ago
song a95d00559b feat: 时间轴接口 4 years ago
wally c7bf2df562 refactor: 重构project init 部分 4 years ago
song e926b751fb feat: 时间轴页面 4 years ago
xuesinan 3beb05e3cd feat: 项目操作面板 4 years ago
xuesinan 30c42d3cc3 feat: 解决冲突 4 years ago
xuesinan a52e6d5a4d feat: 项目列表 4 years ago
song 1a47783aa4 feat: 添加上传按钮 4 years ago
song 0dd443b3c1 perf: 更新代码 4 years ago
wally db9602b85f style: calender格式及细节调整 4 years ago
song 219202a254 Merge branch 'feat' of ssh://101.201.226.163:50022/TALL/TALL-MUI-4 into feat 4 years ago
song 1b46a91b6e feat: 日历页添加 4 years ago
wally 0c08089256 chore: editorconfig update 4 years ago
song 94e4a7d644 feat: 添加日历 4 years ago
song 392c8ccbc0 feat: 更新代码 4 years ago
xuesinan 561c8e6639 feat: 日历页首页 4 years ago
xuesinan 970cf9a364 feat: app.vue 4 years ago
xuesinan 1b3efd8026 feat: 使用uview完成api请求 4 years ago
xuesinan 12ed2ad63c feat: vue3 4 years ago
  1. 149
      .drone.yml
  2. 4
      .editorconfig
  3. 56
      .eslintrc.js
  4. 4
      .hbuilderx/launch.json
  5. 195
      App.vue
  6. 84
      CHANGELOG.md
  7. 21
      apis/plugin.js
  8. 17
      apis/project.js
  9. 12
      apis/role.js
  10. 34
      apis/tall.js
  11. 20
      apis/task.js
  12. 7
      apis/wbs.js
  13. BIN
      common/img/weixinIcon.png
  14. 15
      common/js/config.js
  15. 48
      common/js/dc-clipboard/clipboard.js
  16. 4496
      common/styles/tailwind.scss
  17. 16
      common/styles/theme/default.scss
  18. 5
      common/styles/theme/index.scss
  19. 11
      common/styles/theme/test.scss
  20. 451
      components/Calendar/Calendar.vue
  21. 136
      components/Calendar/generateDates.js
  22. 89
      components/ChooseChecker/ChooseChecker.vue
  23. 81
      components/Globals/Globals.vue
  24. 59
      components/Plugin/Plugin.vue
  25. 480
      components/PrettyExchange/PrettyExchange.vue
  26. 134
      components/Projects/ProjectItem.vue
  27. 57
      components/Projects/Projects.vue
  28. 104
      components/Render/Render.vue
  29. 232
      components/Roles/Roles.vue
  30. 173
      components/Skeleton/Skeleton.vue
  31. 13
      components/Theme/Theme.vue
  32. 126
      components/TimeLine/TimeLine.vue
  33. 38
      components/TimeLine/component/Barrier.vue
  34. 177
      components/TimeLine/component/TaskTools.vue
  35. 130
      components/TimeLine/component/TimeBox.vue
  36. 317
      components/TimeLine/component/TimeStatus.vue
  37. 0
      components/TimeLine/component/Title.vue
  38. 82
      components/Tips/Tips.vue
  39. 223
      components/Title/Title.vue
  40. 466
      components/Title/components/CreateTask copy.vue
  41. 583
      components/Title/components/CreateTask.vue
  42. 210
      components/Title/components/ShareProject.vue
  43. 46
      components/Upload/Upload.vue
  44. 22
      components/uni-popup/message.js
  45. 23
      components/uni-popup/popup.js
  46. 246
      components/uni-popup/uni-popup-dialog.vue
  47. 115
      components/uni-popup/uni-popup-message.vue
  48. 171
      components/uni-popup/uni-popup-share.vue
  49. 289
      components/uni-popup/uni-popup.vue
  50. 97
      config/plugin.js
  51. 2
      config/task.js
  52. 17
      config/time.js
  53. 194
      hooks/project/useGetTasks.js
  54. 122
      hooks/project/useInit.js
  55. 7
      hooks/theme/useTheme.js
  56. 37
      hooks/user/useGetToken.js
  57. 10
      hooks/user/useGetUserIdFromLocal.js
  58. 285
      hooks/user/userMixin - 副本.js
  59. 231
      hooks/user/userMixin.js
  60. 76
      main.js
  61. 40
      manifest.json
  62. 115
      package.json
  63. 35
      pages.json
  64. 179
      pages/index/index.vue
  65. 142
      pages/project/project.vue
  66. 99
      pages/user/accountLogin.vue
  67. 8
      pages/user/forgetPassword.vue
  68. 130
      pages/user/login.vue
  69. 8
      pages/user/rigister.vue
  70. 62
      plugins/p-deliver-check/p-deliver-check.vue
  71. 9
      plugins/p-deliver/p-deliver.vue
  72. 131
      plugins/p-delivery-history/p-delivery-history.vue
  73. 149
      plugins/p-delivery-history/p-delivery-history1.vue
  74. 6
      plugins/p-manage-member/p-manage-member.vue
  75. 6
      plugins/p-manage-project/p-manage-project.vue
  76. 6
      plugins/p-manage-role/p-manage-role.vue
  77. 6
      plugins/p-manage-task/p-manage-task.vue
  78. 54
      plugins/p-subproject/p-subproject.vue
  79. 31
      plugins/p-subtasks/p-subtasks.vue
  80. 10
      plugins/p-task-countdown/p-task-countdown.vue
  81. 9
      plugins/p-task-description/p-task-description.vue
  82. 23
      plugins/p-task-duration-delay/p-task-duration-delay.vue
  83. 18
      plugins/p-task-start-time-delay/p-task-start-time-delay.vue
  84. 10
      plugins/p-task-title/p-task-title.vue
  85. 90
      plugins/p-upload-deliverable/p-upload-deliverable.vue
  86. 71
      plugins/p-wbs-import/p-wbs-import.vue
  87. 3
      store/db/actions.js
  88. 3
      store/db/getters.js
  89. 12
      store/db/index.js
  90. 3
      store/db/mutations.js
  91. 7
      store/db/state.js
  92. 42
      store/index.js
  93. 3
      store/messages/getters.js
  94. 4
      store/messages/index.js
  95. 85
      store/messages/mutations.js
  96. 8
      store/messages/state.js
  97. 1
      store/project/getters.js
  98. 4
      store/project/index.js
  99. 19
      store/project/mutations.js
  100. 2
      store/role/actions.js

149
.drone.yml

@ -0,0 +1,149 @@
---
kind: pipeline
type: docker
name: dev
# 挂载的主机卷,可以映射到docker容器中
volumes:
# maven构建缓存(宿主机目录)
- name: ssh_key
host:
path: /root/.ssh/
- name: cache
host:
path: /var/lib/cache
- name: data
host:
path: /var/lib/data
steps:
# - name: restore-cache
# image: drillster/drone-volume-cache
# volumes:
# - name: cache
# path: /cache
# settings:
# restore: true
# mount:
# - ./node_modules
- name: build
image: node:latest
pull: if-not-exists # default always
# volumes:
# - name: cache
# path: /root/.m2
commands:
- npm config set registry http://registry.npm.taobao.org
- npm i
- npm run test
# - name: rebuild-cache
# image: drillster/drone-volume-cache
# volumes:
# - name: cache
# path: /cache
# settings:
# rebuild: true
# mount:
# - ./node_modules
- name: deploy-scp
image: appleboy/drone-scp
pull: if-not-exists
volumes:
- name: ssh_key
path: /root/.ssh/
settings:
host: test.tall.wiki
port: 22
username: root
key_path: /root/.ssh/id_rsa
rm: true # true则会删除目标目录重建
target: /home/tall/v4.0.0
source: dist/**/*
strip_components: 1 # 去除的目录层数,如果没有该选项,则拷贝过去是 target/xxx.jar,1代表去除target
# - name: run-ssh
# image: appleboy/drone-ssh
# pull: if-not-exists
# volumes:
# - name: ssh_key
# path: /root/.ssh/
# settings:
# settings:
# host: test.tall.wiki
# port: 22
# username: root
# key_path: /root/.ssh/id_rsa
# script_stop: true # stop script after first failure
# #command_timeout: 30s # 30seconds, the maximum amount of time for the execute commands, default is 10 minutes.
# script:
# - cd /home/iacd-platform-drone
# - ./re.sh > /dev/null 2> /dev/null &
- name: notify-email
image: drillster/drone-email
pull: if-not-exists
settings:
host: smtp.qiye.aliyun.com #例如 smtp.qq.com
port: 465 #例如QQ邮箱端口465
username: devops@ccsens.com #邮箱用户名
password: #邮箱密码
from_secret: orgsecret_password_mail_devops
from: devops@ccsens.com
recipients: weizezhao@ccsens.com #收件人,多个用,隔开
when: #执行条件
status:
- success
- changed
- failure
- name: notify-wechatwork
image: fifsky/drone-wechat-work
pull: if-not-exists
settings:
url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=b2b93e9a-128b-41d4-8dce-12004e3f48b9
msgtype: markdown
content: |
{{if eq .Status "success" }}
#### 🎉 ${DRONE_REPO} 构建成功
> Commit: [${DRONE_COMMIT_MESSAGE}](${DRONE_COMMIT_LINK})
> Author: ${DRONE_COMMIT_AUTHOR}
> [点击查看](${DRONE_BUILD_LINK})
{{else}}
#### ❌ ${DRONE_REPO} 构建失败
> Commit: [${DRONE_COMMIT_MESSAGE}](${DRONE_COMMIT_LINK})
> Author: ${DRONE_COMMIT_AUTHOR}
> 请立即修复!!!
> [点击查看](${DRONE_BUILD_LINK})
{{end}}
when:
status:
- failure
- success
trigger:
branch: develop
# - name: notify-dingtalk
# image: lddsb/drone-dingtalk-message
# environment:
# PASSWORD:
# from_secret: password_mail_devops
# settings:
# token: your-dingtalk-robot-access-token
# type: markdown
# message_color: true
# message_pic: true
# sha_link: true
# -name: notify-slack
# image: plugins/slack
# webhook: https://hooks.slack.com/ www.dijiuyy.com services/xxx/xxx/xxx
# channel: dev
# template: >
# {{#success build.status}}
# build {{build.number}} succeeded. Good job.
# {{else}}
# build {{build.number}} failed. Fix me please.
# {{/success}}

4
.editorconfig

@ -1,8 +1,8 @@
root = true
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 140
root = true

56
.eslintrc.js

@ -1,21 +1,41 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:vue/essential",
"airbnb-base"
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:vue/essential',
'airbnb-base',
],
parserOptions: {
ecmaVersion: 13,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: [
'vue',
'@typescript-eslint',
],
rules: {
'vue/html-self-closing': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-param-reassign': 'off',
'max-len': ['error', { code: 140, tabWidth: 2 }],
'object-curly-newline': ['error', { multiline: true }],
'arrow-parens': ['error', 'as-needed'],
'linebreak-style': 'off',
'vue/attributes-order': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/html-indent': 'off',
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
"parserOptions": {
"ecmaVersion": 13,
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"vue",
"@typescript-eslint"
],
"rules": {
}
},
};

4
.hbuilderx/launch.json

@ -2,6 +2,10 @@
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [{
"app-plus" :
{
"launchtype" : "local"
},
"default" :
{
"launchtype" : "local"

195
App.vue

@ -1,36 +1,169 @@
<script>
// import {
// ref,
// computed
// } from 'vue';
// import {
// useStore
// } from 'vuex';
// const store = useStore();
// onLaunch(() => {
// // checkNetwork(); //
// });
// store
// 2g 3g ;
// function checkNetwork() {
// uni.getNetworkType({
// success: ({ networkType }) => {
// this.setNetworkConnected(!(networkType === 'none' || networkType === '2g' || networkType === '3g'));
// },
// });
// //
// uni.onNetworkStatusChange(({ isConnected, networkType }) => {
// this.setNetworkConnected(isConnected && !(networkType === '2g' || networkType === '3g'));
// });
// }
// import { mapState } from 'vuex';
export default {
// computed: {
// ...mapState(['theme']),
// },
// watch: {
// theme(newTheme) {
// console.log('newTheme: ', newTheme);
// if (!newTheme) return;
// this.loadTheme();
// },
// },
async onLaunch(options) {
// this.loadTheme();
console.log('onLaunch options: ', options);
this.checkNetwork(); //
this.getSystemInfo(); //
await this.syncLocalDataToStore(options.query.u); // localStoragestore
const token = await this.getToken();
if (!token) {
this.$ui.showToast('获取用户信息失败, 请登录');
// TODO:
return;
}
this.noPhone(this.$store.state.user.phone);
this.$store.dispatch('socket/initSocket');
},
methods: {
// loadTheme() {
// const path = this.theme.replace('-', '/');
// import(`./common/styles/${path}.scss`);
// },
async getToken() {
const { token } = this.$store.state.user;
const tokenIsAvailable = this.$store.getters['user/tokenIsAvailable'];
const userId = this.$store.getters['user/userId'];
if (token && tokenIsAvailable) {
// 1.1 storetoken 使storetoken
return token;
} else {
// 2. userIdtoken
if (userId) {
try {
const { token } = await this.$store.dispatch('user/getTokenByUserId', userId);
return token;
} catch (error) {
console.error('error: ', error);
return null;
}
} else {
return null;
}
}
},
/**
* 将localStorage里的数据同步到store里
* user, token, tokenExpiredTime
*/
syncLocalDataToStore(urlUserId) {
return new Promise((resolve, reject) => {
try {
const localUser = uni.$storage.getStorageSync('user');
const localToken = uni.$storage.getStorageSync('anyringToken');
const tokenExpiredTime = uni.$storage.getStorageSync('tokenExpiredTime');
if (!this.$store.state.user.user) {
if (localUser) {
// user
const user = JSON.parse(localUser);
if (!urlUserId || user.id === urlUserId) {
this.$store.commit('user/setUser', user);
} else {
this.$store.commit('user/setUser', { id: urlUserId });
}
} else {
this.$store.commit('user/setUser', { id: urlUserId });
}
}
if (this.$store.state.user.token && localToken) {
// token
this.$store.commit('user/setToken', localToken);
}
if (this.$store.state.user.tokenExpiredTime && tokenExpiredTime) {
// tokenExpiredTime
this.$store.commit('user/setTokenExpiredTime', +tokenExpiredTime);
}
resolve();
} catch (error) {
reject(error);
}
});
},
// store
// 2g 3g ;
checkNetwork() {
uni.getNetworkType({
success: ({ networkType }) => {
this.$store.commit('setNetworkConnected', !(networkType === 'none' || networkType === '2g' || networkType === '3g'));
},
});
//
uni.onNetworkStatusChange(({ isConnected, networkType }) => {
this.$store.commit('setNetworkConnected', isConnected && !(networkType === '2g' || networkType === '3g'));
});
},
//
getSystemInfo() {
uni.getSystemInfo({
success: result => {
this.$store.commit('setSystemInfo', result);
},
fail: error => {
console.error('getSystemInfo fail:', error);
},
});
},
//
async signin() {
try {
const data = await uni.$u.api.signin();
if (data && data.token) {
this.$store.commit('user/setUser', data);
this.$store.commit('user/setToken', data.token);
noPhone(data.phone);
} else {
uni.$ui.showToast('返回数据异常');
}
} catch (error) {
console.error('error: ', error);
uni.$ui.showToast(error || '登录失败');
}
},
/**
* 没有手机号 跳转绑定手机号的界面
* @param {string} phone
*/
async noPhone(phone) {
if (!phone) {
// TODO:
// uni.navigateTo({ url: '/pages/phone-bind/phone-bind' });
}
},
},
};
</script>
<style lang="scss">
/*每个页面公共css */
@import "@/uni_modules/vk-uview-ui/index.scss";
@import '@/common/styles/iconfont.scss';
@import '@/common/styles/app.scss';
/*每个页面公共css */
@import '@/uni_modules/vk-uview-ui/index.scss';
@import '@/common/styles/iconfont.scss';
@import '@/common/styles/app.scss';
@import '@/common/styles/tailwind.scss';
@import '@/common/styles/theme/index.scss';
page {
height: 100%;
}
</style>

84
CHANGELOG.md

@ -1,6 +1,82 @@
# 1.0.0 (2021-12-31)
# 1.0.0 (2022-01-12)
范围|描述|commitId
--|--|--
- | Initial commit | 52b8f49
### 🌟 新功能
| 范围 | 描述 | commitId |
| ------- | ------------------------ | ------------------------------------------------------------------------ |
| - | 表单验证 | [8f3bc1e](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/8f3bc1e) |
| - | 更新代码 | [392c8cc](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/392c8cc) |
| - | 日历页首页 | [561c8e6](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/561c8e6) |
| - | 日历页添加 | [1b46a91](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/1b46a91) |
| - | 时间轴接口 | [a95d005](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/a95d005) |
| - | 时间轴页面 | [e926b75](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/e926b75) |
| - | 时间轴展示 | [8b1b380](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/8b1b380) |
| - | 使用 uview 完成 api 请求 | [1b3efd8](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/1b3efd8) |
| - | 手机号登录 | [565585b](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/565585b) |
| - | 添加 timeline | [72dad2b](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/72dad2b) |
| - | 项目操作面板 | [3beb05e](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/3beb05e) |
| - | 项目列表 | [a52e6d5](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/a52e6d5) |
| - | 项目列表新 | [88cf48d](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/88cf48d) |
| - | 账户名密码登录 | [ebf456e](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/ebf456e) |
| - | app.vue | [970cf9a](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/970cf9a) |
| - | first commit | [8dc26de](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/8dc26de) |
| project | 日常任务面板添加 | [b3f16ff](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/b3f16ff) |
| theme | theme demo | [9175758](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/9175758) |
| - | vue3 | [12ed2ad](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/12ed2ad) |
### 🎨 代码样式
| 范围 | 描述 | commitId |
| ---- | ----------------------- | ------------------------------------------------------------------------ |
| - | 细节调整 | [bdd5f87](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/bdd5f87) |
| - | calender 格式及细节调整 | [db9602b](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/db9602b) |
### 🐛 Bug 修复
| 范围 | 描述 | commitId |
| ---------- | ------------------------------------ | ------------------------------------------------------------------------ |
| - | 插件接口修改 | [53c6b90](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/53c6b90) |
| - | 解决 warning | [dcb0079](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/dcb0079) |
| - | 删除多余的引入 | [050b12a](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/050b12a) |
| - | 时间轴任务 | [98abdf6](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/98abdf6) |
| - | 修复一些内容 | [3cdb1ce](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/3cdb1ce) |
| app.vue | 修复获取 token 报错的问题 | [9120d54](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/9120d54) |
| createTask | 修复 createTask v-model 的问题 | [b20d3f0](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/b20d3f0) |
| - | defineExpose, defineEmits 不需要引入 | [902cacc](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/902cacc) |
### 📦 持续集成
| 范围 | 描述 | commitId |
| ---- | -------------- | ------------------------------------------------------------------------ |
| - | 测试 ci | [6ab95f8](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/6ab95f8) |
| - | 测试 ci' | [d25f2a7](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/d25f2a7) |
| - | 更新 drone.yml | [63ec5a3](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/63ec5a3) |
| - | 更新 drone.yml | [a57d598](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/a57d598) |
| - | 测试 ci | [6ab95f8](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/6ab95f8) |
| - | 测试 ci' | [d25f2a7](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/d25f2a7) |
| - | 添加 drone.yml | [9fbae89](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/9fbae89) |
| - | 修改.drone.yml | [f5b52e3](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/f5b52e3) |
### 🔨 代码重构
| 范围 | 描述 | commitId |
| ---- | ---------------------- | ------------------------------------------------------------------------ |
| - | 项目列表 | [0486e98](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/0486e98) |
| - | 重构 project init 部分 | [c7bf2df](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/c7bf2df) |
| - | project init 重构 | [2457a87](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/2457a87) |
### 🚀 性能优化
| 范围 | 描述 | commitId |
| ---- | -------- | ------------------------------------------------------------------------ |
| - | 更新代码 | [0dd443b](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/0dd443b) |
### chore
| 范围 | 描述 | commitId |
| ---- | ------------------- | ------------------------------------------------------------------------ |
| - | editorconfig update | [0c08089](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/0c08089) |
| 范围 | 描述 | commitId |
| ---- | -------------- | ------------------------------------------------------------------------ |
| - | Initial commit | [52b8f49](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/52b8f49) |

21
apis/plugin.js

@ -0,0 +1,21 @@
import Config from '@/common/js/config.js'
const apiUrl = Config.apiUrl;
const defaultwbs = `${apiUrl}/defaultwbs`;
export function setupPlugin(app) {
uni.$u.api = { ...uni.$u.api } || {};
// 获取插件信息
uni.$u.api.getOtherPlugin = param => uni.$u.post(`${apiUrl}/pluginshop/plugin/query?pluginId=${param.pluginId}&styleType=${param.styleType}`);
// 查询子任务
uni.$u.api.findSonTask = param => uni.$u.post(`${defaultwbs}/task/findSonTask`, param);
// 查询子项目
uni.$u.api.findSonProject = param => uni.$u.post(`${defaultwbs}/project/findSonProject`, param);
// 提交交付物
uni.$u.api.saveDeliver = param => uni.$u.post(`${defaultwbs}/deliver/save`, param);
// 查询任务的交付物历史记录
uni.$u.api.queryDeliverOfTask = param => uni.$u.post(`${defaultwbs}/deliver/queryDeliverOfTask`, param);
// 检查交付物
uni.$u.api.checkDeliver = param => uni.$u.post(`${defaultwbs}/deliver/checkDeliver`, param);
};

17
apis/project.js

@ -0,0 +1,17 @@
import Config from '@/common/js/config.js'
const apiUrl = Config.apiUrl;
const defaultwbs = `${apiUrl}/defaultwbs`;
export function setupProject(app) {
uni.$u.api = { ...uni.$u.api } || {};
//根据id获取项目信息
uni.$u.api.findProjectById = param => uni.$u.post(`${defaultwbs}/project/findProjectById`, param);
//创建分享连接
uni.$u.api.createShare = param => uni.$u.post(`${defaultwbs}/share/create`, param);
//点击分享连接
uni.$u.api.clickShare = param => uni.$u.post(`${defaultwbs}/share/click`, param);
};

12
apis/role.js

@ -0,0 +1,12 @@
import Config from '@/common/js/config.js'
const apiUrl = Config.apiUrl;
const defaultwbs = `${apiUrl}/defaultwbs`;
export function setupRole(app) {
uni.$u.api = { ...uni.$u.api } || {};
//根据项目id查找角色
uni.$u.api.findShowRole = param => uni.$u.post(`${defaultwbs}/role/show`, param);
//根据项目id查找所有成员
uni.$u.api.queryChecker = param => uni.$u.post(`${defaultwbs}/deliver/queryChecker`, param);
};

34
apis/tall.js

@ -0,0 +1,34 @@
import Config from '@/common/js/config.js'
const apiUrl = Config.apiUrl;
const tall = `${apiUrl}/tall3/v3.0`;
const tall1 = `http://101.201.226.163/gateway/ptos`;
export function setupTall(app) {
uni.$u.api = { ...uni.$u.api } || {};
// 登录
// uni.$u.api.signin = params => login.index(params);
uni.$u.api.signin = params => uni.$u.http.post(`${tall}/users/signin`, params); // 登录
// 获取图片验证码
uni.$u.api.getImageCode = () => uni.$u.get(`${tall}/users/code`);
// 获取短信验证码
uni.$u.api.getSmsCode = params => uni.$u.get(`${tall}/users/smscode`, params);
// 根据userId获取token
uni.$u.api.getToken = userId => uni.$u.get(`${tall}/users/userId`, { userId });
// 绑定手机号
uni.$u.api.phoneBind = (phone, smsCode) => uni.$u.http.post(`${tall}/users/binding`, { phone, smsCode });
// 是否合并账号
uni.$u.api.phoneMerge = (phone, isMerge) => uni.$u.http.post(`${tall}/users/merge`, { phone, isMerge });
// 修改用户信息
uni.$u.api.updateUserInfo = params => uni.$u.http.post(`${tall}/users/userInfo`, params);
// 获取项目列表
uni.$u.api.getProjects = (startTime, endTime) => uni.$u.post(`${tall}/project/query`, { startTime, endTime });
// 查询日历是否有小红点
uni.$u.api.findRedPoint = (startTime, endTime) => uni.$u.post(`${tall}/project/day`, { startTime, endTime });
// 设置项目顺序
uni.$u.api.setProjectSort = params => uni.$u.post(`${tall}/project/setProjectSort`, params);
// 设置项目父子结构
uni.$u.api.setProjectRelation = params => uni.$u.post(`${tall}/project/setProjectRelation`, params);
// 删除某个项目
uni.$u.api.delProject = projectId => uni.$u.post(`${tall}/project/deleteProject`, { projectId });
}

20
apis/task.js

@ -0,0 +1,20 @@
import Config from '@/common/js/config.js'
const apiUrl = Config.apiUrl;
const defaultwbs = `${apiUrl}/defaultwbs`;
export function setupTask(app) {
uni.$u.api = { ...uni.$u.api } || {};
uni.$u.api.getGlobal = param => uni.$u.post(`${defaultwbs}/task/global`, param);
uni.$u.api.getPermanent = param => uni.$u.post(`${defaultwbs}/task/permanent`, param);
//根据时间基准点和角色查找定期任务
uni.$u.api.getRegularTask = param => uni.$u.post(`${defaultwbs}/task/regular`, param);
//修改任务状态
uni.$u.api.updateTaskType = param => uni.$u.post(`${defaultwbs}/task/type`, param);
//新建任务
uni.$u.api.saveTask = param => uni.$u.post(`${defaultwbs}/task/save`, param);
//克隆任务
uni.$u.api.cloneTask = param => uni.$u.post(`${defaultwbs}/task/clone`, param);
//模糊查询 查找项目下的任务
uni.$u.api.queryTaskOfProject = param => uni.$u.post(`${defaultwbs}/task/queryTaskOfProject`, param);
};

7
apis/wbs.js

@ -0,0 +1,7 @@
import Config from "@/common/js/config.js"
export function setupWbs(app) {
uni.$u.api = { ...uni.$u.api } || {};
// 导入wbs
uni.$u.api.import = formData => uni.$upload.chooseAndUpload(`${Config.apiUrl}/wbs`, formData);
}

BIN
common/img/weixinIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

15
common/js/config.js

@ -0,0 +1,15 @@
var config = {
baseUrl: 'https://test.tall.wiki',
apiUrl: 'https://test.tall.wiki/gateway',
msgUrl: 'wss://test.tall.wiki/websocket/message/v4.0/ws',
projectPath: 'https://test.tall.wiki/tall-project',
// baseUrl: 'https://www.tall.wiki',
// apiUrl: 'https://www.tall.wiki/gateway',
// msgUrl: 'wss://www.tall.wiki/websocket/message/v4.0/ws';
// projectPath: 'https://www.tall.wiki/tall-project',
version: 'v4.0.0'
};
export default config;

48
common/js/dc-clipboard/clipboard.js

@ -0,0 +1,48 @@
/**
* 设置粘贴板数据
* @param {String} text 要设置的字符串
* 如果未设置参数则清空数据
*/
function setClipboardText(text) {
try {
var os = plus.os.name;
text = text || '';
if ('iOS' == os) {
// var UIPasteboard = plus.ios.importClass('UIPasteboard');
// var pasteboard = UIPasteboard.generalPasteboard();
// pasteboard.setValueforPasteboardType(text, 'public.utf8-plain-text');
var pasteboard = plus.ios.invoke('UIPasteboard', 'generalPasteboard');
plus.ios.invoke(pasteboard, 'setValue:forPasteboardType:', text, 'public.utf8-plain-text');
} else {
var main = plus.android.runtimeMainActivity();
// var Context = plus.android.importClass('android.content.Context');
// var clip = main.getSystemService(Context.CLIPBOARD_SERVICE);
var clip = main.getSystemService('clipboard');
plus.android.invoke(clip, 'setText', text);
}
} catch (e) {
console.error('error @setClipboardText!!');
}
}
function getClipboardText() {
try {
var os = plus.os.name;
if ('iOS' == os) {
var pasteboard = plus.ios.invoke('UIPasteboard', 'generalPasteboard');
return plus.ios.invoke(pasteboard, 'valueForPasteboardType:', 'public.utf8-plain-text')
} else {
var main = plus.android.runtimeMainActivity();
var clip = main.getSystemService('clipboard');
return plus.android.invoke(clip, 'getText');
}
} catch (e) {
console.error('error @getClipboardText!!');
}
}
export default {
setClipboardText,
getClipboardText
}

4496
common/styles/tailwind.scss

File diff suppressed because it is too large

16
common/styles/theme/default.scss

@ -0,0 +1,16 @@
// 默认主题文件
.theme-default {
background-color: #007aff;
color: #fff;
.u-card {
font-size: 24px !important;
color: #0f0;
}
.u-navbar {
background-color: #007aff !important;
color: #fff;
.uicon-nav-back {
color: #fff !important;
}
}
}

5
common/styles/theme/index.scss

@ -0,0 +1,5 @@
// 整合所有主题
// 默认主题
@import './default.scss';
@import './test.scss';

11
common/styles/theme/test.scss

@ -0,0 +1,11 @@
// TODO: 测试用的scss主题样式
.theme-test {
background-color: #fff;
.u-card {
font-size: 24px !important;
background-color: #ff0 !important;
}
.u-navbar {
background-color: #ff0 !important;
}
}

451
components/Calendar/Calendar.vue

@ -0,0 +1,451 @@
<template>
<view class="zzx-calendar">
<view class="calendar-header">{{ timeStr }}</view>
<!-- 星期几标题 -->
<view class="calendar-weeks">
<view
class="calendar-week"
:class="{ 'text-red-500': week === '六' || week === '日' }"
v-for="(week, index) in data.weeks"
:key="index"
>{{ week }}</view>
</view>
<view class="calendar-content">
<swiper
class="calendar-swiper"
:style="{
width: '100%',
height: calenderHeight,
}"
:indicator-dots="false"
:autoplay="false"
:duration="500"
:current="data.current"
@change="changeSwp"
:circular="true"
>
<swiper-item class="calendar-item" v-for="sItem in data.swiper" :key="sItem">
<view class="calendar-days">
<!-- 当前的 -->
<template v-if="sItem === data.current">
<view
class="calendar-day"
v-for="(item, index) in data.days"
:key="index"
:class="{ 'day-hidden': !item.show }"
@click="clickItem(item)"
>
<view
class="date"
:class="[item.isToday ? 'is-today' : '', item.fullDate === data.selectedDate ? 'is-checked' : '']"
>{{ item.time.getDate() }}</view>
<view class="dot-show" v-if="item.info === '0'" :style="dotStyle"></view>
</view>
</template>
<template v-else>
<!-- 下一个月/ -->
<template v-if="data.current - sItem === 1 || data.current - sItem === -2">
<view
class="calendar-day"
v-for="(item, index) in preDays"
:key="index"
:class="{
'day-hidden': !item.show,
}"
>
<view class="date" :class="[item.isToday ? 'is-today' : '']">{{ item.time.getDate() }}</view>
</view>
</template>
<!-- 上一个月/ -->
<template v-else>
<view
class="calendar-day"
v-for="(item, index) in nextDays"
:key="index"
:class="{
'day-hidden': !item.show,
}"
>
<view class="date" :class="[item.isToday ? 'is-today' : '']">{{ item.time.getDate() }}</view>
</view>
</template>
</template>
</view>
</swiper-item>
</swiper>
<!-- <view class="mode-change" @click="changeMode">
<view :class="weekMode ? 'mode-arrow-bottom' : 'mode-arrow-top'"> </view>
</view>-->
</view>
<view class="flex justify-center u-font-18" style="color: #3b82f6" @click="goToday">今日</view>
</view>
</template>
<script setup>
import { reactive, computed, watchEffect } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
import { generateDates, formatDate } from './generateDates.js';
const props = defineProps({
//
dotStyle: {
type: Object,
default: () => ({ background: '#4ade80' }),
},
});
const emit = defineEmits(['handleFindPoint', 'handleFindPoint', 'days-change', 'selected-change']);
const data = reactive({
weeks: ['日', '一', '二', '三', '四', '五', '六'], //
current: 1,
currentYear: '',
currentMonth: '',
currentDate: '',
days: [],
weekMode: false, // false -> true ->
swiper: [0, 1, 2],
selectedDate: formatDate(new Date(), 'yyyy-MM-dd'), //
start: dayjs().startOf('month').valueOf(),
end: dayjs().endOf('month').valueOf(),
});
const store = useStore();
const dotList = computed(() => store.state.project.dotList);
const calenderHeight = computed(() => {
//
//
let h = '35px';
if (!data.weekMode) {
const d = new Date(data.currentYear, data.currentMonth, 0);
const days = d.getDate(); //
const day = new Date(d.setDate(1)).getDay();
// if (day === 0) {
// day = 7;
// }
const pre = 8 - day;
const rows = Math.ceil((days - pre) / 7) + 1;
h = `${35 * rows}px`;
}
return h;
});
//
const timeStr = computed(() => {
let str = '';
const d = new Date(data.currentYear, data.currentMonth - 1, data.currentDate);
const y = d.getFullYear();
const m = d.getMonth() + 1 <= 9 ? `0${d.getMonth() + 1}` : d.getMonth() + 1;
str = `${y}${m}`;
return str;
});
// days
const preDays = computed(() => {
let pres = [];
if (data.weekMode) {
//
const d = new Date(data.currentYear, data.currentMonth - 1, data.currentDate);
d.setDate(d.getDate() - 7);
pres = generateDates(d, 'week');
} else {
//
const d = new Date(data.currentYear, data.currentMonth - 2, 1);
pres = generateDates(d, 'month');
}
return pres;
});
// days
const nextDays = computed(() => {
let next = [];
if (data.weekMode) {
//
const d = new Date(data.currentYear, data.currentMonth - 1, data.currentDate);
d.setDate(d.getDate() + 7);
next = generateDates(d, 'week');
} else {
//
const d = new Date(data.currentYear, data.currentMonth, 1);
next = generateDates(d, 'month');
}
return next;
});
watchEffect(() => {
const days = data.days.slice(0);
const index = days.findIndex(day => day.show);
days.forEach((day, i) => {
dotList.value.forEach((item, j) => {
if (i - index === j) {
day.info = item;
}
});
});
data.days = days;
});
//
const initDate = cur => {
let date = '';
if (cur) {
date = new Date(cur);
} else {
date = new Date();
}
data.currentDate = date.getDate(); //
data.currentYear = date.getFullYear(); //
data.currentMonth = date.getMonth() + 1; //
data.currentWeek = date.getDay() === 0 ? 7 : date.getDay(); // 1...6,0
// const nowY = new Date().getFullYear(); //
// const nowM = new Date().getMonth() + 1;
// const nowD = new Date().getDate(); //
// const nowW = new Date().getDay();
// this.selectedDate = formatDate(new Date(), 'yyyy-MM-dd')
data.days = [];
let days = [];
if (data.weekMode) {
days = generateDates(date, 'week');
// this.selectedDate = days[0].fullDate;
} else {
days = generateDates(date, 'month');
// const sel = new Date(this.selectedDate.replace('-', '/').replace('-', '/'));
// const isMonth = sel.getFullYear() === this.currentYear && (sel.getMonth() + 1) === this.currentMonth;
// if(!isMonth) {
// this.selectedDate = formatDate(new Date(this.currentYear, this.currentMonth-1,1), 'yyyy-MM-dd')
// }
}
//
days.forEach((day, i) => {
dotList.value.forEach((item, j) => {
if (i === j) {
day.info = item;
}
});
});
data.days = days;
// ,
const obj = {
start: '',
end: '',
};
if (data.weekMode) {
obj.start = data.days[0].time;
obj.end = data.days[6].time;
} else {
const start = new Date(data.currentYear, data.currentMonth - 1, 1);
const end = new Date(data.currentYear, data.currentMonth, 0);
obj.start = start;
obj.end = end;
}
emit('days-change', obj);
};
initDate();
//
const daysPre = () => {
if (data.weekMode) {
const d = new Date(data.currentYear, data.currentMonth - 1, data.currentDate);
d.setDate(d.getDate() - 7);
initDate(d);
} else {
const d = new Date(data.currentYear, data.currentMonth - 2, 1);
initDate(d);
}
};
//
const daysNext = () => {
if (data.weekMode) {
const d = new Date(data.currentYear, data.currentMonth - 1, data.currentDate);
d.setDate(d.getDate() + 7);
initDate(d);
} else {
const d = new Date(data.currentYear, data.currentMonth, 1);
initDate(d);
}
};
/**
* 滑动切换上下周期
* 根据前一个减去目前的值我们可以判断是下一个月/周还是上一个月/
* current - pre === 1, -2 下一个月/
* current - pre === -1, 2 上一个月或者上一周
*/
const changeSwp = e => {
const pre = data.current;
const { current } = e.detail;
data.current = current;
if (current - pre === 1 || current - pre === -2) {
//
daysNext();
const arr = data.days.filter(s => s.show);
const end = `${arr[arr.length - 1].fullDate} 23:59:59`;
data.start = dayjs(arr[0].fullDate).valueOf();
data.end = dayjs(end).valueOf();
emit('handleFindPoint', data.start, data.end);
} else {
//
daysPre();
const arr = data.days.filter(s => s.show);
const end = `${arr[arr.length - 1].fullDate} 23:59:59`;
data.start = dayjs(arr[0].fullDate).valueOf();
data.end = dayjs(end).valueOf();
emit('handleFindPoint', data.start, data.end);
}
};
//
// const changeMode = () => {
// const premode = this.weekMode;
// let isweek = false;
// if (premode) {
// isweek = !!this.days.find((item) => item.fullDate === this.selectedDate);
// }
// this.weekMode = !this.weekMode;
// let d = new Date(this.currentYear, this.currentMonth - 1, this.currentDate);
// const sel = new Date(this.selectedDate.replace('-', '/').replace('-', '/'));
// const isMonth = sel.getFullYear() === this.currentYear && sel.getMonth() + 1 === this.currentMonth;
// if ((this.selectedDate && isMonth) || isweek) {
// d = new Date(this.selectedDate.replace('-', '/').replace('-', '/'));
// }
// this.initDate(d);
// };
//
const clickItem = e => {
data.selectedDate = e.fullDate;
emit('selected-change', e);
};
//
const goToday = () => {
const d = new Date();
initDate(d);
};
</script>
<style lang="scss" scoped>
.zzx-calendar {
width: 100%;
height: auto;
background-color: #fff;
padding-bottom: 10px;
.calendar-header {
text-align: center;
padding: 16px 0;
position: relative;
font-size: 15px;
}
.calendar-weeks {
width: 100%;
display: flex;
flex-flow: row nowrap;
margin-bottom: 10px;
justify-content: center;
align-items: center;
font-size: 12px;
color: #9ca3af;
font-weight: bold;
.calendar-week {
width: calc(100% / 7);
height: 100%;
text-align: center;
}
}
swiper {
width: 100%;
height: 60upx;
}
.calendar-content {
min-height: 30px;
}
.calendar-swiper {
min-height: 35px;
transition: height ease-out 0.3s;
}
.calendar-item {
margin: 0;
padding: 0;
height: 100%;
}
.calendar-days {
display: flex;
flex-flow: row wrap;
width: 100%;
height: 100%;
overflow: hidden;
font-size: 14px;
.calendar-day {
width: calc(100% / 7);
height: 35px;
text-align: center;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: center;
position: relative;
}
}
.day-hidden {
visibility: hidden;
}
.mode-change {
display: flex;
justify-content: center;
margin-top: 5px;
.mode-arrow-top {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 5px solid #ff6633;
}
.mode-arrow-bottom {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 5px solid #ff6633;
}
}
.is-today {
background: #ffffff;
border: 1upx solid #ff6633;
border-radius: 50%;
color: #ff6633;
}
.is-checked {
background: #ff6633;
color: #ffffff;
}
.date {
width: 25px;
height: 25px;
line-height: 25px;
margin: 0 auto;
border-radius: 25px;
}
.dot-show {
width: 6px;
height: 6px;
// background: red;
border-radius: 5px;
position: absolute;
top: 2px;
right: 10px;
}
}
</style>

136
components/Calendar/generateDates.js

@ -0,0 +1,136 @@
/*
*此函数的作用是根据传入的一个日期返回这一周的日期或者这一个月的日期
* 如果是月的话注意还包含上个月和下个月的日期月的话总共数据有 6 * 7 = 42
*
*/
/*
* 时间格式化函数
* 重要提示微信小程序new Date('2020-04-16')在ios中无法获取时间对象
* 解决方式: 建议将时间都格式化成'2020/04/16 00:00:00'的格式
* 函数示例: formatDate(new Date(), 'YYYY/MM/dd hh:mm:ss')
*/
export const formatDate = (date, fmt) => {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str));
}
}
return fmt;
};
const padLeftZero = str => {
return ('00' + str).substr(str.length);
};
// 判断是不是date对象
export const judgeType = s => {
// 函数返回数据的具体类型
return Object.prototype.toString.call(s).slice(8, -1);
};
export const equalDate = (d1, d2) => {
let result = false;
if (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate()) {
result = true;
}
return result;
};
/* ,2020-04-04
*/
export const dateEqual = (before, after) => {
before = new Date(before.replace('-', '/').replace('-', '/'));
after = new Date(after.replace('-', '/').replace('-', '/'));
if (before.getTime() - after.getTime() === 0) {
return true;
} else {
return false;
}
};
export const generateDates = (date = new Date(), type = 'week') => {
const result = [];
if (judgeType(date) === 'Date') {
// 年,月,日
const y = date.getFullYear();
const m = date.getMonth();
const d = date.getDate();
const days = new Date(y, m + 1, 0).getDate();
// 获取日期是星期几
// let weekIndex = date.getDay() === 0 ? 7 : date.getDay();
let weekIndex = date.getDay();
if (type === 'month') {
const dobj = new Date(y, m, 1);
// weekIndex = dobj.getDay() === 0 ? 7 : dobj.getDay();
weekIndex = dobj.getDay();
}
if (type === 'week') {
for (let i = weekIndex; i > 0; i--) {
const dtemp = new Date(y, m, d);
dtemp.setDate(dtemp.getDate() - i);
result.push({
time: dtemp,
show: true,
fullDate: formatDate(dtemp, 'yyyy-MM-dd'),
isToday: equalDate(new Date(), dtemp),
});
}
for (let i = 0; i <= 7 - weekIndex; i++) {
const dtemp = new Date(y, m, d);
dtemp.setDate(dtemp.getDate() + i);
result.push({
time: dtemp,
show: true,
fullDate: formatDate(dtemp, 'yyyy-MM-dd'),
isToday: equalDate(new Date(), dtemp),
});
}
} else if (type === 'month') {
// 上个月
for (let i = weekIndex; i > 0; i--) {
const dtemp = new Date(y, m, 1);
dtemp.setDate(dtemp.getDate() - i);
result.push({
time: dtemp,
show: false,
fullDate: formatDate(dtemp, 'yyyy-MM-dd'),
isToday: equalDate(new Date(), dtemp),
});
}
// 这个月的日期
for (let i = 0; i < days; i++) {
const dtemp = new Date(y, m, 1);
dtemp.setDate(dtemp.getDate() + i);
result.push({
time: dtemp,
show: true,
fullDate: formatDate(dtemp, 'yyyy-MM-dd'),
isToday: equalDate(new Date(), dtemp),
});
}
const len = 42 - result.length;
// 下个月的日期
for (let i = 1; i <= len; i++) {
const dtemp = new Date(y, m + 1, 0);
dtemp.setDate(dtemp.getDate() + i);
result.push({
time: dtemp,
show: false,
fullDate: formatDate(dtemp, 'yyyy-MM-dd'),
isToday: equalDate(new Date(), dtemp),
});
}
}
}
return result;
};

89
components/ChooseChecker/ChooseChecker.vue

@ -0,0 +1,89 @@
<template>
<view class="my-3" v-if="data.allMembers && data.allMembers.length">
<view class="flex justify-between">
<view class="flex flex-wrap text-center items-center">
<u-tag
:type="member.checked ? 'primary' : 'info'"
:mode="member.checked ? 'dark' : 'light'"
v-for="(member, index) in data.topMembers"
:key="member.memberId"
class="mb-2 mr-3"
style="width: 60px"
:text="member.name"
:closeable="false"
@click="tagClick(index, member, 'topMembers')"
/>
<span class="ml-2" v-if="!data.show" @click="data.show = true">...</span>
</view>
</view>
<!-- 折叠起来的 -->
<view class="flex flex-wrap text-center items-center" v-if="data.show">
<u-tag
:type="member.checked ? 'primary' : 'info'"
:mode="member.checked ? 'dark' : 'light'"
v-for="(member, index) in data.bottomMembers"
:key="member.memberId"
class="mb-2 mr-3"
style="width: 60px"
:text="member.name"
:closeable="false"
@click="tagClick(index, member, 'bottomMembers')"
/>
<u-icon class="ml-2" name="arrow-up" v-if="data.show" size="26" @click="data.show = false"></u-icon>
</view>
</view>
</template>
<script setup>
import { reactive, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
const props = defineProps({
checkerList: {
default: () => [],
type: Array,
},
});
const data = reactive({
allMembers: [],
show: false,
topMembers: [],
bottomMembers: [],
});
const store = useStore();
const members = computed(() => store.state.role.members);
const emit = defineEmits(['setCheckerList']);
onMounted(() => {
if (members.value && members.value.length) {
data.allMembers = members.value;
// TODO:
data.allMembers.forEach(item => {
item.checked = false;
});
data.topMembers = members.value.slice(0, 3);
data.bottomMembers = members.value.slice(3);
}
});
function tagClick(index, item, membersType) {
//
const arr = uni.$u.deepClone(data[membersType]);
arr[index].checked = !arr[index].checked;
data[membersType] = [...arr];
// idcheckerList
emit('setCheckerList', arr[index].checked, item);
}
//
function clearChecked() {
for (let i = 0; i < data.topMembers.length; i++) {
data.topMembers[i].checked = false;
}
for (let i = 0; i < data.bottomMembers.length; i++) {
data.bottomMembers[i].checked = false;
}
}
</script>

81
components/Globals/Globals.vue

@ -1,8 +1,85 @@
<template>
<theme class="m-2" >
<u-card
@click="openCard"
:show-foot="false"
:show-head="false"
:style="{ 'max-height': globalsHeight + 'px' }"
border-radius="25"
margin="0"
class="global-container"
>
<template v-slot:body>
<scroll-view :scrollY="true" :style="{ 'max-height': globalsHeight - 30 + 'px' }">
<skeleton :banner="false" :loading="showGlobalSkeleton" :row="3" animate class="u-line-2 skeleton"></skeleton>
<view class="grid gap-2">
<view v-for="item in globals" :key="item.id">
<template v-if="item.plugins && item.plugins.length">
<view v-for="(pluginArr, i) in item.plugins" :key="i">
<template class="p-0 u-col-between" v-if="pluginArr.length">
<Plugin
:class="[`row-span-${plugin.row}`, `col-span-${plugin.col}`]"
:task="item"
:key="plugin.pluginTaskId"
:plugin-task-id="plugin.pluginTaskId"
:plugin-id="plugin.pluginId"
:param="plugin.param"
:style-type="plugin.styleType || 0"
v-for="plugin in pluginArr"
/>
</template>
</view>
</template>
<!-- 任务名插件 -->
<p-task-title :task="item" v-else />
<!-- 交付物插件 -->
<p-deliver></p-deliver>
</view>
</view>
</scroll-view>
</template>
</u-card>
</theme>
</template>
<script>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import Skeleton from '@/components/Skeleton/Skeleton.vue';
const sysHeight = uni.getSystemInfoSync().screenHeight; //
const globalsHeight = Math.floor(((sysHeight - 44 - 30 - 10) / 5) * 4); //
const store = useStore();
const isShrink = computed(() => store.state.task.isShrink); //
const showGlobalSkeleton = computed(() => store.state.task.showGlobalSkeleton); //
const globals = computed(() => store.getters['task/globals']);
//
function openCard() {
if (isShrink.value) {
store.commit('task/setShrink', false);
}
}
</script>
<style>
<style scoped lang="scss">
.u-card-wrap {
background-color: $u-bg-color;
padding: 1px;
}
.u-body-item {
font-size: 32rpx;
color: #333;
padding: 20rpx 10rpx;
}
.u-body-item image {
width: 120rpx;
flex: 0 0 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-left: 12rpx;
}
</style>

59
components/Plugin/Plugin.vue

@ -0,0 +1,59 @@
<template>
<view class="u-font-14" style="height: 100%">
<view @click="setStorage" class="grid gap-3">
<Render :task="task" :pluginId="pluginId" :styleType="styleType" :pluginTaskId="pluginTaskId" :param="param" />
<!-- <plugin-default /> -->
<!-- <component :task="task" :is="pluginComponent"></component> -->
<p-task-title :task="task" v-if="pluginId === '1'" />
<p-task-description :task="task" v-if="pluginId === '2'" />
<p-task-duration-delay :task="task" v-if="pluginId === '3'" />
<p-task-start-time-delay :task="task" v-if="pluginId === '4'" />
<p-upload-deliverable :task="task" v-if="pluginId === '5' && isMine" />
<p-delivery-history :task="task" v-if="pluginId === '5' && !isMine" />
<p-subtasks :task="task" v-if="pluginId === '6'" />
<p-subproject :task="task" v-if="pluginId === '7'" />
<p-task-countdown :task="task" v-if="pluginId === '8'" />
<p-manage-project :task="task" v-if="pluginId === '9'" />
<p-manage-role :task="task" v-if="pluginId === '10'" />
<p-manage-member :task="task" v-if="pluginId === '11'" />
<p-manage-task :task="task" v-if="pluginId === '12'" />
<p-wbs-import :task="task" v-if="pluginId === '13' || pluginId === '14'" />
<p-deliver-check :task="task" v-if="pluginId === '15'" />
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const props = defineProps({
task: { default: () => {}, type: Object },
pluginId: { default: '1', type: String },
styleType: { default: 0, type: Number },
pluginTaskId: { default: '', type: String },
param: { type: String, default: '' },
});
const store = useStore();
const roleId = computed(() => store.state.role.roleId);
const token = computed(() => store.state.user.token);
const userId = computed(() => store.getters['user/userId']);
const projectId = computed(() => store.getters['project/projectId']);
const isMine = computed(() => store.getters['role/isMine']);
//
// const pluginComponent = computed(() => {
// const target = uni.$pluginConfig.defaults.find(item => item.id === +props.pluginId);
// if (!target) return '';
// return target.component;
// });
if (props.pluginId === '5') {
store.dispatch('role/getAllMembers', { projectId: projectId.value });
}
// storage
async function setStorage() {
uni.$storage.setStorageSync('roleId', roleId.value);
}
</script>

480
components/PrettyExchange/PrettyExchange.vue

@ -0,0 +1,480 @@
<template>
<view>
<scroll-view scroll-y="true">
<view v-if="!data.changeEvent">
<view :id="'cu-' + index" :key="item.id" class="cu-item flex-col" v-for="(item, index) in data.itemList">
<ProjectItem
class="w-full"
:index="index"
:item="item"
:menuList="data.menuList"
@setData="setData"
@openSubProject="openSubProject"
/>
</view>
</view>
<view v-else>
<view
:id="'cu-' + index"
:key="index"
:style="{ 'background-color': item.color }"
@touchend="stops($event, index)"
@touchmove.stop.prevent="move"
@touchstart="start($event, index)"
class="cu-item flex-col" v-for="(item, index) in data.itemList"
>
<view class="border-100 bg-blue-500" v-if="item.showTopBorder"></view>
<!-- 内容区 -->
<!-- 父项目 -->
<view class="w-full">
<view class="flex items-center justify-between p-3">
<u-icon class="mover" name="https://www.tall.wiki/staticrec/drag.svg" size="48"></u-icon>
<view class="flex-1 px-3">
<view class="flex items-center mb-1">
<view class="mr-2">{{ item.name }}</view>
<!-- 状态 TODO:-->
<view class="px-2 text-xs text-green-400 bg-green-100 rounded-full flex-shrink-0">进行中</view>
</view>
<view class="flex items-center text-xs text-gray-400">
<view class="pr-2">{{ dayjs(+item.startTime).format('MM-DD HH:mm') }}</view>
<view class="pl-2">{{ dayjs(+item.endTime).format('MM-DD HH:mm') }}</view>
</view>
</view>
<!-- 箭头 -->
<view v-if="item.sonProjectList && item.sonProjectList.length">
<u-icon @click="openSubProject(item.sonProjectList.length, index)" class="text-gray-400" name="arrow-up" size="14px" v-if="item.show"></u-icon>
<u-icon @click="openSubProject(item.sonProjectList.length, index)" class="text-gray-400" name="arrow-down" size="14px" v-else></u-icon>
</view>
<u-icon class="text-gray-400" name="arrow-right" size="14px" v-else></u-icon>
</view>
<!-- 父项目 end -->
<!-- 子项目 -->
<view class="ml-8" v-if="item.show">
<view :id="'cu-' + index + '-' + subIndex" :key="subIndex"
@touchend.stop.prevent="stops($event, index + '-' + subIndex, item.sonProjectList.length)"
@touchmove.stop.prevent="move($event, item.sonProjectList.length)"
@touchstart.stop.prevent="start($event, index + '-' + subIndex)"
class="cu-item flex-col" v-for="(subItem, subIndex) in item.sonProjectList">
<view class="flex items-center justify-between p-3 w-full">
<u-icon class="mover" name="https://www.tall.wiki/staticrec/drag.svg" size="48">
</u-icon>
<view class="flex-1 px-3">
<view class="flex items-center">
<view class="mr-2">{{ subItem.name }}</view>
<!-- 状态 -->
<view
:class="
subItem.status === 0
? 'text-blue-400 bg-blue-100'
: subItem.status === 1
? 'text-green-400 bg-green-100'
: subItem.status === 2
? 'text-red-400 bg-red-100'
: 'text-gray-400 bg-gray-100'
"
class="px-2 text-xs text-gray-400 bg-gray-100 rounded-full flex-shrink-0">
{{ subItem.status === 0 ? '未开始' : subItem.status === 1 ? '进行中' : subItem.status === 2 ? '暂停' : '已完成' }}
</view>
</view>
</view>
<!-- 箭头 -->
<u-icon class="text-gray-400" name="arrow-right" size="14px"></u-icon>
</view>
</view>
</view>
</view>
<!-- 内容区 end -->
<view class="border-100 bg-blue-500" v-if="item.showBorder"></view>
<view class="border-80 bg-blue-500" v-if="item.showSubBorder"></view>
</view>
</view>
</scroll-view>
<!-- 移动悬浮 begin -->
<view v-if="data.showMoveImage">
<view :style="{ left: moveLeft + 'px', top: moveTop + 'px' }" class="cu-item absolute">
<ProjectItem class="w-full" :item="moveItem" />
</view>
</view>
<!-- 移动悬浮 end -->
<!-- 项目操作面板 -->
<u-action-sheet :list="data.menuList" :tips="data.tips" @click="chooseAction" v-model="data.showMenu"></u-action-sheet>
</view>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import ProjectItem from '@/components/Projects/ProjectItem.vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
const store = useStore();
const projects = computed(() => store.state.project.projects);
const data = ref({
itemTop: 0,
itemLeft: 0,
itemHeight: 0, //
subItemHeight: 0, //
itemWidth: 0, //
showMoveImage: false,
moveItem: '',
moveLeft: 0,
moveTop: 0,
deltaLeft: 0,
deltaTop: 0,
beginleft: 0,
begintop: 0,
itemList: [],
setSubItem: false,
changeEvent: false,
showMenu: false,
tips: {
text: '',
color: '#909399',
fontSize: 28,
},
projectId: 0,
menuList: [{ text: '复制' }, { text: '编辑' }, { text: '删除' }, { text: '置顶' }, { text: '排序' }],
// show: false,
border: 'border border-blue-500 shadow rounded-md',
showBorder: false,
showItemIndex: undefined,
});
watch(projects, (val) => {
data.value.itemList = val;
data.value.itemList.forEach(item => {
item.showBorder = false;
item.showSubBorder = false;
item.showTopBorder = false;
});
})
onMounted(() => {
data.value.itemList = projects.value;
data.value.itemList.forEach(item => {
item.showBorder = false;
item.showSubBorder = false;
item.showTopBorder = false;
});
});
//
function openSubProject(length, index) {
setProjectItemShow({ index, show: data.value.itemList[index].show ? false : true });
if (length && index) {
this.$emit('changeHeight', length, index);
}
data.value.showItemIndex = index;
}
//
function getDate() {
const query = uni.createSelectorQuery().in(this);
query.select(`#cu-0`).boundingClientRect(res => {
console.log('data: ', res);
data.value.begintop = res.top;
data.value.beginleft = res.left;
}).exec();
}
function setData(flag, projectId, tips) {
data.value.showMenu = flag;
data.value.projectId = projectId;
data.value.tips = tips;
}
function chooseAction(e) {
let obj = { index: e, projectId: data.value.projectId };
// this.$emit('chooseAction', data);
actionFun(obj);
}
//
function actionFun(obj) {
let action = data.value.menuList[obj.index].text;
if (action === '排序') {
data.value.changeEvent = true;
uni.$ui.showToast('请移动进行排序');
}
if (action === '删除') {
data.value.changeEvent = false;
delProject(obj.projectId);
}
if (data.value.showItemIndex !== undefined) {
setProjectItemShow({ index: data.value.showItemIndex, show: true });
}
}
function isNumber(val) {
return val === +val;
}
function start(e, index) {
console.log('开始', e);
setTimeout(() => {
getDate();
}, 300);
if (isNumber(index)) {
data.value.setSubItem = false;
const query = uni.createSelectorQuery().in(this);
console.log('2222', query)
query.select(`#cu-${index}`).boundingClientRect(res => {
data.value.moveTop = res.top;
data.value.moveLeft = res.left;
data.value.moveItem = data.value.itemList[index];
data.value.itemWidth = res.width;
data.value.itemHeight = res.height;
}).exec();
} else {
let arr = index.split('-');
data.value.setSubItem = true;
const query = uni.createSelectorQuery().in(this);
query.select(`#cu-${arr[0] - 0}`).boundingClientRect(res => {
data.value.itemHeight = res.height;
}).exec();
query.select(`#cu-${index}`).boundingClientRect(res => {
data.value.moveTop = res.top;
data.value.moveLeft = res.left;
data.value.moveItem = data.value.itemList[arr[0] - 0].sonProjectList[arr[1] - 0];
data.value.itemWidth = res.width;
data.value.subItemHeight = res.height;
}).exec();
}
}
function move(e, length) {
console.log('移动');
data.value.showMoveImage = true; //
const touch = e.touches[0];
if (data.value.deltaLeft == 0) {
//
data.value.deltaLeft = touch.pageX - data.value.moveLeft;
data.value.deltaTop = touch.pageY - data.value.moveTop;
}
data.value.moveLeft = touch.pageX - data.value.deltaLeft;
data.value.moveTop = touch.pageY - data.value.deltaTop;
let lastIndex = (lastIndex = findOverIndex(touch.pageY, length));
console.log('111111', lastIndex);
// 线
for (let i = 0; i < data.value.itemList.length; i++) {
if (data.value.moveLeft > 35) {
data.value.itemList[i].showBorder = false;
data.value.itemList[i].showTopBorder = false;
if (i === lastIndex) {
data.value.itemList[i].showSubBorder = true;
} else {
data.value.itemList[i].showSubBorder = false;
}
} else {
if (lastIndex === -1) {
data.value.itemList[0].showTopBorder = true;
data.value.itemList[i].showSubBorder = false;
data.value.itemList[i].showBorder = false;
} else {
data.value.itemList[i].showSubBorder = false;
data.value.itemList[i].showTopBorder = false;
if (i === lastIndex) {
data.value.itemList[i].showBorder = true;
} else {
data.value.itemList[i].showBorder = false;
}
}
}
}
}
function stops(e, index, length) {
console.log('结束');
const touch = e.mp.changedTouches[0];
let lastIndex = (lastIndex = findOverIndex(touch.pageY, length));
//
for (let i = 0; i < data.value.itemList.length; i++) {
//
if (data.value.itemList[i].showTopBorder) {
if (isNumber(index)) {
let Value = data.value.itemList[index];
data.value.itemList.unshift(Value);
data.value.itemList.splice(index + 1, 1);
} else {
let arr = index.split('-');
let Value = data.value.itemList[arr[0] - 0].sonProjectList[arr[1] - 0];
data.value.itemList.unshift(Value);
data.value.itemList[arr[0] - 0].sonProjectList.splice([arr[1] - 0], 1);
const options = {
id: Value.id,
parentId: 0,
};
this.$emit('change', options);
}
//
clearSet(i);
this.$emit('change', data.value.itemList);
return;
}
//
if (data.value.itemList[i].showBorder) {
if (isNumber(index)) {
let Value = data.value.itemList[index];
data.value.itemList.splice(i + 1, 0, Value);
if (i < index) {
data.value.itemList.splice(index + 1, 1);
} else {
data.value.itemList.splice(index, 1);
}
} else {
let arr = index.split('-');
let Value = data.value.itemList[arr[0] - 0].sonProjectList[arr[1] - 0];
data.value.itemList.splice(i + 1, 0, Value);
data.value.itemList[arr[0] - 0].sonProjectList.splice([arr[1] - 0], 1);
const options = {
id: Value.id,
parentId: 0,
};
this.$emit('change', options);
}
//
clearSet(i);
this.$emit('change', data.value.itemList);
return;
}
//
if (data.value.itemList[i].showSubBorder) {
if (isNumber(index)) {
let Value = data.value.itemList[index];
if (data.value.itemList[lastIndex - 1].sonProjectList && data.value.itemList[lastIndex - 1].sonProjectList.length) {
data.value.itemList[lastIndex - 1].sonProjectList.push(Value);
} else {
data.value.itemList[lastIndex].sonProjectList = [Value];
}
data.value.itemList.splice(index, 1);
//
clearSet(i);
const options = {
id: Value.id,
parentId: data.value.itemList[lastIndex - 1].id,
};
this.$emit('change', options);
} else {
let arr = index.split('-');
let Value = data.value.itemList[arr[0] - 0].sonProjectList[arr[1] - 0];
if (data.value.itemList[lastIndex].sonProjectList && data.value.itemList[lastIndex].sonProjectList.length) {
data.value.itemList[lastIndex].sonProjectList.push(Value);
} else {
data.value.itemList[lastIndex].sonProjectList = [Value];
}
data.value.itemList[arr[0] - 0].sonProjectList.splice([arr[1] - 0], 1);
//
clearSet(i);
const options = {
id: Value.id,
parentId: data.value.itemList[lastIndex].id,
};
this.$emit('change', options);
const options1 = {
id: Value.id,
parentId: 0,
};
this.$emit('change', options1);
}
return;
}
}
}
//
function clearSet(i) {
data.value.itemList[i].showBorder = false;
data.value.itemList[i].showSubBorder = false;
data.value.itemList[i].showTopBorder = false;
data.value.deltaLeft == 0;
data.value.showMoveImage = false;
data.value.setSubItem = false;
data.value.changeEvent = false;
data.value.showItemIndex = undefined;
}
//
function findOverIndex(posY) {
//
let leng = data.value.itemList.length * data.value.itemHeight; //
if (posY < data.value.begintop) {
return -1;
}
for (var i = 0; i < data.value.itemList.length; i++) {
let begin = data.value.itemHeight * i + data.value.begintop;
let end = data.value.itemHeight * i + data.value.begintop + data.value.itemHeight;
if (begin <= posY && end >= posY) {
return i;
}
}
if (posY > leng) {
//
return data.value.itemList.length - 1;
} else if (posY < data.value.begintop) {
return 0;
}
}
//
function delProject(id) {
uni.showModal({
title: '',
content: '是否删除项目?',
showCancel: true,
success: async ({ confirm }) => {
if (confirm) {
await this.$u.api.delProject(id);
let flag_index = 0;
data.value.itemList.forEach((item, index) => {
if (item.id == id) {
flag_index = index;
}
});
data.value.itemList.splice(flag_index, 1);
setProjects(data.value.itemList);
}
},
});
}
</script>
<style lang="scss" scoped>
.cu-item {
width: 100%;
display: flex;
align-items: center;
font-size: 14px;
}
.border-100 {
width: 92%;
height: 4rpx;
}
.border-80 {
width: 84%;
height: 2px;
margin-left: 30px;
}
</style>

134
components/Projects/ProjectItem.vue

@ -0,0 +1,134 @@
<template>
<view class="w-full">
<!-- 有子项目 -->
<view class="flex items-center justify-between p-3">
<u-icon @click="openMenu(item)" class="mover" name="https://www.tall.wiki/staticrec/drag.svg" size="48"></u-icon>
<view @click="openProject(item)" class="flex-1 px-3">
<view class="flex items-center mb-1">
<view class="mr-2">{{ item.name }}</view>
<!-- 状态 TODO:-->
<view class="px-2 text-xs text-green-400 bg-green-100 rounded-full flex-shrink-0">进行中</view>
</view>
<view class="flex items-center text-xs text-gray-400">
<view class="pr-2">{{ dayjs(item.startTime).format('MM-DD HH:mm') }}</view>
<view class="pl-2">{{ dayjs(item.endTime).format('MM-DD HH:mm') }}</view>
</view>
</view>
<!-- 箭头 -->
<view v-if="item.sonProjectList && item.sonProjectList.length">
<u-icon @click="$emit('openSubProject', item.sonProjectList.length, index)" class="text-gray-400" name="arrow-up" size="14px" v-if="item.show"></u-icon>
<u-icon @click="$emit('openSubProject', item.sonProjectList.length, index)" class="text-gray-400" name="arrow-down" size="14px" v-else></u-icon>
</view>
<u-icon @click="openProject(item)" class="text-gray-400" name="arrow-right" size="14px" v-else></u-icon>
</view>
<!-- 有子项目 -->
<view class="ml-8" v-if="item.show">
<view :id="'cu-' + index + '-' + subIndex" :key="subIndex" class="cu-item flex-col" v-for="(subItem, subIndex) in item.sonProjectList">
<!-- <view :key="subItem.id" v-for="subItem in item.sonProjectList"> -->
<view class="flex items-center justify-between p-3">
<u-icon @click="openMenu(subItem)" class="mover" name="https://www.tall.wiki/staticrec/drag.svg" size="48"></u-icon>
<view @click="openProject(subItem)" class="flex-1 px-3">
<view class="flex items-center">
<view class="mr-2">{{ subItem.name }}</view>
<!-- 状态 -->
<view
:class="
subItem.status === 0
? 'text-blue-400 bg-blue-100'
: subItem.status === 1
? 'text-green-400 bg-green-100'
: subItem.status === 2
? 'text-red-400 bg-red-100'
: 'text-gray-400 bg-gray-100'
"
class="px-2 text-xs text-gray-400 bg-gray-100 rounded-full flex-shrink-0"
>
{{ subItem.status === 0 ? '未开始' : subItem.status === 1 ? '进行中' : subItem.status === 2 ? '暂停' : '已完成' }}
</view>
</view>
</view>
<!-- 箭头 -->
<u-icon @click="openProject(subItem)" class="text-gray-400" name="arrow-right" size="14px"></u-icon>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
import { useStore } from 'vuex';
import config from '@/common/js/config.js';
defineProps({
item: {
type: Object,
default: () => {},
},
index: {
type: Number,
default: 0,
},
// menuList: {
// type: Array,
// default: () => [],
// },
});
const emit = defineEmits(['setData']);
const store = useStore();
const userId = computed(() => store.getters['user/userId']);
const data = ref({
showMenu: false,
tips: {
text: '',
color: '#909399',
fontSize: 28,
},
// show: false,
// border: 'border border-blue-500 shadow rounded-md',
// showBorder: false,
projectId: 0,
});
//
function openProject(project) {
const gateway = config.apiUrl;
const url = `${gateway}/defaultwbs`;
const { name, id } = project;
uni.navigateTo({ url: `/pages/project/project?u=${userId.value}&p=${id}&pname=${name}&url=${encodeURIComponent(url)}` });
}
/**
* 弹出项目操作面板
*/
function openMenu(project) {
data.showMenu = true;
data.projectId = project.id;
data.tips.text = project.name;
emit('setData', data.showMenu, data.projectId, data.tips);
// this.$emit('setData', data.showMenu, data.projectId, data.tips);
}
</script>
<style lang="scss" scoped>
.border-100 {
height: 4rpx;
margin: 0 20rpx;
}
.border-80 {
height: 4rpx;
margin: 0 20rpx 0 90rpx;
}
</style>

57
components/Projects/Projects.vue

@ -0,0 +1,57 @@
<template>
<view class="py-3 mt-4 bg-white u-font-15">
<PrettyExchange @change="change" />
</view>
</template>
<script setup>
function change(options) {
if (options instanceof Array) {
let projectIdList = [];
let arr = [];
options.forEach(item => {
projectIdList.push(item.id);
arr.push(item.name);
});
setProjectSort(projectIdList);
} else {
setProjectRelation(options);
}
}
/**
* 设置项目顺序
* @param { Array } projectIdList 项目id
*/
async function setProjectSort(projectIdList) {
try {
const params = { projectIdList };
await uni.$u.api.setProjectSort(params);
uni.$ui.showToast('排序修改成功');
} catch (error) {
console.log('error: ', error);
uni.$ui.showToast(error.msg || '排序修改失败');
}
this.$emit('getProjects');
}
/**
* 设置项目父子结构
* @param { string } id 当前移动的项目的id
* @param { string } parentId 父项目的id
*/
async function setProjectRelation(options) {
try {
const params = options;
await uni.$u.api.setProjectRelation(params);
uni.$ui.showToast('排序修改成功');
} catch (error) {
console.error('error: ', error);
uni.$ui.showToast(error.msg || '排序修改失败');
}
this.$emit('getProjects');
}
</script>
<style lang="scss" scoped>
</style>

104
components/Render/Render.vue

@ -0,0 +1,104 @@
<template>
<view>
<!-- #ifdef H5 -->
<view
class="content"
id="project"
:data-did="task.detailId"
:data-param="param"
:data-pdu="task.planDuration"
:data-pid="projectId"
:data-pstart="task.planStart"
:data-rdu="task.realDuration"
:data-rid="roleId"
:data-tid="task.id"
:data-tname="task.name"
:data-token="token"
:data-rstart="task.realStart"
:data-uid="userId"
></view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view
class="content"
id="project"
:data-did="task.detailId"
:data-param="param"
:data-pdu="task.planDuration"
:data-pid="projectId"
:data-pstart="task.planStart"
:data-rdu="task.realDuration"
:data-rid="roleId"
:data-tid="task.id"
:data-tname="task.name"
:data-token="token"
:data-rstart="task.realStart"
:data-uid="userId"
></view>
<!-- #endif -->
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
defineProps({
task: { default: () => {}, type: Object },
pluginId: { default: '1', type: String },
styleType: { default: 0, type: Number },
pluginTaskId: { default: '', type: String },
param: { type: String, default: '' },
});
const store = useStore();
const roleId = computed(() => store.state.role.roleId);
const token = computed(() => store.state.user.token);
const userId = computed(() => store.getters['user/userId']);
const projectId = computed(() => store.getters['project/projectId']);
</script>
<script module="project" lang="renderjs">
export default {
data() {
return {
pluginContent: null,
pluginJs: null,
};
},
mounted() {
this.$nextTick(() => {
this.getPlugin();
});
},
methods: {
//
async getPlugin() {
const params = { pluginId: this.pluginId, styleType: this.styleType };
this.$catchReq.getOtherPlugin(params, (err, res) => {
if (err) {
console.error('err: ', err);
} else {
if (!res || !res.id) return;
if (res.html && res.js) {
this.$nextTick(() => {
this.init(res);
});
}
}
});
},
init(res) {
const content = document.querySelector('.content');
content.innerHTML = res.html;
const script = document.createElement('script');
script.innerHTML = res.js;
document.body.appendChild(script);
},
},
};
</script>

232
components/Roles/Roles.vue

@ -1,8 +1,236 @@
<template>
<view class="px-2 bg-white wrap">
<view class="home-box u-skeleton">
<scroll-view :enable-flex="true" :scroll-left="data.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 data.roles"
>
<view class="tab-children u-skeleton-fillet u-font-14">{{ item.name }}</view>
</view>
</view>
</scroll-view>
</view>
<!-- 骨架屏 -->
<u-skeleton :animation="true" :loading="data.loading" bg-color="#fff"></u-skeleton>
</view>
</template>
<script>
<script setup>
import { reactive, computed, watchEffect, onMounted, nextTick } from 'vue';
import { useStore } from 'vuex';
const data = reactive({
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,
});
const store = useStore();
const visibleRoles = computed(() => store.state.role.visibleRoles);
const roleId = computed(() => store.state.role.roleId);
const tasks = computed(() => store.state.task.tasks);
watchEffect(() => {
if (visibleRoles.value && visibleRoles.value.length) {
data.roles = visibleRoles.value;
data.loading = false;
}
});
onMounted(() => {
if (!visibleRoles.value || !visibleRoles.value.length) {
data.loading = true;
} else {
data.roles = visibleRoles.value;
}
nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.selectAll('.tab-children')
.boundingClientRect(res => {
if (res.length) {
data.roleLeft = res[0].left;
}
})
.exec();
});
});
function scroll(e) {
data.scrollLeft = e.detail.scrollLeft;
}
//
function setCurrentRole(index) {
const query = uni.createSelectorQuery().in(this);
query
.selectAll('.tab-children')
.boundingClientRect(res => {
res.forEach(item => {
data.tabList.push({ width: item.width });
});
})
.exec();
const system = uni.getSystemInfoSync(); //
//
const screenWidth = system.windowWidth;
//
let left = 0;
for (let i = 0; i < index; i++) {
left += data.tabList[i].width + (data.roleLeft - 8) * 2;
}
left += (data.tabList[index].width + (data.roleLeft - 8) * 2) / 2;
if (left > screenWidth) {
data.scrollLeft = left - screenWidth + screenWidth / 2;
} else if (left > screenWidth / 2) {
data.scrollLeft = left - screenWidth / 2;
} else if (left < screenWidth / 2) {
data.scrollLeft = 0;
}
}
// script
function 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);
}
}
//
// projectroleId
// projectroleId
function changeRole(id, index) {
try {
// script
clearPluginScript();
nextTick(() => {
store.commit('role/setRoleId', id);
// index
setCurrentRole(index);
});
} catch (error) {
console.error('role.vue changeRole error: ', error);
}
}
</script>
<style>
<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>

173
components/Skeleton/Skeleton.vue

@ -0,0 +1,173 @@
<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 setup>
import { computed } from 'vue';
/**
* 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
*
* */
const props = defineProps({
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'
// }
});
const avatarClass = computed(() => {
if (props.avatar === 'top') {
return ['lx-skeleton_avator__top'];
}
if (props.avatar === 'left') {
return ['lx-skeleton_avator__left'];
} return '';
});
const animationClass = computed(() => [props.animate ? 'lx-skeleton_animation' : '']);
const slotClass = computed(() => [!props.loading ? 'show' : 'hide']);
const avatarShapeClass = computed(() => [props.avatarShape == 'round' ? 'lx-skeleton_avator__round' : '']);
const bannerClass = computed(() => [props.banner ? 'lx-skeleton_banner' : '']);
</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>

13
components/Theme/Theme.vue

@ -0,0 +1,13 @@
<template>
<view :class="[theme]">
<slot></slot>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const theme = computed(() => store.state.theme);
</script>

126
components/TimeLine/TimeLine.vue

@ -1,8 +1,126 @@
<template>
<!-- 时间间隔栏 -->
<!-- <Barrier /> -->
<scroll-view
:lower-threshold="300"
scroll-y="true"
:upper-threshold="300"
:scroll-into-view="scrollToTaskId"
@scroll="scroll"
@scrolltolower="handleScrollBottom"
@scrolltoupper="handleScrollTop"
id="scroll"
>
<!-- 时间轴 -->
<!-- <u-divider bg-color="#f3f4f6" class="pt-5" fontSize="14px" v-if="topEnd">到顶啦</u-divider> -->
<TimeBox />
<!-- <u-divider bg-color="#f3f4f6" class="pb-5" fontSize="14px" v-if="bottomEnd">到底啦</u-divider> -->
</scroll-view>
</template>
<script>
</script>
<script setup>
import { reactive, computed } from 'vue';
import { useStore } from 'vuex';
// import Barrier from './component/Barrier.vue';
import dayjs from 'dayjs';
import TimeBox from './component/TimeBox.vue';
const store = useStore();
// const visibleRoles = computed(() => store.state.role.visibleRoles);
// const scrollTop = computed(() => store.state.task.scrollTop);
const tasks = computed(() => store.state.task.tasks);
const topEnd = computed(() => store.state.task.topEnd);
const bottomEnd = computed(() => store.state.task.bottomEnd);
const showSkeleton = computed(() => store.state.task.showSkeleton);
const timeNode = computed(() => store.state.task.timeNode);
const scrollToTaskId = computed(() => store.state.task.scrollToTaskId);
const timeGranularity = computed(() => store.getters['task/timeGranularity']);
const emit = defineEmits(['getTasks']);
const data = reactive({ top: 0 });
<style>
</style>
//
function scroll(e) {
data.top = e.detail.scrollTop;
store.commit('task/setShrink', data.top > data.scrollTop);
store.commit('task/setScrollTop', data.top);
}
//
async function handleScrollTop() {
if (!tasks.value || !tasks.value.length || showSkeleton.value) return;
const startTime = tasks.value[0].planStart - 0;
if (topEnd.value) {
//
console.warn('滚动到顶部没有数据时: ');
const addTasks = uni.$task.setPlaceholderTasks(startTime, true, timeGranularity.value);
store.commit('task/setUpTasks', addTasks);
} else {
//
console.warn('滚动到顶部有数据时: ');
const detailId = tasks.value.findIndex(task => task.detailId);
const timeNodeValue = tasks.value[detailId].planStart - 0;
const upQuery = {
timeNode: timeNodeValue,
queryType: 0,
queryNum: 6,
};
await emit('getTasks', upQuery);
}
}
//
async function handleScrollBottom() {
if (!tasks.value || !tasks.value.length || showSkeleton.value) return;
const startTime = tasks.value[tasks.value.length - 1].planStart - 0;
if (bottomEnd.value) {
//
console.warn('滚动到底部没有数据时: ');
const addTasks = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
store.commit('task/setDownTasks', addTasks);
} else {
// =+
console.warn('滚动到底部有数据时: ');
const arr = [];
tasks.value.forEach(task => {
if (task.detailId) {
arr.push(task);
}
});
const nextQueryTime = +uni.$time.add(+arr[arr.length - 1].planStart, 1, timeGranularity.value);
const downQuery = {
timeNode: nextQueryTime,
queryType: 1,
queryNum: 6,
};
await emit('getTasks', downQuery);
}
}
//
function setScrollPosition() {
// storagetaskId id
const taskId = uni.$storage.getStorageSync('taskId');
if (taskId) {
store.commit('task/setScrollToTaskId', `a${taskId}`);
uni.$storage.setStorageSync('taskId', ''); //
} else {
const item = tasks.value.find(task => task.detailId);
if (item) {
store.commit('task/setScrollToTaskId', `a${item.id}`);
} else {
// taskId
// 线id 线
const task = tasks.value.find(item => dayjs(+item.planStart).isSame(timeNode.value, timeGranularity.value));
task && store.commit('task/setScrollToTaskId', `a${task.id}`); // task id
}
}
}
defineExpose({
setScrollPosition
})
</script>

38
components/TimeLine/component/Barrier.vue

@ -0,0 +1,38 @@
<!--
* @Author: aBin
* @email: binbin0314@126.com
* @Date: 2021-07-19 14:22:54
* @LastEditors: aBin
* @LastEditTime: 2021-07-20 11:46:04
-->
<template>
<view class>
<!-- :class="{ active: cycleTasks.time.start === filter.startTime }" -->
<view class="cycle-time active">
<!-- {{ $util.formatStartTimeToCycleTime(filter.time, cycleTasks.time.start) }} -->
2021年30周
</view>
</view>
</template>
<script setup>
</script>
<style scoped lang="scss">
.cycle-time {
padding: 8rpx 16rpx;
margin-bottom: 16rpx;
background: #fafafc;
color: $uni-text-color;
font-size: 28rpx;
position: sticky;
top: -1px;
left: 0;
z-index: 99;
&.active {
background: $uni-color-primary;
color: $uni-text-color-inverse;
}
}
</style>

177
components/TimeLine/component/TaskTools.vue

@ -0,0 +1,177 @@
<template>
<view>
<view class="flex justify-between" style="min-width: 90px; position: relative">
<u-icon custom-prefix="custom-icon" name="C-bxl-redux" size="17px"></u-icon>
<u-icon custom-prefix="custom-icon" name="attachment" size="21px"></u-icon>
<!-- <u-icon custom-prefix="custom-icon" name="moneycollect" size="20px"></u-icon> -->
<u-icon name="xuanxiang" custom-prefix="custom-icon" size="21px" @click="operation"></u-icon>
<!-- 右上角 ... 弹窗 -->
<view class="popup border shadow-md" v-if="data.show">
<!-- <view class="flex justify-center pb-3 border-b-1">
<span>添加插件</span>
</view> -->
<view class="flex justify-center pb-3 border-b-1">
<span @click="createTask">新建任务</span>
</view>
<view class="flex pt-3 justify-center">
<span>克隆任务</span>
</view>
</view>
</view>
<!-- 遮罩 -->
<view class="mask" v-if="data.maskShow" @click="closeMask"></view>
<!-- 新建任务弹窗 -->
<CreateTask
:startTime="data.startTime"
:endTime="data.endTime"
:task="task"
:source="'regular'"
@showTime="showTime"
@closeMask="closeMask"
class="thirdPopup flex transition-transform"
v-if="data.createTaskShow"
/>
<u-picker title="开始时间" mode="time" v-model="data.showStart" :params="data.params" @confirm="confirmStartTime"></u-picker>
<u-picker title="结束时间" mode="time" v-model="data.showEnd" :params="data.params" @confirm="confirmEndTime"></u-picker>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import CreateTask from '@/components/Title/components/CreateTask.vue';
defineProps({ task: { type: Object, default: () => {} } });
const data = reactive({
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,
},
});
//
function operation() {
// this.$t.ui.showToast('');
data.show = !data.show;
}
//
function createTask() {
// ...
data.show = false;
//
data.maskShow = true;
//
data.createTaskShow = true;
}
//
function closeMask() {
//
data.maskShow = false;
//
data.secondShow = false;
//
data.createTaskShow = false;
}
function showTime() {
data.showStart = !data.showStart;
}
//
function confirmStartTime(e) {
data.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
data.showEnd = true;
}
//
function confirmEndTime(e) {
data.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
}
</script>
<style lang="scss" scoped>
.mask {
width: 100%;
height: 100vh;
z-index: 21;
position: fixed;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.3);
}
.thirdPopup {
background: #ffffff;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 33;
border-radius: 5px;
width: 90%;
}
.popup {
width: 110px;
background: #fff;
position: absolute;
right: 0;
top: 35px;
z-index: 99;
padding: 15px 0;
color: black;
animation: opacity 1s 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>

130
components/TimeLine/component/TimeBox.vue

@ -0,0 +1,130 @@
<template>
<!-- v-finger:pinch="pinchHandler" -->
<view class="column">
<view v-if="tasks && tasks.length">
<view :key="task.id" v-for="task in tasks" :id="`a${task.id}`">
<view class="flex">
<TimeStatus :task="task" />
<view class="flex items-center justify-between flex-1 ml-2 task-column">
<view v-if="task.process !== 4">{{ $moment(+task.planStart).format(startTimeFormat) }}</view>
<view v-else>{{ $moment(+task.planStart).format('D日') }}</view>
<!-- 任务功能菜单 -->
<TaskTools v-if="task.process !== 4" :task="task" />
</view>
</view>
<view class="plugin">
<view class="h-3" v-if="task.process === 4"></view>
<view class="ml-3 overflow-hidden shadow-lg task-box">
<u-card :show-foot="false" :show-head="false" :style="{ height: setHeight(task.panel) }" class="h-16" margin="0" v-if="showSkeleton">
<view slot="body">
<view><skeleton :banner="false" :loading="true" :row="4" animate class="mt-2 u-line-2 skeleton"></skeleton></view>
</view>
</u-card>
<u-card
@click="onClickTask(task.planStart - 0, task.id)"
:style="{ height: setHeight(task.panel) }"
:show-foot="false"
:show-head="false"
class="h-16"
margin="0"
v-if="tasks && tasks.length && task.process !== 4 && !showSkeleton"
>
<template v-slot:body>
<view class="p-0 u-col-between">
<view :key="pIndex" v-for="(row, pIndex) in task.plugins">
<view class="grid gap-2 grid-cols-1" v-if="row.length">
<Plugin
:class="[`row-span-${plugin.row}`, `col-span-${plugin.col}`]"
:task="task"
:key="plugin.pluginTaskId"
:plugin-task-id="plugin.pluginTaskId"
:plugin-id="plugin.pluginId"
:param="plugin.param"
:style-type="data.styleType || 0"
v-for="plugin in row"
/>
</view>
</view>
</view>
</template>
</u-card>
</view>
</view>
</view>
</view>
<!-- 局部弹框操作栏 -->
<Tips />
</view>
</template>
<script setup>
import { useStore } from 'vuex';
import { computed, reactive } from 'vue';
import TimeStatus from './TimeStatus.vue';
import TaskTools from './TaskTools.vue';
import Skeleton from '@/components/Skeleton/Skeleton.vue';
const data = reactive({
currentComponent: '',
styleType: 0,
});
const store = useStore();
const roleId = computed(() => store.state.role.roleId);
const timeUnit = computed(() => store.state.task.timeUnit);
const tasks = computed(() => store.state.task.tasks);
const showSkeleton = computed(() => store.state.task.showSkeleton);
const startTimeFormat = computed(() => store.getters['task/startTimeFormat']);
//
function setHeight(panel) {
if (panel && panel.height) {
return `${panel.height}px`;
}
return 'auto';
}
/**
* 点击了定期任务的面板 更新可变的日常任务
* @param {number} planStart 任务计划开始时间
* @param {string} taskId 任务id
*/
function onClickTask(planStart, taskId) {
const param = { roleId: roleId.value, timeNode: planStart, timeUnit: timeUnit.value };
store.dispatch('task/getGlobal', param);
uni.$storage.setStorageSync('taskId', taskId);
uni.$storage.setStorageSync('roleId', roleId.value);
}
function pinchHandler(evt) {
// evt.scale
console.log(`缩放:${evt.zoom}`);
}
</script>
<style scoped lang="scss">
.task-box {
border-radius: 24rpx;
}
.column {
padding: 24px 14px;
}
.task-column {
height: 33px;
}
.plugin {
margin-top: 8px;
margin-bottom: 8px;
margin-left: 15px;
border-left: 2px solid #d1d5db;
}
::v-deep .ml-2 {
margin-left: 16px;
}
::v-deep .ml-3 {
margin-left: 20px;
}
</style>

317
components/TimeLine/component/TimeStatus.vue

@ -0,0 +1,317 @@
<template>
<view class="u-font-14">
<view
class="flex items-center justify-center rounded-full icon-column"
:style="{ color: orderStyle.color }"
@click="changeStatus(task.process, $event)"
>
<!-- 1进行中 2暂停中 3已完成 -->
<u-circle-progress
:percent="orderStyle.persent - 0"
:active-color="orderStyle.color"
bg-color="rgba(255,255,255,0)"
border-width="4"
:width="task.process !== 4 ? 66 : 50"
v-if="task.process === 1 || task.process === 2 || task.process === 3"
>
<view class="u-progress-content">
<view class="u-progress-dot"></view>
<view class="u-progress-info">
<u-icon :name="orderStyle.icon" v-if="orderStyle.icon" size="15px"></u-icon>
<template v-else>{{ data.durationText }}</template>
</view>
</view>
</u-circle-progress>
<!-- 0未开始 4添加任务 -->
<view class="flex items-center justify-center rounded-full progress-box" v-else :class="task.process === 4 ? 'progress-box-4' : ''">
<view class="u-progress-content">
<view class="u-progress-dot"></view>
<view class="u-progress-info">
<span v-if="orderStyle.icon">
<u-icon :name="orderStyle.icon" v-if="task.process !== 4" size="15px"></u-icon>
<u-icon :name="orderStyle.icon" v-else size="15px"></u-icon>
</span>
<template v-else>{{ data.durationText }}</template>
</view>
</view>
</view>
</view>
<!-- 遮罩 -->
<view class="mask" v-if="data.maskShow" @click="closeMask"></view>
<!-- 新建任务弹窗 -->
<CreateTask
:startTime="data.startTime"
:endTime="data.endTime"
:task="task"
:source="'regular'"
@showTime="showTime"
@closeMask="closeMask"
class="thirdPopup flex transition-transform"
v-if="data.createTaskShow"
/>
<u-picker title="开始时间" mode="time" v-model="data.showStart" :params="data.params" @confirm="confirmStartTime"></u-picker>
<u-picker title="结束时间" mode="time" v-model="data.showEnd" :params="data.params" @confirm="confirmEndTime"></u-picker>
</view>
</template>
<script setup>
import { reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
import CreateTask from '../../Title/components/CreateTask.vue';
const props = defineProps({ task: { type: Object, default: () => {} } });
const data = reactive({
time: '',
start: [{ text: '确认开始任务', color: 'blue' }],
pause: [{ text: '继续' }, { text: '重新开始任务', color: 'blue' }, { text: '结束' }],
proceed: [{ text: '暂停' }, { text: '重新开始任务', color: 'blue' }, { text: '结束' }],
again: [{ text: '重新开始任务', color: 'blue' }],
timer: null,
durationText: 0,
maskShow: false, //
createTaskShow: false, //
startTime: '', //
endTime: '', //
showStart: false,
showEnd: false,
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true,
},
});
const store = useStore();
const tip = computed(() => store.state.task.tip);
const status = computed(() => props.task ? props.task.process : 0);
const taskName = computed(() => props.task ? props.task.name : '');
const taskId = computed(() => props.task ? props.task.id : '');
//
function computeCyclePersent() {
if (!props.task || !props.task.realStart || !props.task.planDuration) return 100;
const { realStart, planDuration } = props.task;
return (((Date.now() - +realStart) * 100) / +planDuration).toFixed(2);
}
const orderStyle = computed(() => {
//
// 0 1 2 3
let color = '#9CA3AF';
let icon = 'play-right-fill';
let persent = 100;
switch (status.value) {
case 1: //
color = '#60A5FA';
icon = '';
if (+computeCyclePersent() > 100) {
persent = 96;
} else {
persent = computeCyclePersent();
}
break;
case 2: //
color = '#F87171';
icon = 'pause';
persent = 50; // TODO:
break;
case 3: //
color = '#34D399';
icon = 'checkmark';
persent = 100;
break;
case 4: //
color = '#60A5FA';
icon = 'plus';
persent = 100;
break;
default:
//
color = '#9CA3AF';
icon = 'play-right';
persent = 100;
break;
}
return { color, icon, persent };
});
// unMounted(() => {
// if (data.timer) {
// clearInterval(data.timer);
// data.timer = null;
// }
// });
/**
* 计算tip的标题内容
*/
function genetateTips(type, content) {
if (type === 0) {
return `确认开始任务"${content}"吗?`;
}
if (type === 3) {
return '是否要重新开始此任务';
}
return '请选择要执行的操作';
}
//
function addTask() {
// uni.$ui.showToast('');
//
data.maskShow = true;
//
data.createTaskShow = true;
}
/**
* 点击了图标 修改任务状态
* @param {object} event
*/
function changeStatus(process, event) {
if (process === 4) {
addTask();
return;
}
// return false;
tip.status = status;
tip.taskId = taskId;
tip.left = event.target.x;
tip.top = event.target.y;
tip.show = true;
tip.text = genetateTips(status, taskName);
store.commit('task/setTip', tip);
}
//
function closeMask() {
//
data.maskShow = false;
//
data.createTaskShow = false;
}
function showTime(type) {
if (type === 1) {
data.showStart = !data.showStart;
} else {
data.showEnd = !data.showEnd;
}
}
//
function confirmStartTime(e) {
data.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
data.showEnd = true;
}
//
function confirmEndTime(e) {
data.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
}
//
// = realStart() + planDuration()
// = -
// = realStart + planDuration - Date.now()
function computeDurationText() {
const { realStart, planDuration } = props.task;
const leftTime = (realStart-0 || 0) + (planDuration-0 || 0) - Date.now(); //
const { num, time } = uni.$time.computeDurationText(leftTime);
if (num <= 0) {
clearInterval(data.timer);
data.timer = null;
}
data.durationText = num;
return time;
}
function updateDurationText(time) {
if (data.timer) {
clearInterval(data.timer);
data.timer = null;
}
if (!time) return;
setInterval(() => {
computeDurationText();
}, time);
}
onMounted(() => {
// TODO:
const time = computeDurationText();
updateDurationText(time);
});
</script>
<style scoped lang="scss">
.icon-column {
height: 33px;
width: 33px;
}
.one {
height: 33px;
width: 33px;
}
.progress-box {
background: rgba(255, 255, 255, 0);
width: 33px;
height: 33px;
border: 2px solid #9ca3af;
}
.progress-box-4 {
width: 25px;
height: 25px;
border: 2px solid #60a5fa;
}
.mask {
width: 100%;
height: 100vh;
z-index: 21;
position: fixed;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.3);
}
.thirdPopup {
background: #ffffff;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 33;
border-radius: 5px;
width: 90%;
}
::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>

0
components/TimeLine/component/Title.vue

82
components/Tips/Tips.vue

@ -0,0 +1,82 @@
<template>
<view
class="fixed shadow-2xl"
style="z-index: 1000"
:style="{
left: tip.left + 'px',
top: data.height - tip.top > 110 ? tip.top + 'px' : '',
bottom: data.height - tip.top > 110 ? '' : '10px',
}"
id="u-icard"
>
<u-card :title="title" style="width: 500rpx; margin: 0 !important" v-if="tip.show" titleSize="28" :headStyle="data.headStyle" :footStyle="data.footStyle">
<view class="" slot="body">{{ tip.text }}</view>
<view class="flex justify-end" slot="foot">
<u-button size="mini" @click="onCancel">取消</u-button>
<u-button v-if="tip.status === 1" size="mini" @click="onChangeStatus(1)">暂停</u-button>
<u-button v-if="tip.status === 2" size="mini" @click="onChangeStatus(2)">继续</u-button>
<u-button v-if="tip.status === 1 || tip.status === 2" size="mini" @click="onChangeStatus(0)">重新开始</u-button>
<u-button v-if="tip.status === 1 || tip.status === 2" type="primary" size="mini" @click="onChangeStatus(3)">结束</u-button>
<u-button v-if="tip.status === 0 || tip.status === 3" type="primary" size="mini" @click="onChangeStatus(0)">确定</u-button>
</view>
</u-card>
</view>
</template>
<script setup>
import { onMounted, computed, reactive } from 'vue';
import { useStore } from 'vuex';
defineProps({ title: { default: '提示', type: String } });
const store = useStore();
const tip = computed(() => store.state.task.tip);
const data = reactive({
footStyle: { padding: '4px 15px' },
headStyle: { paddingTop: '8px', paddingBottom: '8px' },
height: 0,
});
onMounted(() => {
const system = uni.getSystemInfoSync();
data.height = system.windowHeight;
});
/**
* 执行修改任务状态的动作
* @param {number} type 状态码 0开始 1暂停 2继续 3完成 默认0
*/
async function onChangeStatus(type) {
try {
const param = { id: data.tip.taskId, type };
await uni.$u.api.updateTaskType(param);
if (type === 0) {
uni.$ui.showToast('项目已重新开始');
} else if (type === 1) {
uni.$ui.showToast('项目已暂停');
} else if (type === 2) {
uni.$ui.showToast('项目继续');
} else if (type === 3) {
uni.$ui.showToast('项目结束');
}
data.tip.show = false;
// TODO:
// location.reload();
// this.$router.go(0);
} catch (error) {
console.error(error);
uni.$ui.showToast(error.msg || '操作失败');
}
}
//
function onCancel() {
store.commit('task/setTipShow', false);
}
//
// function onConfirm() {
// onCancel();
// }
</script>

223
components/Title/Title.vue

@ -1,8 +1,227 @@
<template>
<theme>
<view>
<!-- :is-back="false" -->
<u-navbar :custom-back="onBack" class="overflow-hidden">
<view class="flex justify-start flex-1 px-3 font-bold min-0">
<view class="truncate">{{ project.name }}</view>
</view>
<view class="mr-2" slot="right">
<u-icon class="m-1" name="xuanzhong2" custom-prefix="custom-icon" size="20px" @click="lwbs"></u-icon>
<u-icon class="m-1" name="shuaxin1" custom-prefix="custom-icon" size="20px" @click="projectOverview"></u-icon>
<u-icon class="m-1" name="home" custom-prefix="custom-icon" size="20px" @click="openIndex"></u-icon>
<u-icon class="m-1" name="xuanxiang" custom-prefix="custom-icon" size="20px" @click="operation"></u-icon>
</view>
</u-navbar>
<view
class="mask"
v-if="data.maskShow"
@click="closeMask"
style="width: 100%; height: 100vh; z-index: 21; position: fixed; background: rgba(0, 0, 0, 0.3)"
></view>
<!-- 右上角 ... 弹窗 -->
<view class="popup border shadow-md" v-if="data.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> -->
</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="data.secondShow" class="second-popup" />
<!-- 新建任务弹窗 -->
<CreateTask
:startTime="data.startTime"
:endTime="data.endTime"
@showTime="showTime"
@closeMask="closeMask"
class="third-popup flex transition-transform"
v-if="data.createTaskShow"
/>
<u-picker title="开始时间" mode="time" v-model="data.showStart" :params="data.params" @confirm="confirmStartTime"></u-picker>
<u-picker title="结束时间" mode="time" v-model="data.showEnd" :params="data.params" @confirm="confirmEndTime"></u-picker>
</view>
</theme>
</template>
<script>
<script setup>
import { reactive, computed } from 'vue';
import { useStore } from 'vuex';
import CreateTask from './components/CreateTask.vue';
import ShareProject from './components/ShareProject.vue';
const data = reactive({
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,
},
});
const store = useStore();
const project = computed(() => store.state.project.project);
const userId = computed(() => store.getters['user/userId']);
function showTime() {
data.showStart = !data.showStart;
}
//
function confirmStartTime(e) {
data.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
data.showEnd = true;
}
//
function confirmEndTime(e) {
data.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`;
}
//
function onBack() {
// eslint-disable-next-line no-undef
const pages = getCurrentPages(); //
console.log('历史pages: ', pages.length);
if (pages.length > 1) {
uni.navigateBack();
} else {
// this.$u.route('/', { u: this.userId });
uni.reLaunch({ url: `/pages/index/index?u=${userId.value}` });
}
}
// LWBS
function lwbs() {
// uni.$ui.showToast('LWBS');
}
//
function projectOverview() {
// uni.$ui.showToast('');
}
//
function openIndex() {
uni.webView.reLaunch({ url: `/pages/index/index?u=${this.userId}` });
}
//
function operation() {
// uni.$ui.showToast('');
data.show = !data.show;
}
//
function createTask() {
// ...
data.show = false;
//
data.maskShow = true;
//
data.createTaskShow = true;
}
//
function share() {
// ...
data.show = false;
//
data.maskShow = true;
//
data.secondShow = true;
}
//
function closeMask() {
//
data.maskShow = false;
//
data.secondShow = false;
//
data.createTaskShow = false;
}
</script>
<style>
<style lang="scss" scoped>
.second-popup {
background: #ffffff;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 33;
border-radius: 5px;
width: 90%;
}
.third-popup {
background: #ffffff;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 33;
border-radius: 5px;
width: 90%;
}
.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>

466
components/Title/components/CreateTask copy.vue

@ -0,0 +1,466 @@
<template>
<div class="new-projects-box">
<div class="form">
<!-- 项目名称 -->
<view class="mb-3 font-bold text-base flex justify-center text-black">新建任务</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) {
uni.$uiowToast('请输入名称');
return;
}
if ((!roleIdList || !roleIdList.length) && !hasRole) {
uni.$uiowToast('请选择负责人');
return;
}
if (!checkerIdList || !checkerIdList.length) {
uni.$uiowToast('请选择检查人');
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;
}
.new-projects-box {
margin-top: 20px;
padding: 15px;
width: 100%;
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;
}
</style>

583
components/Title/components/CreateTask.vue

@ -0,0 +1,583 @@
<template>
<div class="new-projects-box">
<div class="form">
<!-- 项目名称 -->
<view class="new-projects-title font-bold text-base flex justify-center items-center text-black">新建任务</view>
<div class="form-item flex items-center">
<div class="mr-4">名称<span class="text-red-500">*</span></div>
<u-input :maxlength="8" v-model="name" type="text" :inputAlign="'right'" :clearable="false" placeholder="请输入任务名称" />
</div>
<!-- 开始时间 -->
<div class="form-item flex items-center">
<div class="mr-4">开始时间<span class="text-red-500" v-if="source === 'regular'">*</span></div>
<div class="flex justify-end items-center flex-1">
<u-input
placeholder="请选择开始时间"
v-model="startTime"
type="text"
:inputAlign="'right'"
:clearable="false"
@click="$emit('showTime', 1)"
/>
<u-icon class="ml-1" name="arrow-right" color="#969799" size="28"></u-icon>
</div>
</div>
<!-- 结束时间 -->
<div class="form-item flex items-center">
<div class="mr-4">结束时间<span class="text-red-500" v-if="source === 'regular'">*</span></div>
<div class="flex justify-end items-center flex-1">
<u-input
placeholder="请选择结束时间"
v-model="endTime"
type="text"
:inputAlign="'right'"
:clearable="false"
@click="$emit('showTime', 2)"
/>
<u-icon class="ml-1" name="arrow-right" color="#969799" size="28"></u-icon>
</div>
</div>
<!-- 多选框 -->
<div class="form-item flex justify-between items-center">
<div class="mr-4">负责人<span class="text-red-500">*</span></div>
<div class="flex-1 text-right" 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>{{ 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="form-item flex items-center">
<div class="mr-4">描述</div>
<u-input v-model="description" :maxlength="48" type="text" :inputAlign="'right'" :clearable="false" />
</div>
<!-- 所属项目 -->
<div class="form-item flex items-center">
<div class="mr-4">所属项目<span class="text-red-500">*</span></div>
<div class="flex-1 text-right">{{ project.name }}</div>
</div>
<!-- 所属任务 -->
<div class="form-item flex items-center" v-if="task && task.id">
<div class="mr-4">所属任务</div>
<div class="flex-1 text-right">{{ task.name }}</div>
</div>
<!-- 上道工序 -->
<div class="form-item flex items-center">
<div class="mr-4">上道工序</div>
<InputSearch
@searchPrevTask="searchPrevTask"
:dataSource="allTasks"
@select="handleChange"
@clearAllTasks="clearAllTasks"
placeholder="请输入上道工序"
/>
</div>
<!-- 检查人多选框 -->
<div class="form-item flex justify-between items-center">
<div class="mr-4 flex-shrink-0">检查人<span class="text-red-500">*</span></div>
<div label="检查人" class="flex-1" style="width: calc(100% - 65px)">
<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>{{ 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="form-item flex justify-between items-center" v-if="!source">
是否是日常任务
<u-switch v-model="isGlobal" size="28"></u-switch>
</div>
<div class="form-item flex justify-between items-center" v-for="(sort, sortIndex) in deliverSort" :key="sortIndex">
<div class="mr-4">{{ `交付物${sortIndex + 1}` }}</div>
<div class="flex-1">
<u-input
@blur="addDeliverInput"
v-model="sort.name"
:placeholder="`交付物名称${sortIndex + 1}`"
type="text"
:inputAlign="'right'"
:clearable="false"
/>
</div>
</div>
<!-- <div class="form-item flex items-center">
<div class="mr-4">添加插件</div>
<u-input :maxlength="8" v-model="name" type="text" :inputAlign="'right'" :clearable="false" placeholder="选择插件" />
</div> -->
<div class="form-btn flex items-center mt-4">
<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,
},
source: {
type: String,
default: '',
},
},
data() {
return {
// arrow: true,
// show: false,
isGlobal: false, //
name: '', //
// showChooseTime: false,
// timeValue: '', //
// taskStartTime: '', //
// taskEndTime: '', //
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() {
if (this.source) {
this.isGlobal = 0;
}
const {
projectId,
task,
name,
startTime,
endTime,
hasRole,
roleIdList,
roleId,
description,
processTaskId,
checkerIdList,
isGlobal,
} = this;
if (!name) {
uni.$ui.showToast('请输入任务名称');
return;
}
if (!isGlobal) {
if (!startTime) {
uni.$ui.showToast('定期任务时间不能为空');
return;
}
}
if (startTime && endTime) {
let start = startTime ? this.$moment(startTime).format('x') - 0 : '';
let end = endTime ? this.$moment(endTime).format('x') - 0 : '';
if (start > end) {
uni.$ui.showToast('结束时间不能小于开始时间');
return;
}
}
if ((!roleIdList || !roleIdList.length) && !hasRole) {
uni.$ui.showToast('请选择负责人');
return;
}
if (!checkerIdList || !checkerIdList.length) {
uni.$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.process !== 4 && 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.task.process === 4) {
// this.addNewTasks(newTasks);
// }
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;
//
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) {
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 32rpx;
}
// ::v-deep.u-input--border {
// border: none;
// border-radius: 0;
// padding: 0 !important;
// }
::v-deep.u-dropdown__menu__item > uni-view {
border: none !important;
padding: 5px;
}
// .u-input {
// border-bottom: 1px solid #dcdfe6;
// }
.new-projects-box {
margin-top: 20px;
// padding: 15px;
width: 100%;
overflow: hidden;
color: #595959;
}
.new-projects-title {
height: 60px;
}
.form-item {
padding: 0 16px;
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.form-item ::v-deep .input-group uni-input {
border: 0;
font-size: 14px;
text-align: right;
}
.form-btn {
height: 60px;
}
.form-btn .u-btn {
width: 156px;
height: 32px;
font-size: 16px;
border-radius: 4px;
}
.w {
width: 300px;
height: 39px;
}
::v-deep .u-dropdown__menu {
height: 48px !important;
}
::v-deep .u-dropdown__menu__item {
width: 100%;
}
::v-deep .u-dropdown__menu__item .u-flex {
border: 0 !important;
// border-bottom: 1px solid #dcdfe6 !important;
padding: 0 !important;
}
::v-deep .u-dropdown__menu__item__arrow {
margin-left: 10px;
flex-shrink: 0;
}
::v-deep .u-dropdown__menu__item__text {
width: calc(100% - 23px);
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
::v-deep .u-dropdown__content {
top: 48px !important;
box-shadow: 0 4px 6px 1px rgba(0, 0, 0, 0.1) !important;
}
</style>

210
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() {
uni.$uiowToast('复制成功');
},
//
copyError() {
uni.$uiowToast('复制失败,请稍后重试');
},
/**
* 创建分享链接
* @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>

46
components/Upload/Upload.vue

@ -0,0 +1,46 @@
<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>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import Config from '@/common/js/config.js';
const emit = defineEmits(['success', 'error']);
const store = useStore();
const userId = computed(() => store.getters['user/userId']);
// wbs
const handleUpload = async cur => {
try {
const res = await uni.$u.api.import();
// WBS
//
emit('success');
const { apiUrl } = Config;
const defaultwbs = `${apiUrl}/defaultwbs`;
res.url && (defaultwbs = res.url);
setTimeout(() => {
uni.navigateTo({ url: `/pages/project/project?u=${userId.value}&p=${res.id}&pname=${res.pname}&url=${res.url}` });
}, 2000);
} catch (error) {
console.error('error: ', error);
emit('error', error);
}
};
</script>
<style lang="scss" scoped>
.upload {
position: absolute;
right: 10px;
bottom: 0;
transform: translate3d(0, 50%, 0);
color: $uni-color-primary !important;
}
</style>

22
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
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
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
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
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
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>

97
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
config/task.js

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

17
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: '' },
],
};

194
hooks/project/useGetTasks.js

@ -0,0 +1,194 @@
import { computed, nextTick } from 'vue';
import { useStore } from 'vuex';
import { flatten } from 'lodash';
import dayjs from 'dayjs';
export default function useGetTasks() {
const store = useStore();
const tasks = computed(() => store.state.task.tasks);
const roleId = computed(() => store.state.role.roleId);
const timeNode = computed(() => store.state.task.timeNode);
const timeUnit = computed(() => store.state.task.timeUnit);
const projectId = computed(() => store.getters['project/projectId']);
const timeGranularity = computed(() => store.getters['task/timeGranularity']);
// 初始化 定期任务
async function initPlanTasks() {
setPrevPlaceholderTasks(); // 向上加载空数据
setNextPlaceholderTasks(); // 向下加载空数据
await getInitTasks(); // 获取初始数据
}
// 切换了 颗粒度 || 角色时候 获取初始定期任务
function getInitTasks() {
// 预加载 上下的定期任务
function preloadFn(that) {
const detailId = tasks.value.findIndex(task => task.detailId);
const arr = [];
tasks.value.forEach(task => {
if (task.detailId) {
arr.push(task);
}
});
if (detailId !== -1) {
// 只要有1个真实的任务 就预加载上下周期的任务
const { pageCount } = uni.$task;
nextTick(() => {
// 向上拿数据
getTasks({
timeNode: +tasks.value[detailId].planStart,
queryType: 0,
queryNum: pageCount,
});
// 向下拿数据
const nextQueryTime = +uni.$time.add(+arr[arr.length - 1].planStart, 1, timeGranularity.value);
getTasks({
timeNode: nextQueryTime,
queryType: 1,
queryNum: pageCount,
});
});
} else {
// 没有任务 上下显示时间刻度
// 向上加载
setPrevPlaceholderTasks();
// // 向下加载
setNextPlaceholderTasks();
}
}
// 根据时间基准点和角色查找定期任务
getTasks({
queryType: 0,
}); // 向上获取定期任务数据
// 根据项目id获取角色列表
// 向下获取定期任务数据
getTasks(
{
queryType: 1,
},
preloadFn,
);
}
/**
* 生成getTasks所用的参数
* @param {object} query getTasks传递的参数
*/
function generateGetTaskParam(query) {
return {
roleId: roleId.value,
timeNode: query.timeNode || timeNode.value,
timeUnit: query.timeUnit || timeUnit.value,
queryNum: query.queryNum || 3,
queryType: query.queryType,
projectId: projectId.value,
};
}
/**
* 根据时间基准点和角色查找定期任务
* @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向下查找(默认) 下查包含自己上查不包含
*/
function getTasks(query, fn) {
store.commit('task/setShowSkeleton', false);
const params = generateGetTaskParam(query);
uni.$catchReq.getRegularTask(params, (err, data) => {
store.commit('task/setShowSkeleton', false);
if (err) {
// TODO: 提示错误
console.error('err: ', err);
} else {
store.commit('task/setShowScrollTo', true);
// 有数据用数据替换刻度
// 没有数据 继续加载刻度
if (data && data.length) {
replacePrevData(data, params.queryType);
params.queryType === 0 ? store.commit('task/setTopEnd', false) : store.commit('task/setBottomEnd', false);
} else {
// TODO: 0 -> 向上 1 -> 向下
params.queryType === 0 ? setPrevPlaceholderTasks() : setNextPlaceholderTasks();
}
if (tasks.value.length && fn) {
fn(this);
}
}
});
}
/**
* 用拿到的新数据 替换 时间刻度/旧数据
* 先对比 新旧数据的 始末时间 补齐刻度
* 再遍历对比 用任务替换刻度
* @param {array} data 服务端返回的新数据 上边已经处理过空值
* @param {number} type 0 -> 向上 1->向下
*/
function replacePrevData(data, type) {
const obj = { tasks: tasks.value, data, timeGranularity: timeGranularity.value };
let oldTasks = fillPlaceholderTask(obj); // 已经上下补齐时间刻度的
// 遍历对比 用任务替换刻度
// TODO: tasks越来越多 遍历越来越多 需要优化
oldTasks.forEach((taskItem, index) => {
const arr = data.filter(dataItem => dayjs(+dataItem.planStart).isSame(+taskItem.planStart, timeGranularity.value));
if (arr && arr.length) {
oldTasks.splice(index, 1, [...arr]); // 这里加入的数据是array类型的, [{},{},[],[],{}]
}
});
oldTasks = flatten(oldTasks); // 1维拍平
store.commit('task/clearTasks');
type === 0 ? store.commit('task/setUpTasks', oldTasks) : store.commit('task/setDownTasks', oldTasks);
}
/**
* 超出旧数据上下限 补齐时间刻度到新数据的起始时间颗粒度
*/
function fillPlaceholderTask(obj) {
const { prev, next } = uni.$task.computeFillPlaceholderTaskCount(obj);
if (prev) {
const newTasks = uni.$task.setPlaceholderTasks(+obj.tasks[0].planStart, true, obj.timeGranularity, prev);
store.commit('task/setUpTasks', newTasks);
}
if (next) {
const newTasks = uni.$task.setPlaceholderTasks(+obj.tasks[tasks.length - 1].planStart, false, obj.timeGranularity, next);
store.commit('task/setDownTasks', newTasks);
}
return tasks.value;
}
// 设置时间轴向上的空数据
function setPrevPlaceholderTasks() {
store.commit('task/setTopEnd', true);
let startTime = '';
if (!tasks.value || !tasks.value.length) {
startTime = Date.now(); // 没有任务就应该是时间基准点
} else {
startTime = tasks.value[0].planStart - 0; // 有任务就是第一个任务的计划开始时间
}
const placeholderTasks = uni.$task.setPlaceholderTasks(startTime, true, timeGranularity.value);
store.commit('task/setUpTasks', placeholderTasks);
}
// 设置时间轴向下的空数据
function setNextPlaceholderTasks() {
store.commit('task/setBottomEnd', true);
let startTime = '';
if (!tasks.value || !tasks.value.length) {
startTime = Date.now();
} else {
startTime = +tasks.value[tasks.value.length - 1].planStart;
}
const initData = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
store.commit('task/setDownTasks', initData);
}
return {
initPlanTasks,
getTasks,
};
}

122
hooks/project/useInit.js

@ -0,0 +1,122 @@
import { computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { useStore } from 'vuex';
export default function useInit() {
const store = useStore();
const token = computed(() => store.state.user.token);
onLoad(options => {
if (options.share && options.share === '1') {
shareInit(options);
} else {
init(options);
}
});
// onMounted(() => {
// const system = uni.getSystemInfoSync();
// height.value = `${system.windowHeight}px`;
// });
/**
* 通过项目id获取项目信息
* @param {object} params 提交的参数
*/
async function getProjectById(params) {
try {
const data = await uni.$u.api.findProjectById(params);
store.commit('project/setProject', data);
// 根据项目id获取角色列表
getRoles(params);
} catch (error) {
console.log('error: ', error || '获取项目信息失败');
}
}
/**
* 通过项目id获取角色信息
* @param {string} projectId
* @param {object} params 提交的参数
*/
function getRoles(params) {
uni.$catchReq.findShowRole(params, (err, data) => {
if (err) {
console.error('err: ', err || '获取角色信息失败');
} else {
store.commit('role/setInvisibleRoles', data ? data.invisibleList : []);
store.commit('role/setVisibleRoles', data ? data.visibleList : []);
setInitialRoleId(data ? data.visibleList : []);
}
});
}
// 设置 初始显示角色信息
function 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 = uni.$storage.getStorageSync('roleId');
const currentRoleId = storageRoleId || (currentRole ? currentRole.id : '');
store.commit('role/setRoleId', currentRoleId);
// 清空storage
uni.$storage.setStorageSync('roleId', '');
}
/**
* 初始化
* @param {object | null} options
*/
function init(options) {
// 参数里有项目名称 就设置标题里的项目名称
options && options.pname && store.commit('project/setProjectName', options.pname);
if (!options || !options.p) {
uni.$ui.showToast('缺少项目信息参数'); // 没有项目id参数
} else {
if (options.p !== uni.$storage.getStorageSync('projectId')) {
console.log('切项目了');
uni.$storage.setStorageSync('roleId', '');
}
// 根据项目id获取项目信息
const params = { projectId: options.p, num: 0 };
getProjectById(params);
// 根据项目id获取成员列表
store.dispatch('role/getAllMembers', { projectId: options.p });
}
}
// 分享链接来的初始化
async function shareInit(options) {
const storageUser = uni.$storage.getStorageSync('user');
const user = storageUser ? JSON.parse(storageUser) : null;
if (user && user.id) {
await store.dispatch('user/getToken', user.id);
const res = await clickShare({ code: options.shareId });
if (res && res.projectId) {
let query = { ...uni.$route.query };
query = { u: user.id, p: res.projectId };
uni.$router.push({ path: uni.$route.path, query });
init(query);
}
} else {
uni.$ui.showToast('缺少用户信息参数,请登录');
}
}
/**
* 点击分享连接
* @param {any} commit
* @param {object} param 请求参数
*/
async function clickShare(param) {
try {
const data = await uni.$catchReq.clickShare(param);
return data;
} catch (error) {
uni.$ui.showToast(error.msg || '获取失败');
}
}
return { init };
}

7
hooks/theme/useTheme.js

@ -0,0 +1,7 @@
import { computed } from 'vue';
import { useStore } from 'vuex';
export default function useTheme() {
const store = useStore();
const theme = computed(() => store.state.theme);
return theme;
}

37
hooks/user/useGetToken.js

@ -0,0 +1,37 @@
import { computed } from 'vue';
import { useStore } from 'vuex';
/**
* 初始化
* token userId处理
* 1.1 store里有token 且没过期直接使用store的token
* 1.2 store里的token不可用 查localStorage
* 因为一开始就将local的数据同步到了store里所以不用管local的数据了
* 2. store里token不可用 查userId 通过store里的userId获取token
* url local的userId 一开始就同步到了store里所以不用考虑
* @param {object | null} options
*/
export default async function useGetToken() {
const store = useStore();
const token = computed(() => store.state.user.token);
const tokenIsAvailable = computed(() => store.getters['user/tokenIsAvailable']); // token是否可用
const userId = computed(() => store.getters['user/userId']);
debugger;
if (token.value && tokenIsAvailable.value) {
// 1.1 store里有token 且没过期直接:使用store的token
return token.value;
} else {
// 2. 根据userId获取token
if (userId.value) {
try {
const { token } = await store.dispatch('user/getTokenByUserId', userId.value);
return token;
} catch (error) {
console.error('error: ', error);
return null;
}
} else {
return null;
}
}
}

10
hooks/user/useGetUserIdFromLocal.js

@ -0,0 +1,10 @@
// 获取本地localStorage里的userId
export default function useGetUserIdFromLocal() {
try {
const userLocal = uni.$storage.getStorageSync('user');
const user = JSON.parse(userLocal);
return user.id;
} catch (error) {
return null;
}
}

285
hooks/user/userMixin - 副本.js

@ -0,0 +1,285 @@
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import clipboard from "@/common/js/dc-clipboard/clipboard.js"
export default function mixinInit {
const store = useStore();
const user = computed(() => store.state.user.user);
const rules = ref({
phone: [
{
required: true,
message: '请输入手机号',
trigger: ['change','blur'],
},
{
validator: (rule, value, callback) => {
// 调用uView自带的js验证规则,详见:https://www.uviewui.com/js/test.html
return this.$u.test.mobile(value);
},
message: '手机号码不正确',
// 触发器可以同时用blur和change,二者之间用英文逗号隔开
trigger: ['change','blur'],
}
],
verificationCodeValue: [
{
required: true,
message: '请输入图形验证码',
trigger: ['change','blur'],
},
{
type: 'number',
message: '图形验证码只能为数字',
trigger: ['change','blur'],
}
],
smsCode: [
{
required: true,
message: '请输入验证码',
trigger: ['change','blur'],
},
{
type: 'number',
message: '验证码只能为数字',
trigger: ['change','blur'],
}
],
account: [
{
required: true,
message: '请输入用户名',
trigger: ['change','blur'],
},
{
min: 2,
max: 20,
message: '用户名长度在2到20个字符',
trigger: ['change','blur'],
},
{
pattern: /^[a-zA-Z0-9._-]{2,20}$/,
message: '请输入2-20位字母、数字、汉字或字符"_ - ."',
trigger: ['change','blur'],
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['change','blur'],
},
{
min: 6,
max: 20,
message: '密码长度在6到20个字符',
trigger: ['change','blur'],
},
{
// 正则不能含有两边的引号
pattern: /^[a-zA-Z0-9._-]{6,20}$/,
message: '请输入6-20位字母、数字、汉字或字符"_ - ."',
trigger: ['change','blur'],
}
],
});
const errorType = ref(['message']);
const labelPosition = ref('left');
const border = ref(false);
const smsCode = ref(''); // 短信验证码
const showInterval = ref(false);
const interval = ref(120);
const codeTimer = ref(null);
const showPaste = ref(false);
// 获取图形验证码
async function getImageCode() {
console.log('5555')
uni.$ui.showLoading();
try {
const data = await uni.$u.api.getImageCode();
const { imageBase64, verificationCodeId } = data;
imageBase64 = imageBase64 || '';
verificationCodeId = verificationCodeId || '';
uni.$ui.hideLoading();
} catch (error) {
uni.$ui.hideLoading();
uni.$ui.showToast(error);
}
}
return {
// errorType,
// rules,
// labelPosition,
// border,
getImageCode,
// hasvalue,
// getCode,
// getCodeInterval,
// checkRules,
// setCode,
// getClipboardContents,
// verifyPhone,
// verifyLoginname,
// handleWxLogin
}
}
// const mixin = {
// computed: mapState('user', ['user']),
// onReady() {
// this.$refs.uForm.setRules(this.rules);
// },
// methods: {
// ...mapActions('user', ['sendCode']),
// 获取图形验证码
// async getImageCode() {
// this.$util.showLoading();
// try {
// const data = await uni.$u.api.getImageCode();
// const { imageBase64, verificationCodeId } = data;
// this.imageBase64 = imageBase64 || '';
// this.verificationCodeId = verificationCodeId || '';
// uni.hideLoading();
// } catch (error) {
// uni.hideLoading();
// uni.$ui.showToast(error);
// }
// },
// //有图片验证码的值
// hasvalue() {
// if(this.model.smsCode || this.model.showPaste) return
// if (!this.verifyPhone(this.model.phone)) {
// uni.$ui.showToast('请输入正确的手机号');
// return;
// }
// if (!this.model.verificationCodeValue) {
// uni.$ui.showToast('请输入图形验证码');
// return;
// }
// this.getCode();
// },
// // 获取验证码
// async getCode() {
// try {
// const { phone, verificationCodeValue } = this.model;
// const { verificationCodeId } = this;
// if (!verificationCodeId || !verificationCodeValue) {
// uni.$ui.showToast('缺少图形验证码参数');
// return;
// }
// const params = {
// phone,
// verificationCodeId,
// verificationCodeValue,
// };
// const date = await store.dispatch('user/sendCode', params);
// getCodeInterval();
// showPaste.value = true;
// } catch (err) {
// throw err;
// }
// },
// // 获取验证码倒计时
// getCodeInterval() {
// this.showInterval = true;
// this.codeTimer = setInterval(() => {
// if (this.interval === 0) {
// clearInterval(this.codeTimer);
// this.codeTimer = null;
// this.showInterval = false;
// this.interval = 120;
// return;
// }
// this.interval = this.interval - 1;
// }, 1000);
// },
// // 验证信息
// checkRules() {
// // const { smsCode, phone, user } = this;
// if (!this.verifyPhone(phone.value)) {
// uni.$ui.showToast('请输入正确的手机号');
// return false;
// }
// if (!smsCode.value) {
// uni.$ui.showToast('验证码无效');
// return false;
// }
// if (phone.value === user.value.phone) {
// uni.$ui.showToast('新手机号不能与旧手机号相同');
// return;
// }
// return true;
// },
// // 粘贴
// setCode() {
// // 获取粘贴板内容
// // 小程序平台
// //#ifdef MP-WEIXIN
// var _this = this
// uni.getClipboardData({
// success (res) {
// _this.smsCode = res.data;
// }
// });
// //#endif
// // 非小程序平台
// //#ifndef MP-WEIXIN
// this.getClipboardContents()
// //#endif
// },
// // 非小程序平台粘贴
// async getClipboardContents() {
// try {
// const text = await navigator.clipboard.readText();
// this.smsCode = text;
// } catch (err) {
// console.error('Failed to read clipboard contents: ', err);
// }
// },
// /**
// * 验证手机号格式
// * @param {string} phone 手机号
// */
// verifyPhone(phone) {
// const phoneExg = /^1\d{10}$/;
// return phoneExg.test(phone);
// },
// /**
// * 验证账号/密码 格式
// * @param {string} account 账号
// */
// verifyLoginname(account) {
// const accountExg = /^[a-zA-Z0-9._-]{2,20}$/;
// return accountExg.test(account);
// },
// // 微信登录
// handleWxLogin() {
// const origin = 'https://test.tall.wiki/pt-mui'; // 测试
// const appid = 'wxd1842e073e0e6d91';
// const state = 'wx_web';
// const href = 'https://open.weixin.qq.com/connect/qrconnect';
// // eslint-disable-next-line
// window.location.href =
// `${href}?appid=${appid}&redirect_uri=${origin}&response_type=code&scope=snsapi_login&state=${state}#wechat_redirect`;
// },
// // }
// };

231
hooks/user/userMixin.js

@ -0,0 +1,231 @@
import { ref, computed, reactive } from 'vue';
import { useStore } from 'vuex';
import { onReady } from '@dcloudio/uni-app';
import clipboard from "@/common/js/dc-clipboard/clipboard.js";
import Config from '@/common/js/config.js'
export default function userMixin() {
const store = useStore();
const user = computed(() => store.state.user.user);
const rules = {
phone: [{
required: true,
message: '请输入手机号',
trigger: ['change', 'blur'],
},
{
validator: (rule, value, callback) => {
// 调用uView自带的js验证规则,详见:https://www.uviewui.com/js/test.html
return this.$u.test.mobile(value);
},
message: '手机号码不正确',
// 触发器可以同时用blur和change,二者之间用英文逗号隔开
trigger: ['change', 'blur'],
}
],
verificationCodeValue: [{
required: true,
message: '请输入图形验证码',
trigger: ['change', 'blur'],
},
{
type: 'number',
message: '图形验证码只能为数字',
trigger: ['change', 'blur'],
}
],
smsCode: [{
required: true,
message: '请输入验证码',
trigger: ['change', 'blur'],
},
{
type: 'number',
message: '验证码只能为数字',
trigger: ['change', 'blur'],
}
],
account: [{
required: true,
message: '请输入用户名',
trigger: ['change', 'blur'],
},
{
min: 2,
max: 20,
message: '用户名长度在2到20个字符',
trigger: ['change', 'blur'],
},
{
pattern: /^[a-zA-Z0-9._-]{2,20}$/,
message: '请输入2-20位字母、数字、汉字或字符"_ - ."',
trigger: ['change', 'blur'],
}
],
password: [{
required: true,
message: '请输入密码',
trigger: ['change', 'blur'],
},
{
min: 6,
max: 20,
message: '密码长度在6到20个字符',
trigger: ['change', 'blur'],
},
{
// 正则不能含有两边的引号
pattern: /^[a-zA-Z0-9._-]{6,20}$/,
message: '请输入6-20位字母、数字、汉字或字符"_ - ."',
trigger: ['change', 'blur'],
}
],
};
const smsCode = ref(null); // 短信验证码
// const showInterval = ref(false);
// const interval = ref(120);
const codeTimer = ref(null);
// const showPaste = ref(false);
const dataObj = reactive({
showInterval: false,
interval: 120,
showPaste: false
})
//有图片验证码的值
function hasvalue(form) {
if(form.smsCode || dataObj.showPaste) return
if (!verifyPhone(form.phone)) {
uni.$ui.showToast('请输入正确的手机号');
return;
}
if (!form.verificationCodeValue) {
uni.$ui.showToast('请输入图形验证码');
return;
}
getCode(form);
}
// 获取验证码
async function getCode(form) {
try {
if (!form.verificationCodeId || !form.verificationCodeValue) {
uni.$ui.showToast('缺少图形验证码参数');
return;
}
const params = {
phone: form.phone,
verificationCodeId: form.verificationCodeId,
verificationCodeValue: form.verificationCodeValue,
};
const date = await store.dispatch('user/sendCode', params);
getCodeInterval();
dataObj.showPaste = true;
} catch (err) {
throw err;
}
}
// 获取验证码倒计时
function getCodeInterval() {
dataObj.showInterval = true;
codeTimer.value = setInterval(() => {
if (dataObj.interval === 0) {
clearInterval(codeTimer.value);
codeTimer.value = null;
dataObj.showInterval = false;
dataObj.interval = 120;
return;
}
dataObj.interval = dataObj.interval - 1;
}, 1000);
}
// 粘贴
function setCode() {
// 获取粘贴板内容
// 小程序平台
//#ifdef MP-WEIXIN
uni.getClipboardData({
success (res) {
smsCode.value = res.data;
}
});
//#endif
// 非小程序平台
//#ifndef MP-WEIXIN
getClipboardContents()
//#endif
}
// 非小程序平台粘贴
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
smsCode.value = text;
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
// 验证信息
function checkRules() {
if (!verifyPhone(phone.value)) {
uni.$ui.showToast('请输入正确的手机号');
return false;
}
if (!smsCode.value) {
uni.$ui.showToast('验证码无效');
return false;
}
if (phone.value === user.value.phone) {
uni.$ui.showToast('新手机号不能与旧手机号相同');
return;
}
return true;
}
/**
* 验证手机号格式
* @param {string} phone 手机号
*/
function verifyPhone(phone) {
const phoneExg = /^1\d{10}$/;
return phoneExg.test(phone);
}
/**
* 验证账号/密码 格式
* @param {string} account 账号
*/
function verifyLoginname(account) {
const accountExg = /^[a-zA-Z0-9._-]{2,20}$/;
return accountExg.test(account);
}
// 微信登录
function handleWxLogin() {
const origin = `${Config.baseUrl}/pt-mui`; // 测试
const appid = 'wxd1842e073e0e6d91';
const state = 'wx_web';
const href = 'https://open.weixin.qq.com/connect/qrconnect';
// eslint-disable-next-line
window.location.href =
`${href}?appid=${appid}&redirect_uri=${origin}&response_type=code&scope=snsapi_login&state=${state}#wechat_redirect`;
}
return {
rules,
// showPaste,
// showInterval,
// interval,
dataObj,
hasvalue,
checkRules,
setCode,
verifyLoginname,
handleWxLogin
}
}

76
main.js

@ -1,23 +1,63 @@
import App from './App'
import App from './App';
import cache from '@/utils/cache.js';
import cacheAndRequest from '@/utils/cacheAndRequest.js';
import { createSSRApp } from 'vue';
import { setupDayjs } from '@/utils/dayjs.js';
import { setupHttp } from '@/utils/request.js';
import { setupProject } from '@/apis/project.js';
import { setupRole } from '@/apis/role.js';
import { setupTall } from '@/apis/tall.js';
import { setupTask } from '@/apis/task.js';
import { setupWbs } from '@/apis/wbs.js';
import { setupPlugin } from '@/apis/plugin.js';
import storage from '@/utils/storage.js';
import store from './store';
import task from '@/utils/task.js';
import time from '@/utils/time.js';
import timeConfig from '@/config/time';
import taskConfig from '@/config/task';
import pluginConfig from '@/config/plugin';
import uView from './uni_modules/vk-uview-ui'; // 引入 uView UI
import ui from '@/utils/ui.js';
import upload from '@/utils/upload.js';
// #ifndef VUE3
import Vue from 'vue'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(uView) // 使用 uView UI
const app = createSSRApp(App);
app.config.globalProperties.$cache = cache;
app.config.globalProperties.$catchReq = cacheAndRequest;
app.config.globalProperties.$storage = storage;
app.config.globalProperties.$time = time;
app.config.globalProperties.$ui = ui;
app.config.globalProperties.$upload = upload;
app.config.globalProperties.$task = task;
app.config.globalProperties.$timeConfig = timeConfig;
app.config.globalProperties.$taskConfig = taskConfig;
app.config.globalProperties.$pluginConfig = pluginConfig;
uni.$cache = cache;
uni.$catchReq = cacheAndRequest;
uni.$storage = storage;
uni.$time = time;
uni.$ui = ui;
uni.$upload = upload;
uni.$task = task;
uni.$timeConfig = timeConfig;
uni.$taskConfig = taskConfig;
uni.$pluginConfig = pluginConfig;
setupDayjs(app);
app.use(uView); // 使用 uView UI
app.use(store);
setupHttp(app);
setupTall(app);
setupProject(app);
setupRole(app);
setupTask(app);
setupWbs(app);
setupPlugin(app);
return {
app
}
app,
};
}
// #endif

40
manifest.json

@ -1,6 +1,6 @@
{
"name" : "tall-4-project",
"appid" : "__UNI__1EC8558",
"name" : "时物链条",
"appid" : "__UNI__3CBCFFF",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
@ -43,7 +43,41 @@
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
"sdkConfigs" : {
"ad" : {}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
}
},
/* */

115
package.json

@ -1,54 +1,61 @@
{
"name": "tall-4",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"commitizen": "^4.2.4",
"commitlint": "^16.0.1",
"conventional-changelog": "^3.1.25",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.2.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
"prettier": "^2.5.1",
"right-pad": "^1.0.1",
"vue-cli-plugin-commitlint": "^1.0.12"
},
"browserslist": [
"Android >= 4",
"ios >= 8"
],
"config": {
"commitizen": {
"path": "./node_modules/vue-cli-plugin-commitlint/lib/cz"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,json,css,vue}": [
"eslint --fix",
"git add"
],
"*.js": "eslint --cache --fix"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"cz": "npm run log && git add . && git cz",
"log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0"
},
"author": "",
"license": "ISC"
}
{
"name": "tall-4",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {
"axios": "^0.24.0",
"dayjs": "^1.10.7",
"lodash": "^4.17.21",
"qs": "^6.10.2"
},
"devDependencies": {
"@dcloudio/uni-app": "^3.0.0-alpha-3000020210521001",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"commitizen": "^4.2.4",
"commitlint": "^16.0.1",
"conventional-changelog": "^3.1.25",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.2.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
"prettier": "^2.5.1",
"right-pad": "^1.0.1",
"vue-cli-plugin-commitlint": "^1.0.12"
},
"browserslist": [
"Android >= 4",
"ios >= 8"
],
"config": {
"commitizen": {
"path": "./node_modules/vue-cli-plugin-commitlint/lib/cz"
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,json,css,vue}": [
"eslint --fix",
"git add"
],
"*.js": "eslint --cache --fix"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"cz": "npm run log && git add . && git cz",
"fix": "eslint --fix",
"log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0"
},
"author": "",
"license": "ISC"
}

35
pages.json

@ -3,14 +3,45 @@
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
"navigationBarText": "TALL"
}
},
{
"path": "pages/project/project",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white"
}
},
//
{
"path": "pages/user/accountLogin",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white"
}
},
//
{
"path": "pages/user/login",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarTitleText": "TALL",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^p-(.*)": "@/plugins/p-$1/p-$1.vue",
"theme": "@/components/Theme/Theme.vue"
}
}
}

179
pages/index/index.vue

@ -1,66 +1,141 @@
<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 class="flex flex-col h-full bg-gray-50" @click="openAuth"> -->
<view class="flex flex-col h-full bg-gray-50">
<view class="relative" @touchmove="onMove">
<!-- 日历 -->
<Calendar @selected-change="onDateChange" :show-back="true" ref="calendar" @handleFindPoint="handleFindPoint" />
<!-- 上传 导入wbs -->
<Upload @success="onUploadSuccess" @error="onUploadError" />
</view>
<u-button @click="toLogin">登录</u-button>
<!-- 项目列表 -->
<Projects @getProjects="getProjects" class="flex-1 overflow-y-auto" />
<!-- 全局提示框 -->
<u-top-tips ref="uTips"></u-top-tips>
</view>
</template>
<script setup>
import {
ref, onMounted
} from 'vue';
import Navbar from '@/components/Title/Title.vue';
import Roles from '@/components/Roles/Roles.vue';
import Globals from '@/components/Globals/Globals.vue';
import TimeLine from '@/components/TimeLine/TimeLine.vue';
import { reactive, computed, watchEffect, ref } from 'vue';
import { useStore } from 'vuex';
import dayjs from 'dayjs';
const store = useStore();
const token = computed(() => store.state.user.token);
const uTips = ref(null);
const data = reactive({
calendar: null,
days: [],
});
// token
watchEffect(() => {
if (!token.value) return;
if (token.value) {
getProjects();
handleFindPoint();
}
});
let height = ref(null);
//
function getProjects(start = dayjs().startOf('day').valueOf(), end = dayjs().endOf('day').valueOf()) {
// const data = await this.$u.api.getProjects(start, end);
uni.$catchReq.getProjects(start, end, (err, data) => {
if (err) {
console.error('err: ', err);
} else {
data.forEach(item => {
item.show = false;
});
store.commit('project/setProjects', data);
}
});
}
onMounted(() => {
const system = uni.getSystemInfoSync();
height.value = system.windowHeight + 'px';
});
async function handleFindPoint(start, end) {
try {
const startTime = start || dayjs().startOf('month').valueOf();
const endTime = end || dayjs().endOf('month').valueOf();
const res = await uni.$u.api.findRedPoint(startTime, endTime);
store.commit('project/setDotList', res);
} catch (error) {
console.log('error: ', error);
}
}
function getTasks() {
//
const onDateChange = event => {
const day = dayjs(event.fullDate);
const start = day.startOf('date').valueOf();
const end = day.endOf('date').valueOf();
getProjects(start, end);
};
}
//
const onUploadSuccess = () => {
uni.$ui.showToast('导入成功,即将打开新项目', 3000);
// uTips.show({
// title: '',
// type: 'success',
// duration: '3000',
// });
};
//
const onUploadError = error => {
uni.$ui.showToast('导入失败', 6000);
// uTips.show({
// title: error || '',
// type: 'error',
// duration: '6000',
// });
};
// /
function onMove(event) {
const y = event.changedTouches[0].pageY;
if (y - prevY > 0) {
// weekMode=true weekMode=false
data.value.calendar.weekMode && (data.value.calendar.weekMode = false);
} else if (y - prevY < 0) {
// weekMode=false weekMode=true
!data.value.calendar.weekMode && (data.value.calendar.weekMode = true);
}
prevY = y;
data.value.calendar.initDate();
}
function toLogin() {
uni.navigateTo({ url: '/pages/user/accountLogin' })
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

142
pages/project/project.vue

@ -0,0 +1,142 @@
<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" />
<!-- TODO: DEBUG: -->
<u-button @click="$store.commit('setTheme', 'theme-test')">测试切换主题</u-button>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useStore } from 'vuex';
import useInit from '@/hooks/project/useInit';
import useGetTasks from '@/hooks/project/useGetTasks';
const initHook = useInit();
const getTasksHook = useGetTasks();
const store = useStore();
const roleId = computed(() => store.state.role.roleId);
const timeNode = computed(() => store.state.task.timeNode);
const timeUnit = computed(() => store.state.task.timeUnit);
const projectId = computed(() => store.getters['project/projectId']);
const userId = computed(() => store.getters['user/userId']);
const newProjectInfo = computed(() => store.state.task.newProjectInfo);
const showScrollTo = computed(() => store.state.task.showScrollTo);
const height = ref(null);
const timeLine = ref(null);
onMounted(() => {
const system = uni.getSystemInfoSync();
height.value = `${system.windowHeight}px`;
});
//
function getGlobalData() {
const param = {
roleId: roleId.value,
timeNode: timeNode.value,
timeUnit: timeUnit.value,
projectId: projectId.value,
};
store.dispatch('task/getGlobal', param);
}
//
function clearTasksData() {
//
store.commit('task/setPermanents', []);
store.commit('task/setDailyTasks', []);
//
store.commit('task/clearTasks');
//
//
store.commit('task/clearEndFlag');
}
/**
* 当时间基准点发生变化时
* 重新根据时间和角色查询普通日常任务
* 永久日常任务不发生 改变
*/
watch(timeNode, newValue => {
if (newValue && roleId.value) {
console.log('当时间基准点发生变化时');
clearTasksData();
getGlobalData(); //
// initPlanTasks(); //
getTasksHook.initPlanTasks(); //
//
let timer = null;
timer = setInterval(() => {
if (showScrollTo.value) {
clearInterval(timer);
timeLine.value.setScrollPosition();
}
}, 500);
}
});
/**
* 当角色发生变化时
* 重新查询永久日常任务和普通日常任务
* 注意: 切换角色后 重新设置了时间基准点 时间基准点一定会变
* 所以监听时间基准点获取 可变日常任务即可 这里不用获取 避免重复获取
*/
watch(roleId, newValue => {
if (newValue) {
console.log('当角色发生变化时', newValue);
store.commit('task/setTimeNode', Date.now());
//
const params = {
roleId: newValue,
projectId: projectId.value,
};
store.dispatch('task/getPermanent', params);
}
});
/**
* 当时间基准点发生变化时
* 重新根据时间和角色查询普通日常任务
* 永久日常任务不发生改变
*/
watch(newProjectInfo, newValue => {
console.log('当时间基准点发生变化时');
if (newValue && newValue.value.projectId && newValue.value.url) {
uni.$u.route('/', {
u: userId.value,
p: newValue.value.projectId,
url: newValue.value.url,
});
clearTasksData();
store.commit('role/setRoleId', '');
const options = uni.$route.query;
initHook.init(options);
}
});
function getTasks(params) {
getTasksHook.initPlanTasks(params); //
}
</script>
<style lang="scss" scoped>
.border-b {
border-bottom: 1px solid #e4e7ed;
}
</style>

99
pages/user/accountLogin.vue

@ -0,0 +1,99 @@
<template>
<view class="u-p-l-50 u-p-r-50 u-p-t-30">
<u-form :model="form" ref="loginForm" :error-type="['message']">
<u-form-item label="用户名" prop="account" label-width="150">
<u-input placeholder="请输入用户名" v-model="form.account" type="text"></u-input>
</u-form-item>
<u-form-item label="密码" prop="password" label-width="150">
<u-input :password-icon="true" type="password" v-model="form.password" placeholder="请输入密码">
</u-input>
</u-form-item>
<view class="flex flex-nowrap">
<view class="flex-sub"></view>
<view class="u-m-t-30 u-font-12 text-gray-400" @click="openPage('/pages/user/forgetPassword')">忘记密码</view>
</view>
</u-form>
<view class="u-m-t-50">
<u-button @click="submit" type="primary">立即登录</u-button>
</view>
<view class="flex justify-between">
<view class="u-m-t-30" style="color: #2885ED;" @click="openPage('/pages/user/rigister')"> 新用户注册</view>
<view class="u-m-t-30" style="color: #2885ED;" @click="openPage('/pages/user/login')">手机号登录 </view>
</view>
<view style="margin-top: 200rpx;text-align: center; color: #999999;font-size: 35rpx;">
快速登录
</view>
<view style="text-align: center; margin-top: 20rpx;" @click="mixinInit.handleWxLogin">
<image src="/common/img/weixinIcon.png" mode="" style="width: 85rpx;height: 85rpx;"></image>
</view>
</view>
</template>
<script setup>
import { ref, computed, reactive } from 'vue';
import { useStore } from 'vuex';
import { onReady} from '@dcloudio/uni-app';
import userMixin from '@/hooks/user/userMixin'
const store = useStore();
const mixinInit = userMixin();
const loginForm = ref(null);
const form = reactive({
account: '',
password: ''
})
onReady(() => {
loginForm.value.setRules(mixinInit.rules);
});
const submit = () => {
loginForm.value.validate(valid => {
if (valid) {
login()
}
});
}
async function login() {
uni.$ui.showLoading();
try {
const params = reactive({
client: 1,
data: {
identifier: form.account,
credential: form.password,
},
type: 3,
});
let res = await uni.$u.api.signin(params);
store.commit('user/setToken', res.token);
store.commit('user/setUser', res);
uni.$storage.setStorageSync('anyringToken', res.token || '');
uni.$storage.setStorageSync('user', JSON.stringify(res));
uni.$ui.hideLoading();
uni.navigateTo({
url: '/pages/index/index'
});
} catch (error) {
uni.$ui.hideLoading();
uni.$ui.showToast(error);
}
}
function openPage(url) {
uni.navigateTo({
url: url
})
}
</script>
<style>
</style>

8
pages/user/forgetPassword.vue

@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

130
pages/user/login.vue

@ -0,0 +1,130 @@
<template>
<view class="u-p-l-50 u-p-r-50 u-p-t-30">
<u-form :model="form" ref="loginForm" :error-type="['message']">
<u-form-item label="手机号码" prop="phone" label-width="150">
<u-input placeholder="请输入手机号" v-model="form.phone" type="number"></u-input>
</u-form-item>
<u-form-item label="图形验证码" prop="verificationCodeValue" label-width="150">
<u-input placeholder="请输入计算结果" v-model="form.verificationCodeValue" type="number"></u-input>
<image slot="right" :src="imageBase64" mode="aspectFit" class="code-image" @click="getImageCode"></image>
</u-form-item>
<u-form-item label="验证码" prop="smsCode" label-width="150">
<u-input @focus="mixinInit.hasvalue(form)" placeholder="请输入验证码" v-model="form.smsCode" type="text"></u-input>
<u-button slot="right" type="primary" size="mini" v-show="mixinInit.dataObj.showPaste" @click="mixinInit.setCode" class="u-m-r-20">粘贴</u-button>
<u-button slot="right" size="mini" v-if="mixinInit.dataObj.showInterval">{{ mixinInit.dataObj.interval }}</u-button>
</u-form-item>
<view class="flex flex-nowrap">
<view class="flex-sub"></view>
<view class="u-m-t-30 u-font-12 text-gray-400" @click="openPage('/pages/user/forgetPassword')">忘记密码</view>
</view>
</u-form>
<view class="u-m-t-50">
<u-button @click="submit" type="primary">立即登录</u-button>
</view>
<view class="flex justify-between">
<view class="u-m-t-30" style="color: #2885ED;" @click="openPage('/pages/user/rigister')">新用户注册</view>
<view class="u-m-t-30" style="color: #2885ED;" @click="openPage('/pages/user/accountLogin')">用户名登录</view>
</view>
<view style="margin-top: 200rpx;text-align: center; color: #999999;font-size: 35rpx;">
快速登录
</view>
<view style="text-align: center; margin-top: 20rpx;" @click="mixinInit.handleWxLogin">
<image src="/common/img/weixinIcon.png" mode="" style="width: 85rpx;height: 85rpx;"></image>
</view>
</view>
</template>
<script setup>
import { ref, computed, reactive } from 'vue';
import { useStore } from 'vuex';
import { onLoad, onReady } from '@dcloudio/uni-app';
import userMixin from '@/hooks/user/userMixin'
const store = useStore();
const mixinInit = userMixin();
const loginForm = ref(null);
const form = reactive({
phone: '',
verificationCodeId: '', // id
verificationCodeValue: '', //
smsCode: ''
});
const imageBase64 = ref(null); //
getImageCode();
onReady(() => {
loginForm.value.setRules(mixinInit.rules);
});
const submit = () => {
loginForm.value.validate(valid => {
if (valid) {
login()
}
});
}
//
async function getImageCode() {
uni.$ui.showLoading();
try {
const data = await uni.$u.api.getImageCode();
imageBase64.value = data.imageBase64 || '';
form.verificationCodeId = data.verificationCodeId || '';
uni.$ui.hideLoading();
} catch (error) {
uni.$ui.hideLoading();
uni.$ui.showToast(error);
}
}
async function login() {
uni.$ui.showLoading();
try {
const params = reactive({
client: 1,
data: {
identifier: form.phone,
credential: form.smsCode,
},
type: 1,
});
let res = await uni.$u.api.signin(params);
store.commit('user/setToken', res.token);
store.commit('user/setUser', res);
uni.$storage.setStorageSync('anyringToken', res.token || '');
uni.$storage.setStorageSync('user', JSON.stringify(res));
uni.$ui.hideLoading();
uni.navigateTo({
url: '/pages/index/index'
});
} catch (error) {
uni.$ui.hideLoading();
uni.$ui.showToast(error);
}
}
function openPage(url) {
uni.navigateTo({
url: url
})
}
</script>
<style lang="scss" scoped>
.code-image {
width: 200rpx;
height: 70rpx;
}
</style>

8
pages/user/rigister.vue

@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

62
plugins/p-deliver-check/p-deliver-check.vue

@ -0,0 +1,62 @@
<template>
<!-- 上传交付物 -->
<view class="px-3 py-6 bg-white">
<u-input :auto-height="autoHeight" :border="border" :height="height" :type="type" placeholder="输入备注" v-model="remark" />
<view class="flex flex-row-reverse text-xs text-gray-400 mt-2">{{ wordNum }}/140</view>
<!-- 评分 -->
<view class="flex justify-between mt-3">
<slider :value="score" @change="sliderChange" max="100" min="0" show-value style="width: 60%" />
<u-input :border="border" :type="type1" @input="changeNumber" maxlength="100" placeholder="输入分数" v-model="score" />
</view>
<view class="flex flex-col justify-center mt-5">
<u-button @click="submit" size="medium" type="primary">提交</u-button>
<u-button @click="$emit('closeScore')" class="mt-2" size="medium">取消</u-button>
</view>
</view>
</template>
<script setup>
import { reactive, watchEffect } from 'vue';
const data = reactive({
remark: '',
type: 'textarea',
border: true,
height: 100,
autoHeight: true,
wordNum: 0,
score: 0,
type1: 'number',
});
const emit = defineEmits(['submit']);
watchEffect(() => {
if(remark) {
data.wordNum = remark.value.length;
}
if(score) {
data.score1 = score.value;
}
});
//
function submit() {
emit('submit', this.remark, this.score);
}
function sliderChange(e) {
data.score = e.detail.value;
}
function changeNumber(e) {
if (e > 100) {
data.score = 100;
}
}
</script>
<style></style>

9
plugins/p-deliver/p-deliver.vue

@ -0,0 +1,9 @@
<template>
<view class="deliver-container">p-deliver</view>
</template>
<script setup>
</script>
<style lang="scss"></style>

131
plugins/p-delivery-history/p-delivery-history.vue

@ -0,0 +1,131 @@
<template>
<!-- 交付物 -->
<view class="mt-3">
<view v-if="data.lists && data.lists.length">
<view :key="list.id" v-for="list in data.lists">
<view class="p-3 mt-3 shadow">
<view class="text-gray-400 pb-2">
<span class="mr-4">{{ list.name }}</span>
<span>{{ $moment(+list.time).format('YYYY-MM-DD HH:mm:ss') }}</span>
</view>
<view class="pb-2 flex flex-wrap overflow-hidden" v-if="list.content">
<a :href="list.content" class="text-blue-500" target="_blank" v-if="CheckUrl(list.content)">{{ list.content }}</a>
<span v-else>{{ list.content }}</span>
</view>
<view :key="checker.checkerId" v-for="checker in list.checkerList" class="mb-2">
<view class="flex justify-between">
<view class="font-bold">
{{ checker.checkerName }}
<span v-if="checker.isMine">()</span>
</view>
<view>
<span class="text-blue-500" v-if="checker.status === 1">通过</span>
<span class="text-red-500" v-if="checker.status === 2">驳回</span>
<span class="ml-4" v-if="checker.status !== 0">{{ checker.score }}</span>
<span class="text-gray-400" v-if="checker.status === 0 && !checker.isMine">未审核</span>
<view v-if="checker.status === 0 && checker.isMine">
<u-button @click="showScore(checker.checkId, 2)" class="mr-3" plain size="mini" type="error">驳回</u-button>
<u-button @click="showScore(checker.checkId, 1)" plain size="mini" type="primary">通过</u-button>
</view>
</view>
</view>
<view class="text-gray-400 text-xs mt-1">{{ checker.remark }}</view>
</view>
</view>
</view>
</view>
<u-empty icon-size="90" mode="history" text="暂未上传交付物" v-else></u-empty>
<!-- 评分 -->
<!-- <uni-popup :maskClick="false" background-color="#fff" ref="popup" type="bottom"><PDeliverCheck @closeScore="closeScore" @submit="submit"></PDeliverCheck></uni-popup> -->
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
// import UniPopup from '../../components/uni-popup/uni-popup.vue';
import PDeliverCheck from '../p-deliver-check/p-deliver-check.vue';
const props = defineProps({ task: { type: Object, default: null } });
const data = reactive({
lists: [],
show: false,
options: null,
loading: true, //
});
const store = useStore();
const projectId = computed(() => store.getters['project/projectId']);
const popup = ref(null);
onMounted(() => {
getDeliverOfTask();
});
async function getDeliverOfTask() {
try {
const params = { projectId: projectId.value, taskSubId: props.task.id };
const res = await uni.$u.api.queryDeliverOfTask(params);
data.lists = res;
} catch (error) {
console.error('p-delivery-history.vue getDeliverOfTask error: ', error);
uni.$ui.showToast(error.msg || '提交失败');
}
}
function showScore(checkId, status) {
// refuni-popup , type ['top','left','bottom','right','center']
popup.open('bottom');
data.options = { checkId, status };
}
function closeScore() {
popup.close('bottom');
}
async function submit(remark, score) {
try {
await checkDeliver(remark, score);
closeScore();
} catch (error) {
console.error('error: ', error);
}
}
/**
* 检查交付物
* @param {string} checkId 检查记录id
* @param {string} projectId 项目id
* @param {string} remark 评论
* @param {number} score 分数
* @param {number} status 检查状态(1-通过,2-驳回)
*/
async function checkDeliver(remark, score) {
try {
data.show = true;
const { checkId, status } = data.options;
const params = { checkId, projectId: projectId.value, status, remark, score };
await uni.$u.api.checkDeliver(params);
uni.$ui.showToast('交付物检查成功');
data.options = null;
getDeliverOfTask();
} catch (error) {
console.error('p-delivery-history.vue checkDeliver error: ', error);
uni.$t.ui.showToast('交付物检查失败,请稍后重试');
data.options = null;
}
}
//
function CheckUrl(url) {
const reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.)+$/;
if (!reg.test(url)) {
return false;
}
return true;
}
</script>
<style></style>

149
plugins/p-delivery-history/p-delivery-history1.vue

@ -0,0 +1,149 @@
<template>
<!-- 交付物 -->
<view class="mt-3">
<view v-if="lists && lists.length">
<view :key="list.id" v-for="list in lists">
<view class="text-gray-400 u-font-12 font-thin leading-none">
<span class="mr-2">{{ list.name }}</span>
<span>{{ $moment(+list.time).format('YYYY-MM-DD HH:mm:ss') }}</span>
</view>
<view class="mt-2 py-1 px-2.5 border border-gray-200 rounded flex flex-wrap overflow-hidden break-all" v-if="list.content">
<a :href="list.content" class="text-blue-500 u-font-12 font-thin" target="_blank" v-if="CheckUrl(list.content)">{{
list.content
}}</a>
<span v-else>{{ list.content }}</span>
</view>
<view :class="index === 0 ? 'mt-4' : 'mt-3'" v-for="(checker, index) in list.checkerList" :key="index">
<view class="flex justify-between leading-none">
<view>
{{ checker.checkerName }}
<span v-if="checker.isMine">()</span>
</view>
<view>
<span class="text-blue-500" v-if="checker.status === 1">通过</span>
<span class="text-red-500" v-if="checker.status === 2">驳回</span>
<span class="ml-4" v-if="checker.status !== 0">{{ checker.score }}</span>
<span class="text-gray-400" v-if="checker.status === 0 && !checker.isMine">未审核</span>
<view v-if="checker.status === 0 && checker.isMine">
<u-button class="action-btn mr-2" @click="showScore(checker.checkId, 1)" size="mini" shape="circle" type="primary">
通过
</u-button>
<u-button class="action-btn" @click="showScore(checker.checkId, 2)" size="mini" shape="circle" type="error">驳回</u-button>
</view>
</view>
</view>
<view class="text-gray-400 text-xs mt-1">{{ checker.remark }}</view>
</view>
</view>
</view>
<u-empty icon-size="90" mode="history" text="暂未上传交付物" v-else></u-empty>
<!-- 评分 -->
<uni-popup :maskClick="false" background-color="#fff" ref="popup" type="bottom">
<PDeliverCheck @closeScore="closeScore" @submit="submit"></PDeliverCheck>
</uni-popup>
</view>
</template>
<script>
import { mapGetters } from 'vuex';
import UniPopup from '../../components/uni-popup/uni-popup.vue';
import PDeliverCheck from '../p-deliver-check/p-deliver-check.vue';
export default {
name: 'p-delivery-history',
props: { task: { type: Object, default: null } },
components: { PDeliverCheck, UniPopup },
data() {
return {
lists: [],
show: false,
options: null,
loading: true, //
};
},
computed: mapGetters('project', ['projectId']),
mounted() {
this.getDeliverOfTask();
},
methods: {
async getDeliverOfTask() {
try {
const { projectId, task } = this;
const params = { projectId, taskSubId: task.id };
const data = await this.$u.api.queryDeliverOfTask(params);
this.lists = data;
} catch (error) {
console.error('p-delivery-history.vue getDeliverOfTask error: ', error);
this.$t.ui.showToast(error.msg || '提交失败');
}
},
showScore(checkId, status) {
// refuni-popup , type ['top','left','bottom','right','center']
this.$refs.popup.open('bottom');
this.options = { checkId, status };
},
closeScore() {
this.$refs.popup.close('bottom');
},
async submit(remark, score) {
try {
await this.checkDeliver(remark, score);
this.closeScore();
} catch (error) {
console.error('error: ', error);
}
},
/**
* 检查交付物
* @param {string} checkId 检查记录id
* @param {string} projectId 项目id
* @param {string} remark 评论
* @param {number} score 分数
* @param {number} status 检查状态(1-通过,2-驳回)
*/
async checkDeliver(remark, score) {
try {
this.show = true;
const { projectId, options } = this;
const { checkId, status } = options;
const params = { checkId, projectId, status, remark, score };
await this.$u.api.checkDeliver(params);
this.$t.ui.showToast('交付物检查成功');
this.options = null;
this.getDeliverOfTask();
} catch (error) {
console.error('p-delivery-history.vue checkDeliver error: ', error);
this.$t.ui.showToast('交付物检查失败,请稍后重试');
this.options = null;
}
},
//
CheckUrl(url) {
var reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.)+$/;
if (!reg.test(url)) {
return false;
} else {
return true;
}
},
},
};
</script>
<style scoped>
.action-btn {
padding: 0;
width: 80rpx;
height: 40rpx;
line-height: 40rpx;
}
</style>

6
plugins/p-manage-member/p-manage-member.vue

@ -0,0 +1,6 @@
<template>
<view>成员管理</view>
</template>
<script setup>
</script>

6
plugins/p-manage-project/p-manage-project.vue

@ -0,0 +1,6 @@
<template>
<view>项目管理</view>
</template>
<script setup>
</script>

6
plugins/p-manage-role/p-manage-role.vue

@ -0,0 +1,6 @@
<template>
<view>角色管理</view>
</template>
<script setup>
</script>

6
plugins/p-manage-task/p-manage-task.vue

@ -0,0 +1,6 @@
<template>
<view>任务管理</view>
</template>
<script setup>
</script>

54
plugins/p-subproject/p-subproject.vue

@ -0,0 +1,54 @@
<template>
<!-- 子项目插件 -->
<view>
<view v-for="item in data.sonProject" :key="item.detailId">
<span class="text-xs text-blue-500" @click="openProject(item)">{{ item.name }}</span>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue';
import Config from '@/common/js/config.js';
const props = defineProps({
task: {
type: Object,
default: () => {},
},
});
const data = reactive({ sonProject: [] });
onMounted(() => {
getSonProject();
});
async function getSonProject() {
try {
const data = await uni.$u.api.findSonProject({ projectId: props.task.detailId });
data.sonProject = data;
} catch (error) {
console.error('p-subproject.vue getSonProject error: ', error);
}
}
/**
* 打开项目
* @param {object} project 所点击的项目的信息
*/
function openProject(project) {
const { name, id, url } = project;
const { apiUrl } = Config;
const defaultwbs = `${apiUrl}/defaultwbs`;
url && (defaultwbs = url);
uni.$u.route('pages/project/project', {
u: userId.value,
p: id,
pname: name,
url: encodeURIComponent(url),
});
}
</script>
<style></style>

31
plugins/p-subtasks/p-subtasks.vue

@ -0,0 +1,31 @@
<template>
<view>
<view v-for="item in data.sonTask" :key="item.detailId">
<span class="text-xs text-gray-500">{{ item.name }}</span>
</view>
</view>
</template>
<script setup>
import { reactive } from 'vue';
const props = defineProps({
task: {
type: Object,
default: () => {},
},
});
const data = reactive({ sonTask: [] });
async function getSonTask() {
try {
const res = await uni.$u.api.findSonTask({ detailId: props.task.detailId });
data.sonTask = res;
} catch (error) {
console.error('p-subtasks.vue getSonTask error: ', error);
}
}
getSonTask();
</script>
<style></style>

10
plugins/p-task-countdown/p-task-countdown.vue

@ -0,0 +1,10 @@
<template>
<!-- 任务倒计时插件 -->
<view>任务倒计时插件</view>
</template>
<script setup>
</script>
<style></style>

9
plugins/p-task-description/p-task-description.vue

@ -0,0 +1,9 @@
<template>
<!-- 任务描述 -->
<view>{{ task.description }}</view>
</template>
<script setup>
defineProps({ task: { default: () => {}, type: Object } });
</script>

23
plugins/p-task-duration-delay/p-task-duration-delay.vue

@ -0,0 +1,23 @@
<template>
<view v-if="realDuration && planDuration">
<!-- 任务时长延迟插件 -->
<!-- 超时 -->
<span class="font-bold text-green-500" v-if="realDuration - 0 > planDuration - 0">
+{{ $time.formatDuration(realDuration - planDuration) }}
</span>
<!-- 延时 -->
<span class="font-bold text-red-500" v-if="realDuration - 0 < planDuration - 0">
-{{ $time.formatDuration(planDuration - realDuration) }}
</span>
</view>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({ task: { default: () => {}, type: Object } });
const realDuration = computed(() => props.task.realDuration);
const planDuration = computed(() => props.task.planDuration);
</script>

18
plugins/p-task-start-time-delay/p-task-start-time-delay.vue

@ -0,0 +1,18 @@
<template>
<!-- <view>任务开始时间延迟插件</view> -->
<view v-if="realStart && planStart">
<!-- 任务开始时间延迟插件 -->
<!-- 超时 -->
<span>{{ $time.formatDuration(+realStart - +planStart) }}</span>
</view>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({ task: { default: () => {}, type: Object } });
const realStart = computed(() => props.task.realStart);
const planStart = computed(() => props.task.planStart);
</script>

10
plugins/p-task-title/p-task-title.vue

@ -0,0 +1,10 @@
<template>
<!-- 任务名插件 -->
<theme>
<view>{{ task.name }}</view>
</theme>
</template>
<script setup>
defineProps({ task: { type: Object, default: () => {} } });
</script>

90
plugins/p-upload-deliverable/p-upload-deliverable.vue

@ -0,0 +1,90 @@
<template>
<!-- 上传交付物 -->
<view class="py-2">
<u-input :auto-height="data.autoHeight" :border="data.border" :height="data.height" :type="data.type" v-model="data.content" width="100" />
<!-- 选择检查人 -->
<!-- <ChooseChecker ref="checker" :checkerList="data.checkerList" @setCheckerList="setCheckerList"></ChooseChecker> -->
<view class="flex justify-between">
<u-button @click="submit" class="m-0" size="mini" type="primary">提交</u-button>
<u-icon @click="changeShowHistory" name="arrow-up" v-if="data.showHistory"></u-icon>
<u-icon @click="changeShowHistory" name="arrow-down" v-else></u-icon>
</view>
<p-delivery-history :task="task" v-if="data.showHistory" />
</view>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { useStore } from 'vuex';
// import ChooseChecker from '@/components/ChooseChecker/ChooseChecker.vue';
const props = defineProps({
task: { default: () => {}, type: Object },
});
const data = reactive({
content: '',
type: 'textarea',
border: true,
height: 30,
autoHeight: true,
checkerList: [],
showHistory: false, //
});
const checker = ref(null);
const store = useStore();
const members = computed(() => store.state.role.members);
const projectId = computed(() => store.getters['project/projectId']);
const checkers = computed(() => {
let arr = [];
if (members.value.length) {
members.value.forEach(member => {
const item = { value: member.memberId, label: member.name };
arr.push(item);
});
}
return arr;
});
//
function setCheckerList(checked, item) {
if (checked) {
data.checkerList.push(item.memberId);
} else {
const index = data.checkerList.findIndex(checker => checker === item.memberId);
data.checkerList.splice(index, 1);
}
}
//
function changeShowHistory() {
data.showHistory = !data.showHistory;
}
//
async function submit() {
try {
const { content, checkerList } = data;
const { task } = props;
if (!checkerList.length) {
uni.$ui.showToast('请选择检查人');
return;
}
const params = { content, checkerList, projectId: projectId.value, taskSubId: task.id };
await uni.$u.api.saveDeliver(params);
uni.$ui.showToast('交付物提交成功');
data.content = '';
data.checkerList = [];
checker.clearChecked();
} catch (error) {
console.error('p-upload-deliverable.vue submit error: ', error);
uni.$ui.showToast('交付物提交失败,请稍后重试');
}
}
</script>
<style scoped lang="scss"></style>

71
plugins/p-wbs-import/p-wbs-import.vue

@ -0,0 +1,71 @@
<template>
<view>
<view @click="handleUpload" v-if="task.name === '导入WBS新建项目'">{{ task.name }}</view>
<view @click="handleUpdate" v-if="task.name === '导入WBS更新项目'">{{ task.name }}</view>
<!-- 全局提示框 -->
<u-top-tips ref="uTips"></u-top-tips>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
defineProps({ task: { type: Object, default: () => {} } });
const store = useStore();
const userId = computed(() => store.state.user.userId);
const projectId = computed(() => store.getters['project/projectId']);
const uTips = ref(null);
//
function onUploadSuccess() {
uTips.show({
title: '导入成功,即将打开新项目',
type: 'success',
duration: '3000',
});
}
//
function onUploadError(error) {
uTips.show({
title: error || '导入失败',
type: 'error',
duration: '6000',
});
}
//
// TODO:
async function handleUpdate() {
try {
await uni.$u.api.import({ projectId: projectId.value });
// WBS
//
onUploadSuccess();
} catch (error) {
onUploadError(error);
}
}
// wbs
async function handleUpload() {
try {
const res = await uni.$u.api.import();
// WBS
//
onUploadSuccess();
setTimeout(() => {
uni.$u.route('/pages/project/project', {
u: userId.value,
p: res.id,
pname: res.pname,
url: res.url,
});
}, 2000);
} catch (error) {
onUploadError(error);
}
}
</script>

3
store/db/actions.js

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

3
store/db/getters.js

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

12
store/db/index.js

@ -1,12 +0,0 @@
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
store/db/mutations.js

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

7
store/db/state.js

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

42
store/index.js

@ -1,16 +1,16 @@
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';
import { createStore } from 'vuex';
import project from './project/index.js';
import role from './role/index.js';
import socket from './socket/index.js';
import task from './task/index.js';
import user from './user/index.js';
// 不属于具体模块的 应用级的 store内容
const state = {
theme: 'theme-default',
networkConnected: true, // 网络是否连接
forceUseStorage: true, // 强制启用storage
systemInfo: null, // 系统设备信息
};
const getters = {
@ -30,7 +30,29 @@ const mutations = {
setNetworkConnected(state, networkConnected) {
state.networkConnected = networkConnected;
},
/**
* 设置系统信息的数据
* @param {object} state
* @param {object | null} data 获取到的数据
*/
setSystemInfo(state, data) {
state.systemInfo = data;
},
/**
* 设置主题
* @param {object} state
* @param {string} theme 主题名称 默认theme-default
*/
setTheme(state, theme) {
state.theme = theme || 'theme-default';
},
};
Vue.use(Vuex);
export default new Vuex.Store({ state, getters, mutations, modules: { user, messages, socket, project, role, task } });
export default createStore({
state,
getters,
mutations,
modules: { user, socket, project, role, task },
});

3
store/messages/getters.js

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

4
store/messages/index.js

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

85
store/messages/mutations.js

@ -1,85 +0,0 @@
import storage from '@/utils/storage';
const { setStorageSync, getStorageSync, removeStorageSync } = storage;
const mutations = {
/**
* 初始化消息栈
* @param {object} state
* @param {string} type
* type:
* syncMessages 同步消息栈
* faultMessages 故障消息 未处理消息栈
* faults 所有的故障消息栈
*/
messagesInit(state, type) {
const messages = getStorageSync(type) ? JSON.parse(getStorageSync(type)) : [];
state[type] = messages;
},
/**
* 将新 消息添加到 消息栈 最前边
* @param { object } state
* @param { object } data
* data: message, type
* message 消息对象
* type:
* syncMessages 同步消息栈
* faultMessages 故障消息 未处理消息栈
* faults 所有的故障消息栈
* game 游戏的消息
*
* cache: boolean true 本地存储 false 不存储
*/
messagesAdd(state, data) {
const messages = state[data.type];
if (messages.length > 0) {
const result = messages.find(msg => msg.id === data.message.id);
if (result) return;
}
messages.unshift(data.message);
// eslint-disable-next-line no-param-reassign
state[data.type] = messages;
if (data.cache) {
setStorageSync(data.type, JSON.stringify(messages));
}
},
/**
* 通过消息id移除指定 同步 消息
* @param { object } state
* @param { object } data
* data: messageId, type
* messageId: 要移除的消息的messageId
* type:
* syncMessages 同步消息栈
* faultMessages 故障消息 未处理消息栈
* faults 所有的故障消息栈
* cache: boolean true 本地存储 false 不存储
*/
messagesRemoveById(state, data) {
const messages = state[data.type];
const index = messages.findIndex(msg => msg.id === data.messageId);
if (index < 0) return;
messages.splice(index, 1);
// eslint-disable-next-line no-param-reassign
state[data.type] = messages;
if (data.cache) {
setStorageSync(data.type, JSON.stringify(messages));
}
},
/**
* 清除指定type的消息
* @param {any} state
* @param {object} data
* data: type, cache
*/
messagesClear(state, data) {
state[data.type] = [];
if (data.cache) {
removeStorageSync(data.type);
}
},
};
export default mutations;

8
store/messages/state.js

@ -1,8 +0,0 @@
const state = {
syncMessages: [], // 同步消息
faultMessages: [], // 新收到的未处理的 故障消息
faults: [], // 所有的故障消息
game: [], // 游戏的消息
};
export default state;

1
store/project/getters.js

@ -4,7 +4,6 @@ const getters = {
* @param {object} project
*/
projectId({ project }) {
uni.$t.storage.setStorageSync('projectId', project.id);
return project.id;
},
};

4
store/project/index.js

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

19
store/project/mutations.js

@ -12,6 +12,25 @@ const mutations = {
}
},
/**
* 设置子项目收缩展开
* @param { object } state
* @param { object } options options:{ index,show }
*/
setProjectItemShow(state, options) {
if (options.show) {
for (var i = 0; i < state.projects.length; i++) {
if (i === options.index) {
state.projects[i].show = true;
} else {
state.projects[i].show = false;
}
}
} else {
state.projects[options.index].show = false;
}
},
/**
* 设置当前项目信息
* @param { object } state

2
store/role/actions.js

@ -9,7 +9,7 @@ const actions = {
const data = await uni.$u.api.queryChecker(params);
commit('setMembers', data);
} catch (error) {
uni.$t.ui.showToast(error.msg || '成员查询失败');
uni.$ui.showToast(error.msg || '成员查询失败');
}
},
};

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save