78 changed files with 6139 additions and 27 deletions
@ -1,6 +1,6 @@ |
|||
VUE_APP_NODE_ENV=development |
|||
VUE_APP_BASE_URL=https://test.tall.wiki |
|||
VUE_APP_API_URL=https://test.tall.wiki/gateway |
|||
VUE_APP_MSG_URL=wss://test.tall.wiki/websocket/message/v4.0/ws |
|||
VUE_APP_PROJECT_PATH=https://test.tall.wiki/tall-project |
|||
VUE_APP_BASE_URL=https://www.tall.wiki |
|||
VUE_APP_API_URL=https://www.tall.wiki/gateway |
|||
VUE_APP_MSG_URL=wss://www.tall.wiki/websocket/message/v4.0/ws |
|||
VUE_APP_PROJECT_PATH=https://www.tall.wiki/tall-project |
|||
VUE_APP_VERSION=v3.2.0 |
|||
|
@ -0,0 +1,20 @@ |
|||
// 插件的地址是固定的
|
|||
const url = process.env.VUE_APP_API_URL; |
|||
|
|||
const install = (Vue, vm) => { |
|||
vm.$u.api = { ...vm.$u.api } || {}; |
|||
// 获取插件信息
|
|||
vm.$u.api.getOtherPlugin = param => vm.$u.post(`${url}/pluginshop/plugin/query?pluginId=${param.pluginId}&styleType=${param.styleType}`); |
|||
// 查询子任务
|
|||
vm.$u.api.findSonTask = param => vm.$u.post(`${uni.$t.domain}/task/findSonTask`, param); |
|||
// 查询子项目
|
|||
vm.$u.api.findSonProject = param => vm.$u.post(`${uni.$t.domain}/project/findSonProject`, param); |
|||
// 提交交付物
|
|||
vm.$u.api.saveDeliver = param => vm.$u.post(`${uni.$t.domain}/deliver/save`, param); |
|||
// 查询任务的交付物历史记录
|
|||
vm.$u.api.queryDeliverOfTask = param => vm.$u.post(`${uni.$t.domain}/deliver/queryDeliverOfTask`, param); |
|||
// 检查交付物
|
|||
vm.$u.api.checkDeliver = param => vm.$u.post(`${uni.$t.domain}/deliver/checkDeliver`, param); |
|||
}; |
|||
|
|||
export default { install }; |
@ -0,0 +1,16 @@ |
|||
const install = (Vue, vm) => { |
|||
vm.$u.api = { ...vm.$u.api } || {}; |
|||
//根据id获取项目信息
|
|||
vm.$u.api.findProjectById = param => vm.$u.post(`${uni.$t.domain}/project/findProjectById`, param); |
|||
|
|||
//创建分享连接
|
|||
vm.$u.api.createShare = param => vm.$u.post(`${uni.$t.domain}/share/create`, param); |
|||
|
|||
//点击分享连接
|
|||
vm.$u.api.clickShare = param => vm.$u.post(`${uni.$t.domain}/share/click`, param); |
|||
|
|||
//查询医院是否填写了调查问卷
|
|||
vm.$u.api.queryNotWrite = param => vm.$u.post(`${uni.$t.domain}/questionnaire/queryNotWrite`, param); |
|||
}; |
|||
|
|||
export default { install }; |
@ -0,0 +1,9 @@ |
|||
const install = (Vue, vm) => { |
|||
vm.$u.api = { ...vm.$u.api } || {}; |
|||
//根据项目id查找角色
|
|||
vm.$u.api.findShowRole = param => vm.$u.post(`${uni.$t.domain}/role/show`, param); |
|||
//根据项目id查找所有成员
|
|||
vm.$u.api.queryChecker = param => vm.$u.post(`${uni.$t.domain}/deliver/queryChecker`, param); |
|||
}; |
|||
|
|||
export default { install }; |
@ -0,0 +1,17 @@ |
|||
const install = (Vue, vm) => { |
|||
vm.$u.api = { ...vm.$u.api } || {}; |
|||
vm.$u.api.getGlobal = param => vm.$u.post(`${uni.$t.domain}/task/global`, param); |
|||
vm.$u.api.getPermanent = param => vm.$u.post(`${uni.$t.domain}/task/permanent`, param); |
|||
//根据时间基准点和角色查找定期任务
|
|||
vm.$u.api.getRegularTask = param => vm.$u.post(`${uni.$t.domain}/task/regular`, param); |
|||
//修改任务状态
|
|||
vm.$u.api.updateTaskType = param => vm.$u.post(`${uni.$t.domain}/task/type`, param); |
|||
//新建任务
|
|||
vm.$u.api.saveTask = param => vm.$u.post(`${uni.$t.domain}/task/save`, param); |
|||
//克隆任务
|
|||
vm.$u.api.cloneTask = param => vm.$u.post(`${uni.$t.domain}/task/clone`, param); |
|||
//模糊查询 查找项目下的任务
|
|||
vm.$u.api.queryTaskOfProject = param => vm.$u.post(`${uni.$t.domain}/task/queryTaskOfProject`, param); |
|||
}; |
|||
|
|||
export default { install }; |
@ -0,0 +1,87 @@ |
|||
<template> |
|||
<view class="my-3" v-if="allMembers && 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 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="!show" @click="show = true">...</span> |
|||
</view> |
|||
</view> |
|||
<!-- 折叠起来的 --> |
|||
<view class="flex flex-wrap text-center items-center" v-if="show"> |
|||
<u-tag |
|||
:type="member.checked ? 'primary' : 'info'" |
|||
:mode="member.checked ? 'dark' : 'light'" |
|||
v-for="(member, index) in 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="show" size="26" @click="show = false"></u-icon> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState } from 'vuex'; |
|||
|
|||
export default { |
|||
props: { |
|||
checkerList: { |
|||
default: () => [], |
|||
type: Array, |
|||
}, |
|||
}, |
|||
|
|||
data() { |
|||
return { allMembers: [], show: false, topMembers: [], bottomMembers: [] }; |
|||
}, |
|||
|
|||
computed: mapState('role', ['members']), |
|||
|
|||
mounted() { |
|||
if (this.members && this.members.length) { |
|||
this.allMembers = this.members; |
|||
// TODO: 等后台返回默认检查人后修改 |
|||
this.allMembers.forEach(item => { |
|||
item.checked = false; |
|||
}); |
|||
this.topMembers = this.members.slice(0, 3); |
|||
this.bottomMembers = this.members.slice(3); |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
tagClick(index, item, membersType) { |
|||
// 点击选择或取消选择 |
|||
const arr = this.$u.deepClone(this[membersType]); |
|||
arr[index].checked = !arr[index].checked; |
|||
this[membersType] = [...arr]; |
|||
// 将选中的id传给checkerList |
|||
this.$emit('setCheckerList', arr[index].checked, item); |
|||
}, |
|||
|
|||
// 清空所有选中的检查人 |
|||
clearChecked() { |
|||
for (let i = 0; i < this.topMembers.length; i++) { |
|||
this.topMembers[i].checked = false; |
|||
} |
|||
for (let i = 0; i < this.bottomMembers.length; i++) { |
|||
this.bottomMembers[i].checked = false; |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,87 @@ |
|||
<template> |
|||
<view class="m-2" v-if="globals && globals.length"> |
|||
<u-card @click="openCard" :show-foot="false" :show-head="false" style="max-height: 340rpx" border-radius="25" margin="0"> |
|||
<!-- :style="{ 'max-height': isShrink ? '106rpx' : '340rpx' }" --> |
|||
<view slot="body"> |
|||
<scroll-view :scrollY="true" style="max-height: 280rpx"> |
|||
<!-- <scroll-view :scrollY="true" :style="{ 'max-height': isShrink ? '50rpx' : '280rpx' }"> --> |
|||
<skeleton :banner="false" :loading="!globals.length" :row="4" animate class="u-line-2 skeleton"></skeleton> |
|||
<view class="grid gap-2"> |
|||
<block v-for="item in globals" :key="item.id"> |
|||
<template v-if="item.plugins"> |
|||
<block 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> |
|||
</block> |
|||
</template> |
|||
</block> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</u-card> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters, mapMutations, mapState } from 'vuex'; |
|||
import Skeleton from '@/components/Skeleton/Skeleton'; |
|||
|
|||
export default { |
|||
name: 'Global', |
|||
components: { Skeleton }, |
|||
|
|||
data() { |
|||
return { |
|||
// loading: true, |
|||
task: null, |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('task', ['isShrink']), |
|||
...mapGetters('task', ['globals']), |
|||
}, |
|||
|
|||
methods: { |
|||
...mapMutations('task', ['setShrink']), |
|||
|
|||
// 手动展开日常任务 |
|||
openCard() { |
|||
if (this.isShrink) { |
|||
this.setShrink(false); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<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> |
@ -0,0 +1,101 @@ |
|||
<template> |
|||
<view class="input-group flex-1"> |
|||
<input :placeholder="placeholder" @input="search" @blur="hideList" v-model="backName" /> |
|||
<view class="ul"> |
|||
<view class="li" v-for="(item, index) in dataSource" :key="index" @tap="select(item)">{{ item.name }}</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
placeholder: String, //默认提示 |
|||
searchKey: String, //模糊搜索的key值 |
|||
dataSource: { |
|||
type: Array, |
|||
default: function () { |
|||
//数据源 |
|||
return []; |
|||
}, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
list: [], |
|||
name: '', |
|||
backName: '', |
|||
}; |
|||
}, |
|||
destroyed() { |
|||
clearTimeout(this.t); |
|||
}, |
|||
methods: { |
|||
search(e) { |
|||
let val = e.detail.value; |
|||
console.log('val: ', val); |
|||
this.$emit('searchPrevTask', val); |
|||
|
|||
// let arr = []; |
|||
// for (let i = 0; i < dataSource.length; i++) { |
|||
// if (dataSource[i].name.indexOf(val) !== -1) { |
|||
// arr.push(dataSource[i]); |
|||
// } |
|||
// } |
|||
// if (!val) { |
|||
// this.list = []; |
|||
// } else { |
|||
// this.list = arr; |
|||
// } |
|||
}, |
|||
|
|||
select(item) { |
|||
console.log('item: ', item); |
|||
this.backName = item.name; |
|||
this.$emit('select', item); |
|||
}, |
|||
|
|||
hideList() { |
|||
setTimeout(() => { |
|||
this.$emit('clearAllTasks'); |
|||
}, 200); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.input-group { |
|||
position: relative; |
|||
|
|||
input { |
|||
border-bottom: 1upx solid #dcdfe6; |
|||
height: 90upx; |
|||
padding-left: 10upx; |
|||
font-size: 30upx; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.uni-input-placeholder { |
|||
color: rgb(192, 196, 204); |
|||
font-size: 28upx; |
|||
} |
|||
|
|||
.ul { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 100%; |
|||
width: 100%; |
|||
z-index: 999; |
|||
background: #fff; |
|||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
|||
max-height: 100px; |
|||
overflow-y: auto; |
|||
|
|||
.li { |
|||
border-bottom: 1upx solid #f1f1f1; |
|||
padding: 16upx; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,149 @@ |
|||
<template> |
|||
<view class="u-font-14" style="height: 100%"> |
|||
<!-- <view v-if="pluginContent" @click="setStorage"> |
|||
<view |
|||
: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" |
|||
style="height: 100%" |
|||
v-html="pluginContent" |
|||
></view> |
|||
</view> --> |
|||
|
|||
<view @click="setStorage"> |
|||
<!-- <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> |
|||
import { mapGetters, mapState, mapActions } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'Plugin', |
|||
props: { |
|||
task: { default: () => {}, type: Object }, |
|||
pluginId: { default: '1', type: String }, |
|||
styleType: { default: 0, type: Number }, |
|||
pluginTaskId: { default: '', type: String }, |
|||
param: { type: String, default: '' }, |
|||
}, |
|||
|
|||
data() { |
|||
return { pluginContent: null }; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['roleId']), |
|||
...mapState('user', ['token']), |
|||
...mapGetters('user', ['userId']), |
|||
...mapGetters('project', ['projectId']), |
|||
...mapGetters('role', ['isMine']), |
|||
|
|||
// 插件名称 |
|||
// pluginComponent() { |
|||
// const target = this.$t.plugin.defaults.find(item => item.id === +this.pluginId); |
|||
// if (!target) return ''; |
|||
// return target.component; |
|||
// }, |
|||
}, |
|||
|
|||
async created() { |
|||
if (this.pluginId === '5') { |
|||
// 根据项目id获取成员列表 |
|||
await this.getAllMembers({ projectId: this.projectId }); |
|||
} |
|||
await this.getPlugin(); |
|||
}, |
|||
|
|||
methods: { |
|||
...mapActions('role', ['getAllMembers']), |
|||
// 获取插件信息 |
|||
async getPlugin() { |
|||
const { pluginId, styleType } = this; |
|||
const params = { pluginId, styleType }; |
|||
this.$t.$q.getOtherPlugin(params, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err); |
|||
} else { |
|||
if (!data || !data.id) return; |
|||
const reg = /data-root=["|']?(\w+)["|']?/gi; |
|||
let uuid = ''; |
|||
// FIXME: 没有兼容 只有js, 没有html的情况 |
|||
if (data.html) { |
|||
// 查有没有data-root=“xxx” 有的话 将xxx替换为 pluginTaskId |
|||
|
|||
if (reg.test(data.html)) { |
|||
uuid = RegExp.$1; |
|||
const str = data.html.replace(new RegExp(uuid, 'g'), `p${this.pluginTaskId}`); |
|||
this.pluginContent = str; |
|||
} else { |
|||
this.pluginContent = data.html; |
|||
} |
|||
|
|||
const str = data.js.replace(new RegExp(uuid, 'g'), `p${this.pluginTaskId}`); |
|||
this.handleDom(str); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// if (data.js) { |
|||
// if (reg.test(data.js)) { |
|||
// const uuid = RegExp.$1; |
|||
// const str = data.js.replace(new RegExp(uuid, 'g'), `p${this.pluginTaskId}`); |
|||
// this.handleDom(str); |
|||
// } else { |
|||
// this.handleDom(data.js); |
|||
// } |
|||
// } |
|||
}, |
|||
|
|||
// 创建script dom |
|||
handleDom(js) { |
|||
const { pluginTaskId } = this; |
|||
let domList = Array.from(document.getElementsByTagName('script')); |
|||
const index = domList.findIndex(item => item.id === `p${pluginTaskId}`); |
|||
if (index >= 0) { |
|||
document.body.removeChild(document.getElementById(`p${pluginTaskId}`)); |
|||
} |
|||
const scriptDom = document.createElement('script'); |
|||
scriptDom.id = `p${pluginTaskId}`; |
|||
scriptDom.setAttribute('data-type', 'plugin'); |
|||
scriptDom.innerHTML = js; |
|||
this.$nextTick(() => { |
|||
document.body.append(scriptDom); |
|||
}); |
|||
}, |
|||
|
|||
// 点击时存储 storage |
|||
setStorage() { |
|||
this.$t.storage.setStorageSync('roleId', this.roleId); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,252 @@ |
|||
<template> |
|||
<view class="px-2 bg-white wrap"> |
|||
<view class="home-box u-skeleton"> |
|||
<scroll-view :enable-flex="true" :scroll-left="scrollLeft" :throttle="false" scroll-with-animation scroll-x @scroll="scroll"> |
|||
<view class="tab-box"> |
|||
<!-- 角色项 |
|||
default-tab-choice 是 我的角色 && 当前展示 |
|||
default-tab-item 是 我的角色 && 当前不展示 |
|||
tab-choice 不是我的 && 当前展示 |
|||
--> |
|||
<view |
|||
:class="{ |
|||
'default-tab-choice': item.mine == 1 && roleId === item.id, |
|||
'default-tab-item': item.mine == 1 && roleId !== item.id, |
|||
'tab-choice': item.mine == 0 && roleId === item.id, |
|||
}" |
|||
:key="index" |
|||
@click="changeRole(item.id, index)" |
|||
class="tab-item" |
|||
v-for="(item, index) in roles" |
|||
> |
|||
<view class="tab-children u-skeleton-fillet u-font-14"> |
|||
{{ item.name }} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
<!-- 骨架屏 --> |
|||
<u-skeleton :animation="true" :loading="loading" bg-color="#fff"></u-skeleton> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState, mapMutations, mapActions } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'Roles', |
|||
data() { |
|||
return { |
|||
tabIndex: 0, // 当前访问的 index 默认为0 |
|||
tabList: [], // tab dom节点集合 |
|||
scrollLeft: 0, // scrollview需要滚动的距离 |
|||
loading: false, // 是否显示骨架屏组件 |
|||
roles: [ |
|||
{ id: 1, name: '项目经理', mine: 0, pm: 1, sequence: 1 }, |
|||
{ id: 2, name: '运维', mine: 0, pm: 0, sequence: 2 }, |
|||
], |
|||
roleLeft: 0, |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['visibleRoles', 'roleId']), |
|||
...mapState('task', ['tasks']), |
|||
}, |
|||
|
|||
watch: { |
|||
visibleRoles(val) { |
|||
if (val && val.length) { |
|||
this.roles = [...this.visibleRoles]; |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
}, |
|||
|
|||
mounted() { |
|||
if (!this.visibleRoles || !this.visibleRoles.length) { |
|||
this.loading = true; |
|||
} else { |
|||
this.roles = [...this.visibleRoles]; |
|||
} |
|||
|
|||
this.$nextTick(() => { |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query |
|||
.selectAll('.tab-children') |
|||
.boundingClientRect(data => { |
|||
this.roleLeft = data[0].left; |
|||
}) |
|||
.exec(); |
|||
}); |
|||
}, |
|||
|
|||
methods: { |
|||
...mapActions('task', ['handleRegularTask']), |
|||
...mapMutations('role', ['setRoleId']), |
|||
...mapMutations('task', ['setPermanents', 'clearEndFlag']), |
|||
|
|||
scroll(e) { |
|||
this.scrollLeft = e.detail.scrollLeft; |
|||
}, |
|||
|
|||
// 设置滚动位置 |
|||
setCurrentRole(index) { |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query |
|||
.selectAll('.tab-children') |
|||
.boundingClientRect(data => { |
|||
data.forEach(item => { |
|||
this.tabList.push({ width: item.width }); |
|||
}); |
|||
}) |
|||
.exec(); |
|||
const system = uni.getSystemInfoSync(); // 获取系统信息 |
|||
// 屏幕宽度 |
|||
let screenWidth = system.windowWidth; |
|||
// 当前滚动的位置 |
|||
let left = 0; |
|||
setTimeout(() => { |
|||
for (let i = 0; i < index; i++) { |
|||
left += this.tabList[i].width + (this.roleLeft - 8) * 2; |
|||
} |
|||
left += (this.tabList[index].width + (this.roleLeft - 8) * 2) / 2; |
|||
if (left > screenWidth) { |
|||
this.scrollLeft = left - screenWidth + screenWidth / 2; |
|||
} else if (left > screenWidth / 2) { |
|||
this.scrollLeft = left - screenWidth / 2; |
|||
} else if (left < screenWidth / 2) { |
|||
this.scrollLeft = 0; |
|||
} |
|||
}, 10); |
|||
}, |
|||
|
|||
// 切换角色 |
|||
// 查任务这里不用管 project监听了roleId的变化 |
|||
// 时间基准点不用管 project监听了roleId 里处理了 |
|||
changeRole(id, index) { |
|||
try { |
|||
// 清除多余的script |
|||
// this.clearPluginScript(); |
|||
this.$nextTick(() => { |
|||
this.setRoleId(id); |
|||
// 改变index 即手动点击切换 我在此时将当前元素赋值给左边距 实现自动滚动 |
|||
this.setCurrentRole(index); |
|||
}); |
|||
} catch (error) { |
|||
console.error('role.vue changeRole error: ', error); |
|||
} |
|||
}, |
|||
|
|||
// 清除插件script |
|||
clearPluginScript() { |
|||
try { |
|||
const scripts = document.querySelectorAll('script[data-type=plugin]'); |
|||
|
|||
for (let i = 0; i < scripts.length; i++) { |
|||
document.body.removeChild(scripts[i]); |
|||
} |
|||
} catch (error) { |
|||
console.error('clearPluginScript error: ', error); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.home-box { |
|||
// 对此盒子进行 sticky 粘性定位 |
|||
position: sticky; |
|||
top: 0; |
|||
background: #fff; // 设置背景 否则会透明 |
|||
|
|||
/* #ifdef H5 */ |
|||
//粘性定位 在h5下 加 原生头部高度 44px |
|||
top: 88rpx; |
|||
/* #endif */ |
|||
|
|||
.tab-box { |
|||
position: relative; |
|||
white-space: nowrap; |
|||
// height: 84rpx; |
|||
|
|||
.tab-item { |
|||
// width: 20%; |
|||
// height: 84rpx; |
|||
padding: 20rpx 24rpx; |
|||
position: relative; |
|||
display: inline-block; |
|||
text-align: center; |
|||
font-size: 30rpx; |
|||
transition-property: background-color, width; |
|||
} |
|||
|
|||
.default-tab-item { |
|||
color: $roleChoiceColor; |
|||
} |
|||
|
|||
.default-tab-choice { |
|||
//当前选中 基于此类名给与底部选中框 |
|||
position: relative; |
|||
color: $roleChoiceColor; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.default-tab-choice:before { |
|||
content: ''; |
|||
position: absolute; |
|||
left: 0; |
|||
bottom: -20rpx; |
|||
width: 100%; |
|||
height: 6rpx; |
|||
border-radius: 2rpx; |
|||
background: $roleChoiceColor; |
|||
} |
|||
|
|||
.tab-choice { |
|||
//当前选中 基于此类名给与底部选中框 |
|||
position: relative; |
|||
color: $uni-color-primary; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.tab-choice:before { |
|||
content: ''; |
|||
position: absolute; |
|||
left: 0; |
|||
bottom: -20rpx; |
|||
width: 100%; |
|||
height: 6rpx; |
|||
border-radius: 2rpx; |
|||
background: $uni-color-primary; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// // 删除 底部滚动条 |
|||
/* #ifndef APP-NVUE */ |
|||
::-webkit-scrollbar, |
|||
::-webkit-scrollbar, |
|||
::-webkit-scrollbar { |
|||
display: none; |
|||
width: 0 !important; |
|||
height: 0 !important; |
|||
-webkit-appearance: none; |
|||
background: transparent; |
|||
} |
|||
|
|||
/* #endif */ |
|||
/* #ifdef H5 */ |
|||
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条 |
|||
scroll-view ::v-deep ::-webkit-scrollbar { |
|||
display: none; |
|||
} |
|||
|
|||
/* #endif */ |
|||
|
|||
.skeleton { |
|||
height: 44rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,84 @@ |
|||
# skeleton组件 |
|||
|
|||
### 1.描述 |
|||
> 此组件用于加载数据时占位图显示,跟vant-ui骨架屏用法相似,但比vant-ui更灵活 |
|||
|
|||
|
|||
|
|||
### 2.用法 |
|||
|
|||
- 基本用法 |
|||
|
|||
代码: |
|||
```vue |
|||
//基本用法 |
|||
<skeleton :row="3" animate :loading="loading" > |
|||
<view> |
|||
content |
|||
</view> |
|||
</skeleton> |
|||
``` |
|||
|
|||
|
|||
- **显示 title ——通过 **title 属性显示title占位图 |
|||
|
|||
代码: |
|||
```vue |
|||
//显示 title——通过 title 属性显示title占位图 |
|||
<skeleton :row="3" title animate :loading="loading"> |
|||
<view> |
|||
content |
|||
</view> |
|||
</skeleton> |
|||
``` |
|||
|
|||
|
|||
- 显示头像(上面)——通过avatar=‘top’让头像的占位图上面显示 |
|||
|
|||
代码: |
|||
```vue |
|||
<skeleton :avatar="top" avatarAlign="left" :row="3" animate :loading="loading" style="margin-top:24rpx;"> |
|||
<view> |
|||
content |
|||
</view> |
|||
</skeleton> |
|||
``` |
|||
|
|||
|
|||
- 显示头像(左边)——通过avatar=‘left’让头像的占位图左边显示 |
|||
|
|||
代码: |
|||
```vue |
|||
<skeleton title :avatar="left" :row="3" animate :loading="loading" style="margin-top:24rpx;"> |
|||
<view> |
|||
content |
|||
</view> |
|||
</skeleton> |
|||
``` |
|||
|
|||
|
|||
- 显示banner**——通过 **banner属性显示banner占位图(只显示banner,不显示内容占位图时设置row="0") |
|||
|
|||
代码: |
|||
```vue |
|||
<skeleton banner :row="0" animate :loading="loading" style="margin-top:24rpx;"> |
|||
<view> |
|||
content |
|||
</view> |
|||
</skeleton> |
|||
``` |
|||
### |
|||
### 3. API |
|||
### Props |
|||
| **属性名** | **说明** | **类型** | **默认值** | 可取值 | |
|||
| --- | --- | --- | --- | --- | |
|||
| loading | 是否显示骨架屏 | Boolean | true | true/false | |
|||
| row | 段落行数 | Number | String | 3 | 0表示不展现 | |
|||
| rowWidth | 段落行宽度 | Boolean | Number | '100%' | | |
|||
| title | 是否显示标题 | Boolean | String | false | | |
|||
| banner | 是否显示banner | Boolean | String | false | | |
|||
| animate | 是否开启动画 | Boolean | String | false | | |
|||
| avatar | 头像位置 | Boolean | String | ''空 | left/top | |
|||
| avatarSize | 头像大小 | String | - | | |
|||
| avatarShape | 头像形状 | String | circle | circle/round | |
|||
|
@ -0,0 +1,187 @@ |
|||
<template> |
|||
<view> |
|||
<view :class="[avatarClass, animationClass]" class="lx-skeleton" v-show="loading"> |
|||
<view :class="[avatarShapeClass, bannerClass]" :style="{ width: avatarSize, height: avatarSize }" class="avatar-class"></view> |
|||
<view :style="{ width: rowWidth }" class="row"> |
|||
<view class="row-class lx-skeleton_title" v-if="title"></view> |
|||
<view :key="index" class="row-class" v-for="(item, index) in row"></view> |
|||
</view> |
|||
</view> |
|||
<slot v-if="!loading"></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
/** |
|||
* skeleton 骨架屏 |
|||
* @description 用于加载数据时占位图显示,跟Vant-UI用法相似,但比Vant-UI更灵活 |
|||
* @property {Boolean} loading 是否显示骨架屏,默认为true |
|||
* @property {Number | String} row 段落行数,默认为3 |
|||
* @property {Boolean | Number} rowWidth 段落行宽度,默认为100% |
|||
* @property {Boolean | String} title 是否显示标题,默认为false |
|||
* @property {Boolean | String} banner 是否显示banner,默认为false |
|||
* @property {Boolean | String} animate 是否开启动画,默认为false |
|||
* @property {Boolean | String} avatar 头像位置 |
|||
* @property {String} avatarSize 头像大小 |
|||
* @property {String} avatarShape 头像形状,默认为circle |
|||
* |
|||
* */ |
|||
export default { |
|||
props: { |
|||
loading: { |
|||
type: Boolean, |
|||
default: true, |
|||
}, |
|||
row: { |
|||
type: Number, |
|||
default: 3, |
|||
}, |
|||
title: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
avatar: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
animate: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
avatarSize: { type: String }, |
|||
rowWidth: { |
|||
type: String, |
|||
default: '100%', |
|||
}, |
|||
avatarShape: { |
|||
type: String, |
|||
default: 'circle', |
|||
}, |
|||
banner: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
// avator-size:{ |
|||
// type: String, |
|||
// defualt: '32px' |
|||
// } |
|||
}, |
|||
computed: { |
|||
avatarClass() { |
|||
if (this.avatar == 'top') { |
|||
return ['lx-skeleton_avator__top']; |
|||
} else if (this.avatar == 'left') { |
|||
return ['lx-skeleton_avator__left']; |
|||
} else { |
|||
return ''; |
|||
} |
|||
}, |
|||
animationClass() { |
|||
return [this.animate ? 'lx-skeleton_animation' : '']; |
|||
}, |
|||
slotClass() { |
|||
return [!this.loading ? 'show' : 'hide']; |
|||
}, |
|||
avatarShapeClass() { |
|||
return [this.avatarShape == 'round' ? 'lx-skeleton_avator__round' : '']; |
|||
}, |
|||
bannerClass() { |
|||
return [this.banner ? 'lx-skeleton_banner' : '']; |
|||
}, |
|||
}, |
|||
data() { |
|||
return {}; |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.lx-skeleton { |
|||
background-color: #fff; |
|||
padding: 12px; |
|||
} |
|||
|
|||
.lx-skeleton_avator__left { |
|||
display: flex; |
|||
width: 100%; |
|||
} |
|||
|
|||
.lx-skeleton_avator__left .avatar-class, |
|||
.lx-skeleton_avator__top .avatar-class { |
|||
background-color: #f2f3f5; |
|||
border-radius: 50%; |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
|
|||
.lx-skeleton_avator__left .avatar-class.lx-skeleton_avator__round, |
|||
.lx-skeleton_avator__top .avatar-class.lx-skeleton_avator__round { |
|||
border-radius: 0; |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
|
|||
.lx-skeleton_avator__left .avatar-class { |
|||
margin-right: 16px; |
|||
} |
|||
|
|||
.lx-skeleton_avator__top .avatar-class { |
|||
margin: 0 auto 12px auto; |
|||
} |
|||
|
|||
.row-class { |
|||
width: 100%; |
|||
height: 16px; |
|||
background-color: #f2f3f5; |
|||
margin-top: 12px; |
|||
} |
|||
|
|||
.row-class:first-child { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.row { |
|||
flex: 1; |
|||
} |
|||
|
|||
.lx-skeleton_avator__left .row { |
|||
width: calc(100% - 48px); |
|||
} |
|||
|
|||
.row-class:last-child { |
|||
width: 60%; |
|||
} |
|||
|
|||
.lx-skeleton_animation .row-class { |
|||
animation-duration: 1.5s; |
|||
animation-name: blink; |
|||
animation-timing-function: ease-in-out; |
|||
animation-iteration-count: infinite; |
|||
} |
|||
|
|||
@keyframes blink { |
|||
50% { |
|||
opacity: 0.6; |
|||
} |
|||
} |
|||
|
|||
.lx-skeleton_title { |
|||
width: 40%; |
|||
} |
|||
|
|||
.show { |
|||
display: block; |
|||
} |
|||
|
|||
.hide { |
|||
display: none; |
|||
} |
|||
|
|||
.lx-skeleton .lx-skeleton_banner { |
|||
width: 92%; |
|||
margin: 10px auto; |
|||
height: 64px; |
|||
border-radius: 0; |
|||
background-color: #f2f3f5; |
|||
} |
|||
</style> |
@ -0,0 +1,127 @@ |
|||
<template> |
|||
<!-- 时间间隔栏 --> |
|||
<!-- <Barrier /> --> |
|||
|
|||
<scroll-view |
|||
style="height: 100%" |
|||
:lower-threshold="100" |
|||
:scroll-y="true" |
|||
:upper-threshold="100" |
|||
: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> |
|||
// import Barrier from './component/Barrier.vue'; |
|||
import { mapState, mapMutations, mapGetters } from 'vuex'; |
|||
import { setPlaceholderTasks } from '@/utils/task'; |
|||
import TimeBox from './component/TimeBox.vue'; |
|||
|
|||
export default { |
|||
name: 'TimeLine', |
|||
components: { TimeBox }, |
|||
|
|||
data() { |
|||
return { top: 0 }; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['visibleRoles']), |
|||
...mapState('task', ['scrollTop', 'tasks', 'topEnd', 'bottomEnd', 'showSkeleton', 'timeNode', 'scrollToTaskId']), |
|||
...mapGetters('task', ['timeGranularity']), |
|||
}, |
|||
|
|||
methods: { |
|||
...mapMutations('task', ['setScrollTop', 'setShrink', 'setUpTasks', 'setDownTasks', 'setScrollToTaskId']), |
|||
|
|||
// 滚动 |
|||
scroll(e) { |
|||
console.log('e: ', e); |
|||
this.top = e.detail.scrollTop; |
|||
this.setShrink(this.top > this.scrollTop); |
|||
this.setScrollTop(this.top); |
|||
}, |
|||
|
|||
// 滚动到顶部 |
|||
async handleScrollTop() { |
|||
if (!this.tasks || !this.tasks.length || this.showSkeleton) return; |
|||
const startTime = this.tasks[0].planStart - 0; |
|||
if (this.topEnd) { |
|||
// 没有数据时 自动加载数据 |
|||
console.warn('滚动到顶部没有数据时: '); |
|||
const addTasks = setPlaceholderTasks(startTime, true, this.timeGranularity); |
|||
this.setUpTasks(addTasks); |
|||
} else { |
|||
// 有数据时 |
|||
console.warn('滚动到顶部有数据时: '); |
|||
const detailId = this.tasks.findIndex(task => task.detailId); |
|||
const timeNode = this.tasks[detailId].planStart - 0; |
|||
const upQuery = { |
|||
timeNode, |
|||
queryType: 0, |
|||
queryNum: 6, |
|||
}; |
|||
await this.$emit('getTasks', upQuery); |
|||
} |
|||
}, |
|||
|
|||
// 滚动到底部 |
|||
async handleScrollBottom() { |
|||
if (!this.tasks || !this.tasks.length || this.showSkeleton) return; |
|||
const { tasks, timeGranularity } = this; |
|||
const startTime = tasks[tasks.length - 1].planStart - 0; |
|||
if (this.bottomEnd) { |
|||
// 没有数据时 自动加载数据 |
|||
console.warn('滚动到底部没有数据时: '); |
|||
const addTasks = setPlaceholderTasks(startTime, false, this.timeGranularity); |
|||
this.setDownTasks(addTasks); |
|||
} else { |
|||
// 时间基准点=最后一个任务的开始时间+当前时间颗粒度 |
|||
console.warn('滚动到底部有数据时: '); |
|||
const arr = []; |
|||
this.tasks.forEach(task => { |
|||
if (task.detailId) { |
|||
arr.push(task); |
|||
} |
|||
}); |
|||
const nextQueryTime = +this.$t.time.add(+arr[arr.length - 1].planStart, 1, timeGranularity); |
|||
const downQuery = { |
|||
timeNode: nextQueryTime, |
|||
queryType: 1, |
|||
queryNum: 6, |
|||
}; |
|||
await this.$emit('getTasks', downQuery); |
|||
} |
|||
}, |
|||
|
|||
// 设置自动滚动位置 |
|||
setScrollPosition() { |
|||
// 如果storage里有taskId 滚动到这个id的任务 |
|||
const taskId = this.$t.storage.getStorageSync('taskId'); |
|||
if (taskId) { |
|||
this.setScrollToTaskId(`a${taskId}`); |
|||
this.$t.storage.setStorageSync('taskId', ''); // 记录后即刻清除本地存储 |
|||
} else { |
|||
const item = this.tasks.find(task => task.detailId); |
|||
if (item) { |
|||
this.setScrollToTaskId(`a${item.id}`); |
|||
} else { |
|||
// 没有本地记录的taskId |
|||
// 找到当前时间基准线的任务id 记录 并滚动到当前时间基准线 |
|||
const task = this.tasks.find(item => this.$moment(+item.planStart).isSame(this.timeNode, this.timeGranularity)); |
|||
task && this.setScrollToTaskId(`a${task.id}`); // 有这个task 就记录他的id |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,42 @@ |
|||
<!-- |
|||
* @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> |
|||
export default { |
|||
name: 'Barrier', |
|||
data() { |
|||
return {}; |
|||
}, |
|||
}; |
|||
</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> |
@ -0,0 +1,185 @@ |
|||
<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="show"> |
|||
<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="maskShow" @click="closeMask"></view> |
|||
<!-- 新建任务弹窗 --> |
|||
<CreateTask |
|||
:startTime="startTime" |
|||
:endTime="endTime" |
|||
:task="task" |
|||
@showTime="showTime" |
|||
@closeMask="closeMask" |
|||
class="thirdPopup flex transition-transform" |
|||
v-if="createTaskShow" |
|||
/> |
|||
|
|||
<u-picker title="开始时间" mode="time" v-model="showStart" :params="params" @confirm="confirmStartTime"></u-picker> |
|||
<u-picker title="结束时间" mode="time" v-model="showEnd" :params="params" @confirm="confirmEndTime"></u-picker> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import CreateTask from '../../Title/components/CreateTask.vue'; |
|||
|
|||
export default { |
|||
components: { CreateTask }, |
|||
|
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
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, |
|||
}, |
|||
}; |
|||
}, |
|||
|
|||
methods: { |
|||
//操作 |
|||
operation() { |
|||
// this.$t.ui.showToast('操作'); |
|||
this.show = !this.show; |
|||
}, |
|||
|
|||
// 新建项目 |
|||
createTask() { |
|||
// 关闭 ... 弹窗 |
|||
this.show = false; |
|||
// 打开遮罩 |
|||
this.maskShow = true; |
|||
// 打开新建项目弹窗 |
|||
this.createTaskShow = true; |
|||
}, |
|||
|
|||
//点击遮罩,关闭弹窗 |
|||
closeMask() { |
|||
// 关闭遮罩 |
|||
this.maskShow = false; |
|||
// 关闭分享项目弹窗 |
|||
this.secondShow = false; |
|||
// 关闭新建项目弹窗 |
|||
this.createTaskShow = false; |
|||
}, |
|||
|
|||
showTime() { |
|||
this.showStart = !this.showStart; |
|||
}, |
|||
|
|||
// 选择开始时间 |
|||
confirmStartTime(e) { |
|||
this.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`; |
|||
this.showEnd = true; |
|||
}, |
|||
|
|||
// 选择结束时间 |
|||
confirmEndTime(e) { |
|||
this.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`; |
|||
}, |
|||
}, |
|||
}; |
|||
</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> |
@ -0,0 +1,142 @@ |
|||
<template> |
|||
<view class="column"> |
|||
<!-- v-if="tasks && tasks.length" --> |
|||
<view> |
|||
<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="border-l-2 border-gray-300 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)" |
|||
:show-foot="false" |
|||
:show-head="false" |
|||
:style="{ height: setHeight(task.panel) }" |
|||
class="h-16" |
|||
margin="0" |
|||
v-if="tasks && tasks.length && task.process !== 4 && !showSkeleton" |
|||
> |
|||
任务面板插件 |
|||
<view slot="body"> |
|||
<view class="p-0 u-col-between"> |
|||
<view :key="pIndex" v-for="(row, pIndex) in task.plugins"> |
|||
<view class="grid gap-2" 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="styleType || 0" |
|||
v-for="plugin in row" |
|||
/> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</u-card> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<!-- 局部弹框操作栏 --> |
|||
<Tips /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex'; |
|||
import Skeleton from '@/components/Skeleton/Skeleton'; |
|||
import TimeStatus from './TimeStatus.vue'; |
|||
import TaskTools from './TaskTools.vue'; |
|||
|
|||
export default { |
|||
name: 'TimeBox', |
|||
components: { TimeStatus, Skeleton, TaskTools }, |
|||
|
|||
data() { |
|||
return { currentComponent: '', styleType: 0 }; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['roleId']), |
|||
...mapState('task', ['timeUnit', 'tasks', 'taskLoading', 'showSkeleton']), |
|||
...mapGetters('task', ['startTimeFormat']), |
|||
}, |
|||
|
|||
methods: { |
|||
...mapActions('task', ['getGlobal']), |
|||
...mapMutations('task', ['setTipsContent', 'setTipsContent']), |
|||
|
|||
// 设置任务面板高度 |
|||
setHeight(panel) { |
|||
if (panel && panel.height) { |
|||
return panel.height + 'px'; |
|||
} else { |
|||
return 'auto'; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 点击了定期任务的面板 更新可变的日常任务 |
|||
* @param {number} planStart 任务计划开始时间 |
|||
* @param {string} taskId 任务id |
|||
*/ |
|||
onClickTask(planStart, taskId) { |
|||
const param = { roleId: this.roleId, timeNode: planStart, timeUnit: this.timeUnit }; |
|||
this.getGlobal(param); |
|||
this.$t.storage.setStorageSync('taskId', taskId); |
|||
this.$t.storage.setStorageSync('roleId', this.roleId); |
|||
}, |
|||
}, |
|||
}; |
|||
</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; |
|||
} |
|||
::v-deep .ml-2 { |
|||
margin-left: 16px; |
|||
} |
|||
|
|||
::v-deep .ml-3 { |
|||
margin-left: 20px; |
|||
} |
|||
</style> |
@ -0,0 +1,231 @@ |
|||
<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>{{ 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>{{ durationText }}</template> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState, mapMutations } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'TimeStatus', |
|||
props: { task: { type: Object, default: () => {} } }, |
|||
|
|||
data() { |
|||
return { |
|||
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, |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('task', ['tip']), |
|||
status() { |
|||
return this.task ? this.task.process : 0; |
|||
}, |
|||
taskName() { |
|||
return this.task ? this.task.name : ''; |
|||
}, |
|||
taskId() { |
|||
return this.task ? this.task.id : ''; |
|||
}, |
|||
// 图标文本颜色 |
|||
// 任务状态 0未开始 1进行中 2暂停中 3已完成 |
|||
orderStyle() { |
|||
let color = '#9CA3AF'; |
|||
let icon = 'play-right-fill'; |
|||
let persent = 100; |
|||
switch (this.status) { |
|||
case 1: // 进行中 |
|||
color = '#60A5FA'; |
|||
icon = ''; |
|||
if (+this.computeCyclePersent() > 100) { |
|||
persent = 96; |
|||
} else { |
|||
persent = this.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 }; |
|||
}, |
|||
}, |
|||
|
|||
mounted() { |
|||
// TODO: 计算在不在窗口内显示 |
|||
const time = this.computeDurationText(); |
|||
this.updateDurationText(time); |
|||
}, |
|||
|
|||
destroyed() { |
|||
if (this.timer) { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
...mapMutations('task', ['setTip']), |
|||
|
|||
/** |
|||
* 点击了图标 修改任务状态 |
|||
* @param {object} event |
|||
*/ |
|||
changeStatus(process, event) { |
|||
if (process === 4) { |
|||
this.addTask(); |
|||
return; |
|||
} |
|||
// return false; |
|||
const { status, taskId, taskName, tip } = this; |
|||
tip.status = status; |
|||
tip.taskId = taskId; |
|||
tip.left = event.target.x; |
|||
tip.top = event.target.y; |
|||
tip.show = true; |
|||
tip.text = this.genetateTips(status, taskName); |
|||
|
|||
this.setTip(tip); |
|||
}, |
|||
|
|||
// 新建任务 |
|||
addTask() { |
|||
this.$t.ui.showToast('新建任务'); |
|||
}, |
|||
|
|||
// 计算圆环的弧度百分比 |
|||
computeCyclePersent() { |
|||
if (!this.task || !this.task.realStart || !this.task.planDuration) return 100; |
|||
const { realStart, planDuration } = this.task; |
|||
return (((Date.now() - +realStart) * 100) / +planDuration).toFixed(2); |
|||
}, |
|||
|
|||
/** |
|||
* 计算tip的标题内容 |
|||
*/ |
|||
genetateTips(status, content) { |
|||
switch (status) { |
|||
case 0: |
|||
return `确认开始任务"${content}"吗?`; |
|||
case 1: |
|||
return `请选择要执行的操作`; |
|||
case 2: |
|||
return `请选择要执行的操作`; |
|||
case 3: |
|||
return `是否要重新开始此任务`; |
|||
} |
|||
}, |
|||
|
|||
// 计算进行中状态剩余时间 |
|||
// 预计结束时间 = realStart(实际开始) + planDuration(计划时长) |
|||
// 剩余时间 = 预计结束时间 - 当前时间 |
|||
// 剩余时间 = realStart + planDuration - Date.now() |
|||
computeDurationText() { |
|||
const { realStart, planDuration } = this.task; |
|||
const leftTime = +realStart + +planDuration - Date.now(); // 剩余时间 |
|||
const { num, time } = this.$t.time.computeDurationText(leftTime); |
|||
if (num <= 0) { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
} |
|||
this.durationText = num; |
|||
return time; |
|||
}, |
|||
|
|||
updateDurationText(time) { |
|||
if (this.timer) { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
} |
|||
if (!time) return; |
|||
setInterval(() => { |
|||
this.computeDurationText(); |
|||
}, 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; |
|||
} |
|||
</style> |
@ -0,0 +1,7 @@ |
|||
<!-- |
|||
* @Author: aBin |
|||
* @email: binbin0314@126.com |
|||
* @Date: 2021-07-19 15:40:02 |
|||
* @LastEditors: aBin |
|||
* @LastEditTime: 2021-07-19 15:40:03 |
|||
--> |
@ -0,0 +1,95 @@ |
|||
<template> |
|||
<view |
|||
class="fixed shadow-2xl" |
|||
style="z-index: 1000" |
|||
:style="{ |
|||
left: tip.left + 'px', |
|||
top: height - tip.top > 110 ? tip.top + 'px' : '', |
|||
bottom: 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="headStyle" |
|||
:footStyle="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> |
|||
import { mapState, mapMutations } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'Tips', |
|||
props: { title: { default: '提示', type: String } }, |
|||
|
|||
computed: mapState('task', ['tip']), |
|||
|
|||
data() { |
|||
return { |
|||
footStyle: { padding: '4px 15px' }, |
|||
headStyle: { paddingTop: '8px', paddingBottom: '8px' }, |
|||
height: 0, |
|||
}; |
|||
}, |
|||
|
|||
mounted() { |
|||
const system = uni.getSystemInfoSync(); |
|||
this.height = system.windowHeight; |
|||
}, |
|||
|
|||
methods: { |
|||
...mapMutations('task', ['setTipShow']), |
|||
// 点击了确认 |
|||
onConfirm() { |
|||
this.onCancel(); |
|||
}, |
|||
|
|||
/** |
|||
* 执行修改任务状态的动作 |
|||
* @param {number} type 状态码 0开始 1暂停 2继续 3完成 默认0 |
|||
*/ |
|||
async onChangeStatus(type) { |
|||
try { |
|||
const param = { id: this.tip.taskId, type }; |
|||
await uni.$u.api.updateTaskType(param); |
|||
if (type === 0) { |
|||
this.$t.ui.showToast('项目已重新开始'); |
|||
} else if (type === 1) { |
|||
this.$t.ui.showToast('项目已暂停'); |
|||
} else if (type === 2) { |
|||
this.$t.ui.showToast('项目继续'); |
|||
} else if (type === 3) { |
|||
this.$t.ui.showToast('项目结束'); |
|||
} |
|||
this.tip.show = false; |
|||
// TODO: 更新界面 不要整体刷新 |
|||
// location.reload(); |
|||
// this.$router.go(0); |
|||
} catch (error) { |
|||
console.error(error); |
|||
this.$t.ui.showToast(error.msg || '操作失败'); |
|||
} |
|||
}, |
|||
|
|||
// 点击了取消 |
|||
onCancel() { |
|||
this.setTipShow(false); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,233 @@ |
|||
<template> |
|||
<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="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="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>新建任务</view> |
|||
</view> |
|||
<view class="flex pt-3"> |
|||
<u-icon name="share" size="32" style="margin: 0 15px 3px 0"></u-icon> |
|||
<view @click="share">分享项目</view> |
|||
</view> |
|||
</view> --> |
|||
<!-- 分享项目弹窗 --> |
|||
<ShareProject v-if="secondShow" class="second-popup" /> |
|||
<!-- 新建任务弹窗 --> |
|||
<CreateTask |
|||
:startTime="startTime" |
|||
:endTime="endTime" |
|||
@showTime="showTime" |
|||
@closeMask="closeMask" |
|||
class="third-popup flex transition-transform" |
|||
v-if="createTaskShow" |
|||
/> |
|||
|
|||
<u-picker title="开始时间" mode="time" v-model="showStart" :params="params" @confirm="confirmStartTime"></u-picker> |
|||
<u-picker title="结束时间" mode="time" v-model="showEnd" :params="params" @confirm="confirmEndTime"></u-picker> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters, mapState } from 'vuex'; |
|||
import CreateTask from './components/CreateTask.vue'; |
|||
import ShareProject from './components/ShareProject.vue'; |
|||
|
|||
export default { |
|||
name: 'ProjectTitle', |
|||
components: { CreateTask, ShareProject }, |
|||
data() { |
|||
return { |
|||
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, |
|||
}, |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('project', ['project']), |
|||
...mapGetters('user', ['userId']), |
|||
}, |
|||
|
|||
methods: { |
|||
showTime() { |
|||
this.showStart = !this.showStart; |
|||
}, |
|||
|
|||
// 选择开始时间 |
|||
confirmStartTime(e) { |
|||
this.startTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`; |
|||
this.showEnd = true; |
|||
}, |
|||
|
|||
// 选择结束时间 |
|||
confirmEndTime(e) { |
|||
this.endTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`; |
|||
}, |
|||
|
|||
// 点击返回按钮 |
|||
onBack() { |
|||
// eslint-disable-next-line no-undef |
|||
uni.navigateBack(); |
|||
// const pages = getCurrentPages(); // 获取页面栈数组 |
|||
// console.log('历史pages: ', pages.length); |
|||
// if (pages.length > 1) { |
|||
// } else { |
|||
// // this.$u.route('/', { u: this.userId }); |
|||
// uni.webView.reLaunch({ url: `/pages/index/index?u=${this.userId}` }); |
|||
// } |
|||
}, |
|||
|
|||
// LWBS提示 |
|||
lwbs() { |
|||
// this.$t.ui.showToast('LWBS'); |
|||
}, |
|||
//项目概览 |
|||
projectOverview() { |
|||
// this.$t.ui.showToast('项目概览'); |
|||
}, |
|||
// 回到首页 |
|||
openIndex() { |
|||
console.log(111); |
|||
uni.webView.reLaunch({ url: `/pages/index/index?u=${this.userId}` }); |
|||
}, |
|||
|
|||
//操作 |
|||
operation() { |
|||
// this.$t.ui.showToast('操作'); |
|||
this.show = !this.show; |
|||
}, |
|||
|
|||
// 新建项目 |
|||
createTask() { |
|||
// 关闭 ... 弹窗 |
|||
this.show = false; |
|||
// 打开遮罩 |
|||
this.maskShow = true; |
|||
// 打开新建项目弹窗 |
|||
this.createTaskShow = true; |
|||
}, |
|||
|
|||
//分享项目 |
|||
share() { |
|||
// 关闭 ... 弹窗 |
|||
this.show = false; |
|||
// 打开遮罩 |
|||
this.maskShow = true; |
|||
// 打开分享项目弹窗 |
|||
this.secondShow = true; |
|||
}, |
|||
|
|||
//点击遮罩,关闭弹窗 |
|||
closeMask() { |
|||
// 关闭遮罩 |
|||
this.maskShow = false; |
|||
// 关闭分享项目弹窗 |
|||
this.secondShow = false; |
|||
// 关闭新建项目弹窗 |
|||
this.createTaskShow = false; |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.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> |
@ -0,0 +1,460 @@ |
|||
<template> |
|||
<div class="new-projects-box"> |
|||
<div class="form"> |
|||
<!-- 项目名称 --> |
|||
<view class="mb-3 font-bold text-base flex justify-center">新建任务</view> |
|||
<div class="flex items-center mb-2"> |
|||
<div>名称<span class="text-red-500">*</span>:</div> |
|||
<u-input max-length="5" v-model="name" :type="type" :border="border" /> |
|||
</div> |
|||
<!-- 起止时间 --> |
|||
<div class="mb-2"> |
|||
<div>起止时间:</div> |
|||
<u-input placeholder="请选择起止时间" v-model="timeValue" :type="type" :border="border" @click="$emit('showTime')" /> |
|||
</div> |
|||
<!-- 多选框 --> |
|||
<div class="flex justify-between items-center"> |
|||
<div>负责人<span class="text-red-500">*</span>:</div> |
|||
<div class="flex-1" v-if="hasRole">{{ roleName }}</div> |
|||
<div label="负责人" class="flex-1" v-else> |
|||
<u-dropdown disabled ref="uDropdown" placeholder="请选择负责人"> |
|||
<u-dropdown-item :title="roleList"> |
|||
<view class="slot-content bg-white"> |
|||
<div |
|||
class="flex flex-row justify-between mb-1 drop-item" |
|||
v-for="(role, roleIndex) in roleOptions" |
|||
:key="roleIndex" |
|||
@click="change(roleIndex)" |
|||
> |
|||
<view v-model="role.id">{{ role.name }}</view> |
|||
<u-icon v-if="role.dropdownShow" name="checkbox-mark" color="#2979ff" size="28"></u-icon> |
|||
</div> |
|||
</view> |
|||
</u-dropdown-item> |
|||
</u-dropdown> |
|||
</div> |
|||
</div> |
|||
<!-- 下拉图标 --> |
|||
<div class="flex justify-center my-6"> |
|||
<u-icon v-if="arrow" name="arrow-down" size="28" @click="openDropdown"></u-icon> |
|||
<u-icon v-else name="arrow-up" size="28" @click="closeSecondDropdown"></u-icon> |
|||
</div> |
|||
<!-- 下拉框的内容 --> |
|||
<div v-if="show" class="mb-6"> |
|||
<!-- 描述 --> |
|||
<div class="flex items-center mb-2"> |
|||
<div>描述:</div> |
|||
<u-input v-model="description" max-length="48" type="textarea" height="36" auto-height :border="border" /> |
|||
</div> |
|||
<!-- 所属项目 --> |
|||
<div class="w flex items-center mb-2"> |
|||
<div>所属项目<span class="text-red-500">*</span>:</div> |
|||
<div>{{ project.name }}</div> |
|||
</div> |
|||
<!-- 所属任务 --> |
|||
<div class="w flex items-center mb-2" v-if="task && task.id"> |
|||
<div>所属任务:</div> |
|||
<div>{{ task.name }}</div> |
|||
</div> |
|||
<!-- 上道工序 --> |
|||
<div class="flex items-center mb-2"> |
|||
<div>上道工序:</div> |
|||
<InputSearch |
|||
@searchPrevTask="searchPrevTask" |
|||
:dataSource="allTasks" |
|||
@select="handleChange" |
|||
@clearAllTasks="clearAllTasks" |
|||
placeholder="请输入上道工序" |
|||
/> |
|||
</div> |
|||
<!-- 检查人多选框 --> |
|||
<div class="flex justify-between items-center"> |
|||
<div>检查人<span class="text-red-500">*</span>:</div> |
|||
<div label="检查人" class="flex-1"> |
|||
<u-dropdown ref="dropdown"> |
|||
<u-dropdown-item :title="checkerList"> |
|||
<view class="slot-content bg-white"> |
|||
<div |
|||
class="flex flex-row justify-between mb-1 drop-item" |
|||
v-for="(checkoutOption, Index) in checkoutOptions" |
|||
:key="Index" |
|||
@click="choose(Index)" |
|||
> |
|||
<view v-model="checkoutOption.value">{{ checkoutOption.name }}</view> |
|||
<u-icon v-if="checkoutOption.dropdownShow" name="checkbox-mark" color="#2979ff" size="28"></u-icon> |
|||
</div> |
|||
</view> |
|||
</u-dropdown-item> |
|||
</u-dropdown> |
|||
</div> |
|||
</div> |
|||
<!-- 是否是日常任务 --> |
|||
<div class="flex justify-between items-center mt-6"> |
|||
是否是日常任务: |
|||
<u-switch v-model="isGlobal" size="28"></u-switch> |
|||
</div> |
|||
<div class="mt-6"> |
|||
<div>交付物:</div> |
|||
<div v-for="(sort, sortIndex) in deliverSort" :key="sortIndex"> |
|||
<u-input |
|||
@blur="addDeliverInput" |
|||
v-model="sort.name" |
|||
:placeholder="`交付物名称${sortIndex + 1}`" |
|||
:type="type" |
|||
:border="border" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="flex items-center mb-6"> |
|||
<u-button type="primary" size="medium" @click="setParameters">提交</u-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState, mapGetters, mapMutations } 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']), |
|||
|
|||
// 负责人下拉多选选中 |
|||
change(index) { |
|||
let arr = [...this.roleOptions]; |
|||
// 选择多选项图标的展示 |
|||
arr[index].dropdownShow = !arr[index].dropdownShow; |
|||
// 多选展示的改变 |
|||
this.roleList = arr[index].name; |
|||
let shows = ''; |
|||
// 遍历arr,如果选中,添加到多选展示框上 |
|||
arr.map(val => { |
|||
if (val.dropdownShow === true) { |
|||
shows += val.name + ','; |
|||
this.roleIdList.push(val.id); |
|||
} |
|||
}); |
|||
this.roleOptions = [...arr]; |
|||
// 删除最后的',' |
|||
this.roleList = shows.slice(0, shows.length - 1); |
|||
}, |
|||
|
|||
// 检查人下拉多选选中 |
|||
choose(index) { |
|||
let arr = [...this.checkoutOptions]; |
|||
// 选择多选项图标的展示 |
|||
arr[index].dropdownShow = !arr[index].dropdownShow; |
|||
// 多选展示的改变 |
|||
this.checkerList = arr[index].name; |
|||
let shows = ''; |
|||
// 遍历arr,如果选中,添加到多选展示框上 |
|||
arr.map(val => { |
|||
if (val.dropdownShow === true) { |
|||
shows += val.name + ','; |
|||
this.checkerIdList.push(val.id); |
|||
} |
|||
}); |
|||
this.checkoutOptions = [...arr]; |
|||
// 删除最后的',' |
|||
this.checkerList = shows.slice(0, shows.length - 1); |
|||
// this.roleList = arr[value - 1].name; |
|||
}, |
|||
|
|||
// 打开下拉框 |
|||
openDropdown() { |
|||
this.arrow = !this.arrow; |
|||
this.show = true; |
|||
}, |
|||
|
|||
// 关闭下拉框 |
|||
closeSecondDropdown() { |
|||
this.arrow = !this.arrow; |
|||
this.show = false; |
|||
}, |
|||
|
|||
/** |
|||
* 模糊查询 查找项目下的任务 |
|||
* @param name 任务名 |
|||
* @param projectId 项目id |
|||
*/ |
|||
async searchPrevTask(val) { |
|||
try { |
|||
const params = { name: val, projectId: this.projectId }; |
|||
const data = await this.$u.api.queryTaskOfProject(params); |
|||
this.allTasks = data; |
|||
return data; |
|||
} catch (error) { |
|||
console.error('error: ', error); |
|||
} |
|||
}, |
|||
|
|||
//用户点击获取的数据 |
|||
handleChange(data) { |
|||
console.log('data', data); |
|||
this.processTaskId = data.detailId; |
|||
}, |
|||
// 清空模糊查询信息 |
|||
clearAllTasks() { |
|||
this.allTasks = []; |
|||
}, |
|||
|
|||
// 数组最后一项有值 添加一条交付物输入框 |
|||
addDeliverInput() { |
|||
if (this.deliverSort[this.deliverSort.length - 1].name) { |
|||
this.deliverSort.push({ name: '' }); |
|||
} |
|||
}, |
|||
|
|||
// 设置提交参数 |
|||
async setParameters() { |
|||
const { |
|||
projectId, |
|||
task, |
|||
name, |
|||
startTime, |
|||
endTime, |
|||
hasRole, |
|||
roleIdList, |
|||
roleId, |
|||
description, |
|||
processTaskId, |
|||
checkerIdList, |
|||
isGlobal, |
|||
} = this; |
|||
if (!name) { |
|||
this.$t.ui.showToast('请输入名称'); |
|||
return; |
|||
} |
|||
if ((!roleIdList || !roleIdList.length) && !hasRole) { |
|||
this.$t.ui.showToast('请选择负责人'); |
|||
return; |
|||
} |
|||
if (!checkerIdList || !checkerIdList.length) { |
|||
this.$t.ui.showToast('请选择检查人'); |
|||
return; |
|||
} |
|||
const deliverList = []; |
|||
this.deliverSort.forEach(item => { |
|||
if (item.name) { |
|||
deliverList.push(item.name); |
|||
} |
|||
}); |
|||
const params = { |
|||
name, |
|||
startTime: startTime ? this.$moment(startTime).format('x') - 0 : '', |
|||
endTime: endTime ? this.$moment(endTime).format('x') - 0 : '', |
|||
roleIdList: hasRole ? [roleId] : roleIdList, |
|||
description, |
|||
projectId, |
|||
parentTaskId: task && task.id ? task.id : '', // 父任务 |
|||
processTaskId, // 上道工序 TODO |
|||
checkerIdList, |
|||
global: isGlobal ? 1 : 0, |
|||
deliverList, |
|||
}; |
|||
await this.handleSubmit(params); |
|||
}, |
|||
|
|||
/** |
|||
* 新建任务 |
|||
* @param name 任务名 |
|||
* @param startTime 开始时间 |
|||
* @param endTime 结束时间 |
|||
* @param roleIdList 负责人id(数组) |
|||
* @param description 描述 |
|||
* @param projectId 所属项目id |
|||
* @param parentTaskId 所属任务id |
|||
* @param processTaskId 上道工序(任务id) |
|||
* @param checkerIdList 检查人id(数组) |
|||
* @param global 是否日常任务 0否 1是 |
|||
* @param deliverList 交付物名字(数组) |
|||
*/ |
|||
async handleSubmit(params) { |
|||
try { |
|||
const data = await this.$u.api.saveTask(params); |
|||
// TODO 任务新建成功 继续 or 取消 |
|||
this.$emit('closeMask'); |
|||
const newTasks = { |
|||
data: data[0], |
|||
processTaskId: params.processTaskId, |
|||
}; |
|||
// 将新加的任务加到store |
|||
// 判断不是子任务 |
|||
if (!this.task || !this.task.id) { |
|||
this.addNewTasks(newTasks); |
|||
} |
|||
} catch (error) { |
|||
this.$emit('closeMask'); |
|||
console.error('error: ', error); |
|||
} |
|||
}, |
|||
|
|||
// 添加任务后更新tasks |
|||
addNewTasks(data) { |
|||
const oldTasks = this.$u.deepClone(this.tasks); |
|||
let res = data.data; |
|||
// 判断有没有选择上道工序 |
|||
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]); |
|||
}, |
|||
}, |
|||
}; |
|||
</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> |
@ -0,0 +1,210 @@ |
|||
<template> |
|||
<view class="flex justify-center"> |
|||
<view class="content p-3 pb-8"> |
|||
<view class="mb-3 font-bold text-base flex justify-center">创建分享链接</view> |
|||
<view class="flex flex-col"> |
|||
<view class="mb-1">用户以什么角色加入项目</view> |
|||
<!-- 下拉多选 --> |
|||
<view class="uni-list"> |
|||
<view class="uni-list-cell"> |
|||
<view class="uni-list-cell-db ml-2" v-if="rolesArray.length"> |
|||
<picker @change="changeRole" :value="index" :range="rolesArray"> |
|||
<view class="uni-input">{{ allRolesName[index].name }}</view> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 复制链接 --> |
|||
<view class="link flex items-center mt-4"> |
|||
<view class="link-url">{{ links }}</view> |
|||
<u-button |
|||
style="border-radius: 0; height: 100%" |
|||
type="primary" |
|||
v-clipboard:copy="copyText" |
|||
v-clipboard:success="copySuccess" |
|||
v-clipboard:error="copyError" |
|||
> |
|||
复制链接 |
|||
</u-button> |
|||
</view> |
|||
<view @click="select"> |
|||
<!-- 全选按钮 --> |
|||
<!-- <view class="flex mt-4"> |
|||
<view> |
|||
<u-checkbox-group> |
|||
<u-checkbox v-model="checked" @change="checkedAll"></u-checkbox> |
|||
</u-checkbox-group> |
|||
</view> |
|||
<view>已选择({{ this.quantity }})</view> |
|||
<view style="color: #f37378; margin-left: 20px">批量删除</view> |
|||
</view> --> |
|||
<!-- 多选框 --> |
|||
<!-- <view> |
|||
<u-checkbox-group class="checkboxs flex flex-1 items-center mt-4" v-for="(item, index) in list" :key="index"> |
|||
<div class="flex-1 flex items-center"> |
|||
<u-checkbox v-model="item.checked"></u-checkbox> |
|||
<u-avatar :src="item.src" size="55" style="background: #d8dce0; margin-right: 10px"></u-avatar> |
|||
<div style="width: 60%; font-size: 12px"> |
|||
<div style="color: gray">{{ item.name }}</div> |
|||
<div style="color: #c4d0e1">{{ item.joinMethod }}</div> |
|||
</div> |
|||
</div> |
|||
</u-checkbox-group> |
|||
</view> --> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters, mapState } from 'vuex'; |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
rolesArray: [], |
|||
allRolesName: [], |
|||
index: 0, |
|||
links: '', //复制的链接 |
|||
copyText: '', |
|||
checked: false, //全选按钮是否选中 |
|||
roleName: '观众', |
|||
// 多选框列表 |
|||
list: [ |
|||
{ |
|||
name: '冯老师', |
|||
src: '', |
|||
joinMethod: '文件创建者', |
|||
role: '观众', |
|||
checked: false, |
|||
disabled: false, |
|||
}, |
|||
{ |
|||
name: '马壮', |
|||
src: '', |
|||
joinMethod: '通过链接加入', |
|||
role: '干系人', |
|||
checked: false, |
|||
disabled: false, |
|||
}, |
|||
{ |
|||
name: '张野', |
|||
src: '', |
|||
joinMethod: '通过链接加入', |
|||
role: '观众', |
|||
checked: false, |
|||
disabled: false, |
|||
}, |
|||
], |
|||
quantity: 0, //多选里面已选择的数量 |
|||
path: '', |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['visibleRoles', 'invisibleRoles']), |
|||
...mapState('project', ['project']), |
|||
...mapGetters('project', ['projectId']), |
|||
}, |
|||
|
|||
mounted() { |
|||
this.$nextTick(() => { |
|||
this.path = window.location.href.split('?')[0]; |
|||
const { path, projectId } = this; |
|||
const params = { path: `${path}`, projectId, roleId: '0' }; |
|||
this.creatShare(params); |
|||
}); |
|||
|
|||
if (this.visibleRoles.length || this.invisibleRoles.length) { |
|||
const arr = this.visibleRoles.concat(this.invisibleRoles); |
|||
arr.forEach(role => { |
|||
let item = { id: '', name: '' }; |
|||
item.id = role.id; |
|||
item.name = role.name; |
|||
this.allRolesName.push(item); |
|||
this.rolesArray.push(role.name); |
|||
}); |
|||
const firstItem = { id: '0', name: '观众' }; |
|||
this.allRolesName.unshift(firstItem); |
|||
this.rolesArray.unshift('观众'); |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
// 选择角色 |
|||
async changeRole(e) { |
|||
this.index = e.target.value; |
|||
this.roleName = this.allRolesName[this.index].name; |
|||
const { path, projectId } = this; |
|||
const params = { path, projectId, roleId: this.allRolesName[this.index].id }; |
|||
await this.creatShare(params); |
|||
}, |
|||
|
|||
// 复制成功 |
|||
copySuccess() { |
|||
this.$t.ui.showToast('复制成功'); |
|||
}, |
|||
// 复制失败 |
|||
copyError() { |
|||
this.$t.ui.showToast('复制失败,请稍后重试'); |
|||
}, |
|||
|
|||
/** |
|||
* 创建分享链接 |
|||
* @param path 路径前缀 |
|||
* @param projectId 项目id |
|||
* @param roleId 角色id |
|||
*/ |
|||
async creatShare(params) { |
|||
try { |
|||
const data = await this.$u.api.createShare(params); |
|||
this.links = data.path; |
|||
this.copyText = `邀请您加入${this.project.name}的项目,角色为${this.roleName},链接为${data.path}&url=${this.$t.domain}`; |
|||
} catch (error) { |
|||
console.error('error: ', error); |
|||
} |
|||
}, |
|||
|
|||
//已选择 |
|||
select() { |
|||
this.quantity = 0; |
|||
this.list.forEach(val => { |
|||
if (val.checked == true) { |
|||
this.quantity++; |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 全选 |
|||
checkedAll() { |
|||
this.list.map(val => { |
|||
val.checked = !this.checked; |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.content { |
|||
width: 100%; |
|||
max-height: 400px; |
|||
} |
|||
|
|||
.link { |
|||
height: 40px; |
|||
border: 1px solid #afbed1; |
|||
} |
|||
|
|||
.link-url { |
|||
color: #afbed1; |
|||
width: 80%; |
|||
line-height: 40px; |
|||
margin-left: 5px; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
} |
|||
</style> |
@ -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(); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
@ -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], |
|||
}; |
@ -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> |
@ -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> |
@ -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> |
@ -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: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层 |
|||
// 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> |
@ -0,0 +1,276 @@ |
|||
<template> |
|||
<view |
|||
v-if="isShow" |
|||
ref="ani" |
|||
class="uni-transition" |
|||
:class="[ani.in]" |
|||
:style="'transform:' + transform + ';' + stylesObject" |
|||
@click="change" |
|||
> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
// #ifdef APP-NVUE |
|||
const animation = uni.requireNativePlugin('animation'); |
|||
// #endif |
|||
/** |
|||
* Transition 过渡动画 |
|||
* @description 简单过渡动画组件 |
|||
* @tutorial https://ext.dcloud.net.cn/plugin?id=985 |
|||
* @property {Boolean} show = [false|true] 控制组件显示或隐藏 |
|||
* @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型 |
|||
* @value fade 渐隐渐出过渡 |
|||
* @value slide-top 由上至下过渡 |
|||
* @value slide-right 由右至左过渡 |
|||
* @value slide-bottom 由下至上过渡 |
|||
* @value slide-left 由左至右过渡 |
|||
* @value zoom-in 由小到大过渡 |
|||
* @value zoom-out 由大到小过渡 |
|||
* @property {Number} duration 过渡动画持续时间 |
|||
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red` |
|||
*/ |
|||
export default { |
|||
name: 'uniTransition', |
|||
props: { |
|||
show: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
modeClass: { |
|||
type: Array, |
|||
default() { |
|||
return []; |
|||
}, |
|||
}, |
|||
duration: { |
|||
type: Number, |
|||
default: 300, |
|||
}, |
|||
styles: { |
|||
type: Object, |
|||
default() { |
|||
return {}; |
|||
}, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
isShow: false, |
|||
transform: '', |
|||
ani: { in: '', active: '' }, |
|||
}; |
|||
}, |
|||
watch: { |
|||
show: { |
|||
handler(newVal) { |
|||
if (newVal) { |
|||
this.open(); |
|||
} else { |
|||
this.close(); |
|||
} |
|||
}, |
|||
immediate: true, |
|||
}, |
|||
}, |
|||
computed: { |
|||
stylesObject() { |
|||
let styles = { |
|||
...this.styles, |
|||
'transition-duration': this.duration / 1000 + 's', |
|||
}; |
|||
let transfrom = ''; |
|||
for (let i in styles) { |
|||
let line = this.toLine(i); |
|||
transfrom += line + ':' + styles[i] + ';'; |
|||
} |
|||
return transfrom; |
|||
}, |
|||
}, |
|||
created() { |
|||
// this.timer = null |
|||
// this.nextTick = (time = 50) => new Promise(resolve => { |
|||
// clearTimeout(this.timer) |
|||
// this.timer = setTimeout(resolve, time) |
|||
// return this.timer |
|||
// }); |
|||
}, |
|||
methods: { |
|||
change() { |
|||
this.$emit('click', { detail: this.isShow }); |
|||
}, |
|||
open() { |
|||
clearTimeout(this.timer); |
|||
this.isShow = true; |
|||
this.transform = ''; |
|||
this.ani.in = ''; |
|||
for (let i in this.getTranfrom(false)) { |
|||
if (i === 'opacity') { |
|||
this.ani.in = 'fade-in'; |
|||
} else { |
|||
this.transform += `${this.getTranfrom(false)[i]} `; |
|||
} |
|||
} |
|||
this.$nextTick(() => { |
|||
setTimeout(() => { |
|||
this._animation(true); |
|||
}, 50); |
|||
}); |
|||
}, |
|||
close() { |
|||
clearTimeout(this.timer); |
|||
this._animation(false); |
|||
}, |
|||
_animation(type) { |
|||
let styles = this.getTranfrom(type); |
|||
// #ifdef APP-NVUE |
|||
if (!this.$refs['ani']) return; |
|||
animation.transition( |
|||
this.$refs['ani'].ref, |
|||
{ |
|||
styles, |
|||
duration: this.duration, //ms |
|||
timingFunction: 'ease', |
|||
needLayout: false, |
|||
delay: 0, //ms |
|||
}, |
|||
() => { |
|||
if (!type) { |
|||
this.isShow = false; |
|||
} |
|||
this.$emit('change', { detail: this.isShow }); |
|||
}, |
|||
); |
|||
// #endif |
|||
// #ifndef APP-NVUE |
|||
this.transform = ''; |
|||
for (let i in styles) { |
|||
if (i === 'opacity') { |
|||
this.ani.in = `fade-${type ? 'out' : 'in'}`; |
|||
} else { |
|||
this.transform += `${styles[i]} `; |
|||
} |
|||
} |
|||
this.timer = setTimeout(() => { |
|||
if (!type) { |
|||
this.isShow = false; |
|||
} |
|||
this.$emit('change', { detail: this.isShow }); |
|||
}, this.duration); |
|||
// #endif |
|||
}, |
|||
getTranfrom(type) { |
|||
let styles = { transform: '' }; |
|||
this.modeClass.forEach(mode => { |
|||
switch (mode) { |
|||
case 'fade': |
|||
styles.opacity = type ? 1 : 0; |
|||
break; |
|||
case 'slide-top': |
|||
styles.transform += `translateY(${type ? '0' : '-100%'}) `; |
|||
break; |
|||
case 'slide-right': |
|||
styles.transform += `translateX(${type ? '0' : '100%'}) `; |
|||
break; |
|||
case 'slide-bottom': |
|||
styles.transform += `translateY(${type ? '0' : '100%'}) `; |
|||
break; |
|||
case 'slide-left': |
|||
styles.transform += `translateX(${type ? '0' : '-100%'}) `; |
|||
break; |
|||
case 'zoom-in': |
|||
styles.transform += `scale(${type ? 1 : 0.8}) `; |
|||
break; |
|||
case 'zoom-out': |
|||
styles.transform += `scale(${type ? 1 : 1.2}) `; |
|||
break; |
|||
} |
|||
}); |
|||
return styles; |
|||
}, |
|||
_modeClassArr(type) { |
|||
let mode = this.modeClass; |
|||
if (typeof mode !== 'string') { |
|||
let modestr = ''; |
|||
mode.forEach(item => { |
|||
modestr += item + '-' + type + ','; |
|||
}); |
|||
return modestr.substr(0, modestr.length - 1); |
|||
} else { |
|||
return mode + '-' + type; |
|||
} |
|||
}, |
|||
// getEl(el) { |
|||
// console.log(el || el.ref || null); |
|||
// return el || el.ref || null |
|||
// }, |
|||
toLine(name) { |
|||
return name.replace(/([A-Z])/g, '-$1').toLowerCase(); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
.uni-transition { |
|||
transition-timing-function: ease; |
|||
transition-duration: 0.3s; |
|||
transition-property: transform, opacity; |
|||
} |
|||
|
|||
.fade-in { |
|||
opacity: 0; |
|||
} |
|||
|
|||
.fade-active { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.slide-top-in { |
|||
/* transition-property: transform, opacity; */ |
|||
transform: translateY(-100%); |
|||
} |
|||
|
|||
.slide-top-active { |
|||
transform: translateY(0); |
|||
/* opacity: 1; */ |
|||
} |
|||
|
|||
.slide-right-in { |
|||
transform: translateX(100%); |
|||
} |
|||
|
|||
.slide-right-active { |
|||
transform: translateX(0); |
|||
} |
|||
|
|||
.slide-bottom-in { |
|||
transform: translateY(100%); |
|||
} |
|||
|
|||
.slide-bottom-active { |
|||
transform: translateY(0); |
|||
} |
|||
|
|||
.slide-left-in { |
|||
transform: translateX(-100%); |
|||
} |
|||
|
|||
.slide-left-active { |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
|
|||
.zoom-in-in { |
|||
transform: scale(0.8); |
|||
} |
|||
|
|||
.zoom-out-active { |
|||
transform: scale(1); |
|||
} |
|||
|
|||
.zoom-out-in { |
|||
transform: scale(1.2); |
|||
} |
|||
</style> |
@ -0,0 +1,3 @@ |
|||
export const db = null; // indexedDB 对象
|
|||
export const name = 'TALL_indexedDB'; // indexDB name
|
|||
export const version = 1; // indexDB version
|
@ -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列表
|
|||
}; |
@ -0,0 +1,2 @@ |
|||
// 每页加载颗粒度的个数
|
|||
export default { pageCount: 10 }; |
@ -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: '' }, |
|||
], |
|||
}; |
@ -0,0 +1,425 @@ |
|||
<template> |
|||
<view :style="{ height: height }" class="flex flex-col overflow-hidden u-font-14"> |
|||
<!-- 标题栏 --> |
|||
<Title /> |
|||
|
|||
<view class="container flex flex-col flex-1 overflow-hidden bg-gray-100"> |
|||
<!-- 角色栏 --> |
|||
<Roles /> |
|||
|
|||
<!-- 日常任务面板 --> |
|||
<Globals /> |
|||
|
|||
<!-- 定期任务面板 --> |
|||
<TimeLine @getTasks="getTasks" class="flex-1 overflow-hidden" ref="timeLine" /> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'; |
|||
import { setPlaceholderTasks, computeFillPlaceholderTaskCount } from '@/utils/task'; |
|||
import { flatten } from 'lodash'; |
|||
|
|||
export default { |
|||
data() { |
|||
return { height: '', show: false, questionnaires: [], count: 0, chooseItem: false }; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('user', ['user', 'token']), |
|||
...mapState('role', ['visibleRoles', 'roleId']), |
|||
...mapState('task', ['timeNode', 'timeUnit', 'tasks', 'regularTask', 'newProjectInfo', 'showSkeleton', 'showScrollTo']), |
|||
...mapState('project', ['project']), |
|||
...mapGetters('task', ['timeGranularity']), |
|||
...mapGetters('project', ['projectId']), |
|||
...mapGetters('user', ['userId']), |
|||
}, |
|||
|
|||
onLoad(options) { |
|||
console.log('options: ', options); |
|||
if (options.share && options.share === '1') { |
|||
this.shareInit(options); |
|||
} else { |
|||
this.init(options); |
|||
} |
|||
}, |
|||
|
|||
watch: { |
|||
/** |
|||
* 当时间基准点发生变化时 |
|||
* 重新根据时间和角色查询普通日常任务 |
|||
* 永久日常任务不发生改变 |
|||
*/ |
|||
timeNode(val) { |
|||
console.log('val: ', val); |
|||
if (val && this.roleId) { |
|||
this.clearTasksData(); |
|||
this.getGlobalData(); // 查可变日常任务 |
|||
this.initPlanTasks(); // 处理定期任务 |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 当角色发生变化时 |
|||
* 重新查询永久日常任务和普通日常任务 |
|||
* 注意: 切换角色后 重新设置了时间基准点 时间基准点一定会变 |
|||
* 所以监听时间基准点获取 可变日常任务即可 这里不用获取 避免重复获取 |
|||
*/ |
|||
roleId(val) { |
|||
if (val) { |
|||
this.setTimeNode(Date.now()); |
|||
// 根据角色查找永久的日常任务 |
|||
const params = { roleId: val, projectId: this.projectId }; |
|||
this.getPermanent(params); |
|||
} |
|||
}, |
|||
|
|||
// 收到跳转新项目的消息 |
|||
newProjectInfo(val) { |
|||
if (val && val.projectId && val.url) { |
|||
this.$u.route('/', { u: this.userId, p: val.projectId, url: val.url }); |
|||
this.clearTasksData(); |
|||
this.setRoleId(''); |
|||
const options = this.$route.query; |
|||
this.init(options); |
|||
} |
|||
}, |
|||
}, |
|||
|
|||
mounted() { |
|||
const system = uni.getSystemInfoSync(); |
|||
this.height = system.windowHeight + 'px'; |
|||
}, |
|||
|
|||
onUnload() { |
|||
this.clearTasksData(); |
|||
this.setRoleId(''); |
|||
}, |
|||
|
|||
methods: { |
|||
...mapActions('user', ['getToken']), |
|||
...mapActions('task', ['getRegulars', 'getPermanent', 'getGlobal']), |
|||
...mapMutations('user', ['setToken']), |
|||
...mapMutations('project', ['setProject', 'setProjectName']), |
|||
...mapMutations('role', ['setInvisibleRoles', 'setVisibleRoles', 'setRoleId']), |
|||
...mapMutations('task', [ |
|||
'setPermanents', |
|||
'setUpTasks', |
|||
'setDownTasks', |
|||
'setDailyTasks', |
|||
'setTimeNode', |
|||
'clearTasks', |
|||
'clearEndFlag', |
|||
'setShowSkeleton', |
|||
'setTopEnd', |
|||
'setBottomEnd', |
|||
'setShowScrollTo', |
|||
]), |
|||
|
|||
// 初始化 定期任务 |
|||
async initPlanTasks() { |
|||
this.setPrevPlaceholderTasks(); // 向上加载空数据 |
|||
this.setNextPlaceholderTasks(); // 向下加载空数据 |
|||
// // this.$nextTick(() => this.$refs.timeLine.setScrollPosition()); // 滚动到对应位置 |
|||
await this.getInitTasks(); // 获取初始数据 |
|||
// 滚动到对应位置 |
|||
let timer = null; |
|||
timer = setInterval(() => { |
|||
if (this.showScrollTo) { |
|||
clearInterval(timer); |
|||
this.$nextTick(() => this.$refs.timeLine.setScrollPosition()); |
|||
} |
|||
}, 500); |
|||
}, |
|||
|
|||
// 切换了 颗粒度 || 角色时候 获取初始定期任务 |
|||
getInitTasks() { |
|||
// 预加载 上下的定期任务 |
|||
function preloadFn(that) { |
|||
const detailId = that.tasks.findIndex(task => task.detailId); |
|||
const arr = []; |
|||
that.tasks.forEach(task => { |
|||
if (task.detailId) { |
|||
arr.push(task); |
|||
} |
|||
}); |
|||
if (detailId !== -1) { |
|||
// 只要有1个真实的任务 就预加载上下周期的任务 |
|||
const { pageCount } = that.$t.task; |
|||
that.$nextTick(() => { |
|||
// 向上拿数据 |
|||
const { tasks, timeGranularity } = that; |
|||
that.getTasks({ timeNode: +tasks[detailId].planStart, queryType: 0, queryNum: pageCount }); |
|||
// 向下拿数据 |
|||
const nextQueryTime = +that.$t.time.add(+arr[arr.length - 1].planStart, 1, timeGranularity); |
|||
that.getTasks({ timeNode: nextQueryTime, queryType: 1, queryNum: pageCount }); |
|||
}); |
|||
} else { |
|||
// 没有任务 上下显示时间刻度 |
|||
// 向上加载 |
|||
// that.setPrevPlaceholderTasks(); |
|||
// // 向下加载 |
|||
// that.setNextPlaceholderTasks(); |
|||
} |
|||
} |
|||
console.log('preloadFn: '); |
|||
// 根据项目id获取角色列表 |
|||
this.getTasks({ queryType: 1 }, preloadFn); // 向下获取定期任务数据 |
|||
// 根据时间基准点和角色查找定期任务 |
|||
this.getTasks({ queryType: 0 }); // 向上获取定期任务数据 |
|||
}, |
|||
|
|||
/** |
|||
* 根据时间基准点和角色查找定期任务 |
|||
* @param {object} query |
|||
* @param {string} query.roleId 角色id |
|||
* @param {string} query.timeNode 时间基准点 默认当前 |
|||
* @param {string} query.timeUnit 时间颗粒度 默认天 |
|||
* @param {string} query.queryNum 查找颗粒度数量 默认3个 |
|||
* @param {number} query.queryType 0向上查找 1向下查找(默认) 下查包含自己,上查不包含 |
|||
*/ |
|||
getTasks(query, fn) { |
|||
this.setShowSkeleton(true); |
|||
const params = this.generateGetTaskParam(query); |
|||
this.$t.$q.getRegularTask(params, (err, data) => { |
|||
this.setShowSkeleton(false); |
|||
if (err) { |
|||
// TODO: 提示错误 |
|||
console.error('err: ', err); |
|||
} else { |
|||
this.setShowScrollTo(true); |
|||
// 有数据用数据替换刻度 |
|||
// 没有数据 继续加载刻度 |
|||
if (data && data.length) { |
|||
this.replacePrevData(data, params.queryType); |
|||
params.queryType === 0 ? this.setTopEnd(false) : this.setBottomEnd(false); |
|||
} else { |
|||
// TODO: 0 -> 向上 1 -> 向下 |
|||
params.queryType === 0 ? this.setPrevPlaceholderTasks() : this.setNextPlaceholderTasks(); |
|||
} |
|||
if (this.tasks.length && fn) { |
|||
fn(this); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 生成getTasks所用的参数 |
|||
* @param {object} query getTasks传递的参数 |
|||
*/ |
|||
generateGetTaskParam(query) { |
|||
const { roleId, timeNode, timeUnit, projectId } = this; |
|||
return { |
|||
roleId, |
|||
timeNode: query.timeNode || timeNode, |
|||
timeUnit: query.timeUnit || timeUnit, |
|||
queryNum: query.queryNum || 3, |
|||
queryType: query.queryType, |
|||
projectId, |
|||
}; |
|||
}, |
|||
|
|||
// 设置时间轴向上的空数据 |
|||
setPrevPlaceholderTasks() { |
|||
this.setTopEnd(true); |
|||
let startTime = ''; |
|||
const { tasks } = this; |
|||
if (!tasks || !tasks.length) { |
|||
startTime = Date.now(); // 没有任务就应该是时间基准点 |
|||
} else { |
|||
startTime = tasks[0].planStart - 0; // 有任务就是第一个任务的计划开始时间 |
|||
} |
|||
const placeholderTasks = setPlaceholderTasks(startTime, true, this.timeGranularity); |
|||
this.setUpTasks(placeholderTasks); |
|||
}, |
|||
|
|||
// 设置时间轴向下的空数据 |
|||
setNextPlaceholderTasks() { |
|||
this.setBottomEnd(true); |
|||
let startTime = ''; |
|||
if (!this.tasks || !this.tasks.length) { |
|||
startTime = Date.now(); |
|||
} else { |
|||
startTime = +this.tasks[this.tasks.length - 1].planStart; |
|||
} |
|||
const initData = setPlaceholderTasks(startTime, false, this.timeGranularity); |
|||
this.setDownTasks(initData); |
|||
}, |
|||
|
|||
/** |
|||
* 用拿到的新数据 替换 时间刻度/旧数据 |
|||
* 先对比 新旧数据的 始末时间 补齐刻度 |
|||
* 再遍历对比 用任务替换刻度 |
|||
* @param {array} data 服务端返回的新数据 上边已经处理过空值 |
|||
* @param {number} type 0 -> 向上 1->向下 |
|||
*/ |
|||
replacePrevData(data, type) { |
|||
const { timeGranularity } = this; |
|||
let oldTasks = this.fillPlaceholderTask({ tasks: this.tasks, data, timeGranularity }); // 已经上下补齐时间刻度的 |
|||
// 遍历对比 用任务替换刻度 |
|||
// TODO: tasks越来越多 遍历越来越多 需要优化 |
|||
oldTasks.forEach((taskItem, index) => { |
|||
const arr = data.filter(dataItem => this.$moment(+dataItem.planStart).isSame(+taskItem.planStart, timeGranularity)); |
|||
if (arr && arr.length) { |
|||
oldTasks.splice(index, 1, [...arr]); // 这里加入的数据是array类型的, [{},{},[],[],{}] |
|||
} |
|||
}); |
|||
|
|||
oldTasks = flatten(oldTasks); // 1维拍平 |
|||
this.clearTasks(); // setUpTasks setUpTasks 的限制 需要清空 |
|||
type === 0 ? this.setUpTasks(oldTasks) : this.setDownTasks(oldTasks); |
|||
}, |
|||
|
|||
/** |
|||
* 超出旧数据上、下限 补齐时间刻度到新数据的起始时间颗粒度 |
|||
*/ |
|||
fillPlaceholderTask({ tasks, data, timeGranularity }) { |
|||
const { prev, next } = computeFillPlaceholderTaskCount({ tasks, data, timeGranularity }); |
|||
if (prev) { |
|||
const newTasks = setPlaceholderTasks(+tasks[0].planStart, true, timeGranularity, prev); |
|||
this.setUpTasks(newTasks); |
|||
} |
|||
if (next) { |
|||
const newTasks = setPlaceholderTasks(+tasks[tasks.length - 1].planStart, false, timeGranularity, next); |
|||
this.setDownTasks(newTasks); |
|||
} |
|||
return this.tasks; |
|||
}, |
|||
|
|||
/** |
|||
* 初始化 |
|||
* @param {object | null} options |
|||
*/ |
|||
init(options) { |
|||
if (!this.token) { |
|||
// 不论有没有token都直接从userId获取token |
|||
// token有过期时间 从本地获取可能是过期 干脆直接从userId获取 |
|||
if (!options || !options.u) { |
|||
this.$t.ui.showToast('缺少用户信息参数'); // 参数里没有u (userId)提示 |
|||
} else { |
|||
this.getToken(options.u); |
|||
} |
|||
} |
|||
|
|||
// 参数里有项目名称 就设置标题里的项目名称 |
|||
options && options.pname && this.setProjectName(options.pname); |
|||
|
|||
if (!options || !options.p) { |
|||
this.$t.ui.showToast('缺少项目信息参数'); // 没有项目id参数 |
|||
} else { |
|||
if (options.p !== this.$t.storage.getStorageSync('projectId')) { |
|||
this.$t.storage.setStorageSync('roleId', ''); |
|||
} |
|||
// TODO |
|||
this.getProjectById({ projectId: options.p, num: 0 }); // 根据项目id获取项目信息 |
|||
} |
|||
}, |
|||
|
|||
// 分享链接来的初始化 |
|||
async shareInit(options) { |
|||
const storageUser = this.$t.storage.getStorageSync('user'); |
|||
const user = storageUser ? JSON.parse(storageUser) : null; |
|||
if (user && user.id) { |
|||
await this.getToken(user.id); |
|||
const res = await this.clickShare({ code: options.shareId }); |
|||
if (res && res.projectId) { |
|||
let query = { ...this.$route.query }; |
|||
query = { |
|||
u: user.id, |
|||
p: res.projectId, |
|||
}; |
|||
this.$router.push({ path: this.$route.path, query }); |
|||
this.init(query); |
|||
} |
|||
} else { |
|||
this.$t.ui.showToast('缺少用户信息参数,请登录'); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 点击分享连接 |
|||
* @param {any} commit |
|||
* @param {object} param 请求参数 |
|||
*/ |
|||
async clickShare(param) { |
|||
try { |
|||
const data = await this.$u.api.clickShare(param); |
|||
return data; |
|||
} catch (error) { |
|||
this.$t.ui.showToast(error.msg || '获取失败'); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 通过项目id获取项目信息 |
|||
* @param {object} params 提交的参数 |
|||
*/ |
|||
async getProjectById(params) { |
|||
try { |
|||
const data = await uni.$u.api.findProjectById(params); |
|||
this.setProject(data); |
|||
// 根据项目id获取角色列表 |
|||
this.getRoles(params); |
|||
} catch (error) { |
|||
console.log('error: ', error || '获取项目信息失败'); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 通过项目id获取角色信息 |
|||
* @param {string} projectId |
|||
* @param {object} params 提交的参数 |
|||
*/ |
|||
getRoles(params) { |
|||
this.$t.$q.findShowRole(params, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err || '获取角色信息失败'); |
|||
} else { |
|||
this.setInvisibleRoles(data ? data.invisibleList : []); |
|||
this.setVisibleRoles(data ? data.visibleList : []); |
|||
this.setInitialRoleId(data ? data.visibleList : []); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 设置 初始显示角色信息 |
|||
setInitialRoleId(visibleList) { |
|||
if (!visibleList || !visibleList.length) return; |
|||
const index = visibleList.findIndex(item => +item.mine === 1); |
|||
const currentRole = index > 0 ? visibleList[index] : visibleList[0]; |
|||
const storageRoleId = this.$t.storage.getStorageSync('roleId'); |
|||
const currentRoleId = storageRoleId ? storageRoleId : currentRole ? currentRole.id : ''; |
|||
this.setRoleId(currentRoleId); |
|||
// 清空storage |
|||
this.$t.storage.setStorageSync('roleId', ''); |
|||
}, |
|||
|
|||
// 获取可变全局任务 |
|||
getGlobalData() { |
|||
const { roleId, timeNode, timeUnit, projectId } = this; |
|||
const param = { roleId, timeNode, timeUnit, projectId }; |
|||
this.getGlobal(param); |
|||
}, |
|||
|
|||
// 清除已有的任务数据 |
|||
clearTasksData() { |
|||
// 清空日常任务的数据 |
|||
this.setPermanents([]); |
|||
this.setDailyTasks([]); |
|||
// 清空定期任务数据 |
|||
this.clearTasks(); |
|||
// 到顶的标志复位 |
|||
// 到底的标志复位 |
|||
this.clearEndFlag(); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.border-b { |
|||
border-bottom: 1px solid #e4e7ed; |
|||
} |
|||
</style> |
@ -0,0 +1,64 @@ |
|||
<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> |
|||
export default { |
|||
name: 'p-deliver-check', |
|||
data() { |
|||
return { |
|||
remark: '', |
|||
type: 'textarea', |
|||
border: true, |
|||
height: 100, |
|||
autoHeight: true, |
|||
wordNum: 0, |
|||
score: 0, |
|||
type1: 'number', |
|||
}; |
|||
}, |
|||
|
|||
watch: { |
|||
remark(val) { |
|||
this.wordNum = val.length; |
|||
}, |
|||
|
|||
score(val) { |
|||
this.score1 = val; |
|||
}, |
|||
}, |
|||
|
|||
methods: { |
|||
// 提交交付物 |
|||
submit() { |
|||
this.$emit('submit', this.remark, this.score); |
|||
}, |
|||
|
|||
sliderChange(e) { |
|||
this.score = e.detail.value; |
|||
}, |
|||
|
|||
changeNumber(e) { |
|||
if (e > 100) { |
|||
this.score = 100; |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style></style> |
@ -0,0 +1,140 @@ |
|||
<template> |
|||
<!-- 交付物 --> |
|||
<view class="mt-3"> |
|||
<view v-if="lists && lists.length"> |
|||
<view :key="list.id" v-for="list in 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> |
|||
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) { |
|||
// 通过组件定义的ref调用uni-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></style> |
@ -0,0 +1,7 @@ |
|||
<template> |
|||
<view>成员管理</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default {}; |
|||
</script> |
@ -0,0 +1,7 @@ |
|||
<template> |
|||
<view>项目管理</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default {}; |
|||
</script> |
@ -0,0 +1,7 @@ |
|||
<template> |
|||
<view>角色管理</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default {}; |
|||
</script> |
@ -0,0 +1,7 @@ |
|||
<template> |
|||
<view>任务管理</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default {}; |
|||
</script> |
@ -0,0 +1,60 @@ |
|||
<template> |
|||
<!-- 子项目插件 --> |
|||
<view> |
|||
<view v-for="item in sonProject" :key="item.detailId"> |
|||
<span class="text-xs text-blue-500" @click="openProject(item)">{{ item.name }}</span> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'p-subproject', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { sonProject: [] }; |
|||
}, |
|||
|
|||
computed: mapGetters('project', ['projectId']), |
|||
|
|||
mounted() { |
|||
this.getSonProject(); |
|||
}, |
|||
|
|||
methods: { |
|||
async getSonProject() { |
|||
try { |
|||
const data = await this.$u.api.findSonProject({ projectId: this.task.detailId }); |
|||
this.sonProject = data; |
|||
} catch (error) { |
|||
console.error('p-subproject.vue getSonProject error: ', error); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 打开项目 |
|||
* @param {object} project 所点击的项目的信息 |
|||
*/ |
|||
openProject(project) { |
|||
const { name, id, url } = project; |
|||
url && (uni.$t.domain = url); |
|||
this.$u.route('pages/project/project', { |
|||
u: this.userId, |
|||
p: id, |
|||
pname: name, |
|||
url: encodeURIComponent(url), |
|||
}); |
|||
}, |
|||
}, |
|||
watch: {}, |
|||
}; |
|||
</script> |
|||
|
|||
<style></style> |
@ -0,0 +1,39 @@ |
|||
<template> |
|||
<view> |
|||
<view v-for="item in sonTask" :key="item.detailId"> |
|||
<span class="text-xs text-gray-500">{{ item.name }}</span> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-subtasks', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { sonTask: [] }; |
|||
}, |
|||
|
|||
created() { |
|||
this.getSonTask(); |
|||
}, |
|||
|
|||
methods: { |
|||
async getSonTask() { |
|||
try { |
|||
const data = await this.$u.api.findSonTask({ detailId: this.task.detailId }); |
|||
this.sonTask = data; |
|||
} catch (error) { |
|||
console.error('p-subtasks.vue getSonTask error: ', error); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style></style> |
@ -0,0 +1,20 @@ |
|||
<template> |
|||
<!-- 任务倒计时插件 --> |
|||
<view>任务倒计时插件</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-task-countdown', |
|||
props: { item: { type: Object, default: null } }, |
|||
data() { |
|||
return {}; |
|||
}, |
|||
|
|||
computed: {}, |
|||
methods: {}, |
|||
watch: {}, |
|||
}; |
|||
</script> |
|||
|
|||
<style></style> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<!-- 任务描述 --> |
|||
<view>{{ task.description }}</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-task-description', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<view v-if="realDuration && planDuration"> |
|||
<!-- 任务时长延迟插件 --> |
|||
<!-- 超时 --> |
|||
<span class="font-bold text-green-500" v-if="realDuration - 0 > planDuration - 0"> |
|||
+{{ $t.time.formatDuration(realDuration - planDuration) }} |
|||
</span> |
|||
<!-- 延时 --> |
|||
<span class="font-bold text-red-500" v-if="realDuration - 0 < planDuration - 0"> |
|||
-{{ $t.time.formatDuration(planDuration - realDuration) }} |
|||
</span> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-task-duration-delay', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
|
|||
computed: { |
|||
realDuration() { |
|||
return this.task.realDuration; |
|||
}, |
|||
planDuration() { |
|||
return this.task.planDuration; |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,23 @@ |
|||
<template> |
|||
<!-- <view>任务开始时间延迟插件</view> --> |
|||
<view v-if="realStart && planStart"> |
|||
<!-- 任务开始时间延迟插件 --> |
|||
<!-- 超时 --> |
|||
<span>{{ $t.time.formatDuration(+realStart - +planStart) }}</span> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-task-start-time-delay', |
|||
props: { task: { type: Object, default: () => {} } }, |
|||
computed: { |
|||
realStart() { |
|||
return this.task.realStart; |
|||
}, |
|||
planStart() { |
|||
return this.task.planStart; |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<!-- 任务名插件 --> |
|||
<view>{{ task.name }}</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'p-task-title', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,94 @@ |
|||
<template> |
|||
<!-- 上传交付物 --> |
|||
<view class="py-2"> |
|||
<u-input :auto-height="autoHeight" :border="border" :height="height" :type="type" v-model="content" width="100" /> |
|||
|
|||
<!-- 选择检查人 --> |
|||
<ChooseChecker ref="checker" :checkerList="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="showHistory"></u-icon> |
|||
<u-icon @click="changeShowHistory" name="arrow-down" v-else></u-icon> |
|||
</view> |
|||
|
|||
<p-delivery-history :task="task" v-if="showHistory" /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import ChooseChecker from '@/components/ChooseChecker/ChooseChecker.vue'; |
|||
import { mapState, mapGetters } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'p-upload-deliverable', |
|||
components: { ChooseChecker }, |
|||
props: { task: { type: Object, default: null } }, |
|||
data() { |
|||
return { |
|||
content: '', |
|||
type: 'textarea', |
|||
border: true, |
|||
height: 30, |
|||
autoHeight: true, |
|||
checkerList: [], |
|||
showHistory: false, // 展开历史记录 |
|||
}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapState('role', ['members']), |
|||
...mapGetters('project', ['projectId']), |
|||
|
|||
checkers() { |
|||
const arr = []; |
|||
if (this.members.length) { |
|||
this.members.forEach(member => { |
|||
const item = { value: member.memberId, label: member.name }; |
|||
arr.push(item); |
|||
}); |
|||
} |
|||
return arr; |
|||
}, |
|||
}, |
|||
|
|||
methods: { |
|||
// 设置检查人 |
|||
setCheckerList(checked, item) { |
|||
if (checked) { |
|||
this.checkerList.push(item.memberId); |
|||
} else { |
|||
const index = this.checkerList.findIndex(checker => checker === item.memberId); |
|||
this.checkerList.splice(index, 1); |
|||
} |
|||
}, |
|||
|
|||
// 展开合上历史记录 |
|||
changeShowHistory() { |
|||
this.showHistory = !this.showHistory; |
|||
}, |
|||
|
|||
// 提交交付物 |
|||
async submit() { |
|||
try { |
|||
const { content, checkerList, projectId, task } = this; |
|||
if (!this.checkerList.length) { |
|||
this.$t.ui.showToast('请选择检查人'); |
|||
return; |
|||
} |
|||
const params = { content, checkerList, projectId, taskSubId: task.id }; |
|||
await this.$u.api.saveDeliver(params); |
|||
this.$t.ui.showToast('交付物提交成功'); |
|||
this.content = ''; |
|||
this.checkerList = []; |
|||
this.$refs.checker.clearChecked(); |
|||
} catch (error) { |
|||
console.error('p-upload-deliverable.vue submit error: ', error); |
|||
this.$t.ui.showToast('交付物提交失败,请稍后重试'); |
|||
} |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped lang="scss"></style> |
@ -0,0 +1,85 @@ |
|||
<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> |
|||
import { mapGetters, mapMutations } from 'vuex'; |
|||
|
|||
export default { |
|||
name: 'p-wbs-import', |
|||
props: { |
|||
task: { |
|||
type: Object, |
|||
default: () => {}, |
|||
}, |
|||
}, |
|||
data() { |
|||
return {}; |
|||
}, |
|||
|
|||
computed: { |
|||
...mapGetters('user', ['userId']), |
|||
...mapGetters('project', ['projectId']), |
|||
}, |
|||
|
|||
methods: { |
|||
...mapMutations('project', ['setShowAlert']), |
|||
|
|||
// 导入wbs |
|||
async handleUpload() { |
|||
try { |
|||
const data = await this.$u.api.import(); |
|||
// 导入WBS成功后 |
|||
// 直接打开导入的项目 |
|||
this.onUploadSuccess(); |
|||
setTimeout(() => { |
|||
this.$u.route('/pages/project/project', { |
|||
u: this.userId, |
|||
p: data.id, |
|||
pname: data.pname, |
|||
url: data.url, |
|||
}); |
|||
}, 2000); |
|||
} catch (error) { |
|||
this.onUploadError(error); |
|||
} |
|||
}, |
|||
|
|||
// 更新项目 |
|||
// TODO: 更新接口没写完 |
|||
async handleUpdate() { |
|||
try { |
|||
await this.$u.api.import({ projectId: this.projectId }); |
|||
// 导入WBS成功后 |
|||
// 直接打开导入的项目 |
|||
this.onUploadSuccess(); |
|||
} catch (error) { |
|||
this.onUploadError(error); |
|||
} |
|||
}, |
|||
|
|||
// 导入成功 |
|||
onUploadSuccess() { |
|||
this.$refs.uTips.show({ |
|||
title: '导入成功,即将打开新项目', |
|||
type: 'success', |
|||
duration: '3000', |
|||
}); |
|||
}, |
|||
|
|||
// 导入失败 |
|||
onUploadError(error) { |
|||
this.$refs.uTips.show({ |
|||
title: error || '导入失败', |
|||
type: 'error', |
|||
duration: '6000', |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -0,0 +1,3 @@ |
|||
const actions = {}; |
|||
|
|||
export default actions; |
@ -0,0 +1,3 @@ |
|||
const getters = {}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,3 @@ |
|||
const mutations = {}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,7 @@ |
|||
const state = { |
|||
db: null, // indexedDB对象
|
|||
name: 'TALL_indexedDB', |
|||
version: 1, |
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,17 @@ |
|||
const actions = { |
|||
/** |
|||
* 根据项目id查找所有成员信息 |
|||
* @param {*} commit |
|||
* @param {object} params |
|||
*/ |
|||
async getAllMembers({ commit }, params) { |
|||
try { |
|||
const data = await uni.$u.api.queryChecker(params); |
|||
commit('setMembers', data); |
|||
} catch (error) { |
|||
uni.$t.ui.showToast(error.msg || '成员查询失败'); |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,13 @@ |
|||
const getters = { |
|||
// 是不是负责人
|
|||
isMine({ roleId, invisibleRoles, visibleRoles }) { |
|||
if (!visibleRoles || !visibleRoles.length) return false; |
|||
const visible = visibleRoles.find(visible => visible.id === roleId); |
|||
if (visible) return visible.mine; |
|||
const invisible = invisibleRoles.find(invisible => invisible.id === roleId); |
|||
if (invisible) return visible.mine; |
|||
return false; |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,39 @@ |
|||
const mutations = { |
|||
/** |
|||
* 设置不展示的角色信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setInvisibleRoles(state, data) { |
|||
state.invisibleRoles = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置展示的角色信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setVisibleRoles(state, data) { |
|||
state.visibleRoles = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置当前角色信息 |
|||
* @param {Object} state |
|||
* @param {string} roleId 当前正在展示的角色的id |
|||
*/ |
|||
setRoleId(state, roleId) { |
|||
state.roleId = roleId; |
|||
}, |
|||
|
|||
/** |
|||
* 设置项目下所有成员信息 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setMembers(state, data) { |
|||
state.members = data || []; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,8 @@ |
|||
const state = { |
|||
invisibleRoles: [], // 不展示的角色信息
|
|||
visibleRoles: [], // 展示的角色信息
|
|||
roleId: '', // 当前展示查看的角色id
|
|||
members: [], // 项目下所有成员
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,33 @@ |
|||
const actions = { |
|||
/** |
|||
* 根据角色查找永久的日常任务 |
|||
* @param {*} commit |
|||
* @param {string} roleId 角色id |
|||
*/ |
|||
getPermanent({ commit }, param) { |
|||
uni.$t.$q.getPermanent(param, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err); |
|||
} else { |
|||
commit('setPermanents', data); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 根据时间和角色查找日常任务 |
|||
* @param {*} commit |
|||
* @param {object} param 请求参数 roleId, timeNode, timeUnit |
|||
*/ |
|||
getGlobal({ commit }, param) { |
|||
uni.$t.$q.getGlobal(param, (err, data) => { |
|||
if (err) { |
|||
console.error('err: ', err); |
|||
} else { |
|||
commit('setDailyTasks', data); |
|||
} |
|||
}); |
|||
}, |
|||
}; |
|||
|
|||
export default actions; |
@ -0,0 +1,23 @@ |
|||
const getters = { |
|||
// 所有的日常任务 永久 + 可变 日常任务
|
|||
globals({ dailyTasks, permanents }) { |
|||
return [...permanents, ...dailyTasks]; |
|||
}, |
|||
|
|||
unitConfig({ timeUnit }) { |
|||
const target = uni.$t.timeConfig.timeUnits.find(item => item.id === timeUnit); |
|||
return target; |
|||
}, |
|||
|
|||
// 计算任务开始时间的格式
|
|||
startTimeFormat(state, { unitConfig }) { |
|||
return unitConfig.format || 'D日 HH:mm'; |
|||
}, |
|||
|
|||
// 计算颗粒度 对应的 dayjs add 的单位
|
|||
timeGranularity(state, { unitConfig }) { |
|||
return unitConfig.granularity; |
|||
}, |
|||
}; |
|||
|
|||
export default getters; |
@ -0,0 +1,12 @@ |
|||
import state from './state'; |
|||
import getters from './getters'; |
|||
import mutations from './mutations'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
namespaced: true, |
|||
state, |
|||
getters, |
|||
mutations, |
|||
actions, |
|||
}; |
@ -0,0 +1,208 @@ |
|||
const mutations = { |
|||
/** |
|||
* 记录时间轴向上滚动的距离 |
|||
* @param { object } state |
|||
* @param { number } num |
|||
*/ |
|||
setScrollTop(state, num) { |
|||
state.scrollTop = num; |
|||
}, |
|||
|
|||
/** |
|||
* 记录时间轴向上滚动的距离 |
|||
* @param { object } state |
|||
* @param {string} taskId |
|||
*/ |
|||
setScrollToTaskId(state, taskId) { |
|||
state.scrollToTaskId = taskId; |
|||
}, |
|||
|
|||
/** |
|||
* 设置日常任务当前是否应该处于收缩状态 |
|||
* @param { object } state |
|||
* @param { boolean } data |
|||
*/ |
|||
setShrink(state, data) { |
|||
state.isShrink = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置tip的值 |
|||
* @param {object} state |
|||
* @param {object} data |
|||
*/ |
|||
setTip(state, data) { |
|||
if (!data) return; |
|||
state.tip = { ...data }; |
|||
}, |
|||
|
|||
/** |
|||
* 是否显示tips |
|||
* @param { object } state |
|||
* @param { boolean } show |
|||
*/ |
|||
setTipShow(state, show) { |
|||
state.tip.show = show; |
|||
}, |
|||
|
|||
/** |
|||
* 是否显示tips |
|||
* @param { object } state |
|||
* @param { number } status |
|||
*/ |
|||
setStatus(state, status) { |
|||
state.tip.status = status; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间基准点 |
|||
* @param { object } state |
|||
* @param { number } data |
|||
*/ |
|||
setTimeNode(state, data) { |
|||
state.timeNode = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间颗粒度 |
|||
* @param { object } state |
|||
* @param { number } data |
|||
*/ |
|||
setTimeUnit(state, data) { |
|||
state.timeUnit = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置向上查到的定期任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setUpTasks(state, data) { |
|||
if (!state.tasks.length) { |
|||
state.tasks = [...data]; // 原来没有数据
|
|||
} else { |
|||
state.tasks = [...data, ...state.tasks]; |
|||
// state.tasks = [...data.concat(state.tasks)];
|
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 设置向下查到的定期任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setDownTasks(state, data) { |
|||
console.log('setDownTasks: '); |
|||
if (!state.tasks && !state.tasks.length) { |
|||
state.tasks = [...data]; |
|||
} else { |
|||
state.tasks = [...state.tasks, ...data]; |
|||
// state.tasks = [...state.tasks.concat(data)];
|
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 添加任务后更新tasks |
|||
* @param {Object} state |
|||
* @param {Array} data 新添加的task |
|||
*/ |
|||
updateTasks(state, data) { |
|||
console.log('updateTasks: '); |
|||
state.tasks = [...data]; |
|||
}, |
|||
|
|||
/** |
|||
* 设置添加任务的位置 |
|||
* @param {*} state |
|||
* @param {*} data |
|||
*/ |
|||
setAddPosition(state, data) { |
|||
console.log('data: ', data); |
|||
}, |
|||
|
|||
/** |
|||
* 设置日常任务数据 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setDailyTasks(state, data) { |
|||
state.dailyTasks = data || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置永久固定任务 |
|||
* @param {object} state |
|||
* @param {array} tasks 服务端查询到的永久日常任务书籍 |
|||
*/ |
|||
setPermanents(state, tasks) { |
|||
state.permanents = tasks || []; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间轴是否继续向上查任务 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setTopEnd(state, show) { |
|||
state.topEnd = show; |
|||
}, |
|||
|
|||
/** |
|||
* 设置时间轴是否继续向下查任务 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setBottomEnd(state, show) { |
|||
state.bottomEnd = show; |
|||
}, |
|||
|
|||
// 清空标志位 如切换角色等使用
|
|||
clearEndFlag(state) { |
|||
state.topEnd = false; |
|||
state.bottomEnd = false; |
|||
}, |
|||
|
|||
// 清空定期任务
|
|||
clearTasks(state) { |
|||
state.tasks = []; |
|||
}, |
|||
|
|||
/** |
|||
* 收到消息设置任务状态 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setTaskStatus(state, data) { |
|||
const item = state.tasks.find(i => i.id === data.id); |
|||
item.process = data.taskStatus; |
|||
}, |
|||
|
|||
/** |
|||
* 收到打开新项目消息状态 |
|||
* @param {Object} state |
|||
* @param {Array} data 服务端返回的模板数组 |
|||
*/ |
|||
setNewProjectInfo(state, data) { |
|||
state.newProjectInfo = data; |
|||
}, |
|||
|
|||
/** |
|||
* 设置骨架屏是否显示 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setShowSkeleton(state, show) { |
|||
state.showSkeleton = show; |
|||
}, |
|||
|
|||
/** |
|||
* 是否设置时间轴自动滚动的位置 |
|||
* @param {Object} state |
|||
* @param {Boolean} show |
|||
*/ |
|||
setShowScrollTo(state, show) { |
|||
state.showScrollTo = show; |
|||
}, |
|||
}; |
|||
|
|||
export default mutations; |
@ -0,0 +1,25 @@ |
|||
const state = { |
|||
scrollTop: 0, |
|||
scrollToTaskId: '', // 时间轴自动滚动的位置
|
|||
isShrink: false, // true: 收起, false:展开
|
|||
tip: { |
|||
taskId: '', // 当前正在修改状态的任务的id
|
|||
show: false, |
|||
status: 0, // 所点击任务的当前状态码
|
|||
text: '', |
|||
left: 0, // 鼠标点击位置距离左边的距离
|
|||
top: 0, // 鼠标点击位置距离上边的距离
|
|||
}, |
|||
timeNode: new Date().getTime(), // 时间基准点
|
|||
timeUnit: 4, // 时间颗粒度
|
|||
topEnd: false, // 时间轴向上查任务到顶了
|
|||
bottomEnd: false, // 时间轴向下查任务到底了
|
|||
permanents: [], // 永久日常任务
|
|||
dailyTasks: [], // 日常任务
|
|||
tasks: [], // 所有的定期任务
|
|||
showSkeleton: false, // 定期任务骨架屏
|
|||
newProjectInfo: {}, |
|||
showScrollTo: false, // 是否可以设置时间轴自动滚动的位置
|
|||
}; |
|||
|
|||
export default state; |
@ -0,0 +1,24 @@ |
|||
import { computeFillPlaceholderTaskCount } from '../../utils/task'; |
|||
|
|||
describe('computeFillPlaceholderTaskCount', () => { |
|||
// 2021/8/17 ~ 2021/8/21
|
|||
const tasks = [ |
|||
{ id: '99724910037144221455', panel: {}, plugins: [], process: 4, planStart: 1629169800242 }, |
|||
{ id: '65053357415671253512', panel: {}, plugins: [], process: 4, planStart: 1629256200242 }, |
|||
{ id: '38735454515347179194', panel: {}, plugins: [], process: 4, planStart: 1629342600242 }, |
|||
{ id: '49602681534756706607', panel: {}, plugins: [], process: 4, planStart: 1629429000242 }, |
|||
{ id: '98860265376222512018', panel: {}, plugins: [], process: 4, planStart: 1629515400242 }, |
|||
{ id: '44419041575700334936', panel: {}, plugins: [], process: 4, planStart: 1629601800242 }, |
|||
]; |
|||
const timeGranularity = 'day'; |
|||
|
|||
it('超出上限 补齐', () => { |
|||
const data = [{ planStart: `${new Date('2021/8/10').getTime()}` }, { planStart: `${new Date('2021/8/11').getTime()}` }]; |
|||
expect(computeFillPlaceholderTaskCount({ tasks, data, timeGranularity }).prev.toString()).toMatch(/(7|8)/); |
|||
}); |
|||
|
|||
it('超出下限 补齐', () => { |
|||
const data = [{ planStart: `${new Date('2021/8/10').getTime()}` }, { planStart: `${new Date('2021/8/22').getTime()}` }]; |
|||
expect(computeFillPlaceholderTaskCount({ tasks, data, timeGranularity }).next.toString()).toMatch(/(1|2)/); |
|||
}); |
|||
}); |
@ -0,0 +1,46 @@ |
|||
import Time from '../../utils/time.js'; |
|||
|
|||
// 测试计算进行中剩余时长显示数值
|
|||
describe('utils/time.js computeDurationText function', () => { |
|||
const { computeDurationText } = Time; |
|||
// const leftTime = +realStart + +planDuration - Date.now(); // 剩余时间
|
|||
it ('leftTime is 60ms, num=60, time=16', () => { |
|||
expect(computeDurationText(60)).toEqual({ num: 60, time: 16 }) |
|||
}) |
|||
|
|||
it ('leftTime is 300ms, num=300, time=16', () => { |
|||
expect(computeDurationText(300)).toEqual({ num: 300, time: 16 }) |
|||
}) |
|||
|
|||
it ('leftTime is 10s20ms, num=10, time=1000', () => { |
|||
expect(computeDurationText(10*1000 + 20)).toEqual({ num: 10, time: 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime is 8分钟10s20ms, num=8, time=1000', () => { |
|||
expect(computeDurationText(8*60*1000 + 10*1000 + 20)).toEqual({ num: 8, time: 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime is 3小时8分钟10s20ms, num=3, time=1000', () => { |
|||
expect(computeDurationText(3*60*60*1000 + 8*60*1000 + 10*1000 + 20)).toEqual({ num: 3, time: 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime is 11天3小时8分钟10s20ms, num=11, time=60 * 60 * 1000', () => { |
|||
expect(computeDurationText(11*24*60*60*1000 + 3*60*60*1000 + 8*60*1000 + 10*1000 + 20)).toEqual({ num: 11, time: 60 * 60 * 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime is 2个月11天3小时8分钟10s20ms, num=2, time=60 * 60 * 1000', () => { |
|||
expect(computeDurationText(2*30*24*60*60*1000 + 11*24*60*60*1000 + 3*60*60*1000 + 8*60*1000 + 10*1000 + 20)).toEqual({ num: 2, time: 60 * 60 * 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime is 7年2个月11天3小时8分钟10s20ms, num=7, time=60 * 60 * 1000', () => { |
|||
expect(computeDurationText(7*12*30*24*60*60*1000 + 2*30*24*60*60*1000 + 11*24*60*60*1000 + 3*60*60*1000 + 8*60*1000 + 10*1000 + 20)).toEqual({ num: 7, time: 60 * 60 * 1000 }) |
|||
}) |
|||
|
|||
it ('leftTime <=0, num=0, time=null', () => { |
|||
expect(computeDurationText(-10)).toEqual({ num: 0, time: null }) |
|||
}) |
|||
|
|||
it ('leftTime 不是数字, num=0, time=null', () => { |
|||
expect(computeDurationText('abc')).toEqual({ num: 0, time: null }) |
|||
}) |
|||
}) |
@ -0,0 +1,163 @@ |
|||
import { name } from '@/config/db'; |
|||
import { curry } from 'lodash'; |
|||
|
|||
// 创建表
|
|||
const createCollection = (Vue, db) => { |
|||
// projects项目表
|
|||
!db.objectStoreNames.contains('projects') && db.createObjectStore('projects', { keyPath: 'id' }); |
|||
// roles 角色表
|
|||
!db.objectStoreNames.contains('roles') && db.createObjectStore('roles', { keyPath: 'id' }); |
|||
// plan_tasks 定期任务
|
|||
!db.objectStoreNames.contains('plan_tasks') && db.createObjectStore('plan_tasks', { keyPath: 'id' }); |
|||
// fixed_tasks 固定全局任务
|
|||
Vue.prototype.$db.fixed_tasks = !db.objectStoreNames.contains('fixed_tasks') && db.createObjectStore('fixed_tasks', { keyPath: 'id' }); |
|||
// variable_tasks 可变全局任务
|
|||
Vue.prototype.$db.variable_tasks = |
|||
!db.objectStoreNames.contains('variable_tasks') && db.createObjectStore('variable_tasks', { keyPath: 'id' }); |
|||
// plugins 插件表
|
|||
Vue.prototype.$db.plugins = !db.objectStoreNames.contains('plugins') && db.createObjectStore('plugins', { keyPath: 'id' }); |
|||
}; |
|||
|
|||
/** |
|||
* 新增数据 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
* @param {object} data 数据 |
|||
*/ |
|||
const create = (db, collection, data) => { |
|||
return new Promise((resolve, reject) => { |
|||
const request = db.transaction([collection], 'readwrite').objectStore(collection).add(data); |
|||
request.onsuccess = () => resolve(); |
|||
|
|||
request.onerror = event => { |
|||
const { name, message } = event.target.error; |
|||
if (name === 'ConstraintError') { |
|||
reject('数据已存在'); |
|||
} else { |
|||
reject(message); |
|||
} |
|||
}; |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 找到1条数据 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
* @param {string} key 索引关键字 一般是id |
|||
*/ |
|||
const findOne = (db, collection, key) => { |
|||
return new Promise((resolve, reject) => { |
|||
const request = db.transaction([collection]).objectStore(collection).get(key); |
|||
request.onerror = event => reject(event.target.error.message); |
|||
request.onsuccess = event => resolve(event.target.result); |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 找到所有数据 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
*/ |
|||
const find = (db, collection) => { |
|||
return new Promise((resolve, reject) => { |
|||
const request = db.transaction(collection).objectStore(collection).openCursor(); |
|||
let result = []; |
|||
|
|||
request.onerror = event => reject(event.target.error.message); |
|||
request.onsuccess = event => { |
|||
const cursor = event.target.result; |
|||
if (cursor) { |
|||
result.push(cursor.value); |
|||
cursor.continue(); |
|||
} else { |
|||
resolve(result); |
|||
} |
|||
}; |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 更新数据 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
* @param {object} newData 新数据 |
|||
*/ |
|||
const update = (db, collection, newData) => { |
|||
return new Promise((resolve, reject) => { |
|||
const request = db.transaction([collection], 'readwrite').objectStore(collection).put(newData); |
|||
request.onerror = event => reject(event.target.error.message); |
|||
request.onsuccess = () => resolve(newData); |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 移除数据 通过关键字 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
* @param {string} key 关键字 |
|||
*/ |
|||
const remove = (db, collection, key) => { |
|||
return new Promise((resolve, reject) => { |
|||
const request = db.transaction([collection], 'readwrite').objectStore(collection).delete(key); |
|||
request.onerror = event => reject(event.target.error.message); |
|||
request.onsuccess = () => resolve(); |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* 创建索引 |
|||
* |
|||
* @param {object} db 数据库database |
|||
* @param {string} collection 集合/表 |
|||
* @param {string} field 创建索引的字段名称 |
|||
* @param {string} key 关键字 |
|||
*/ |
|||
const createIndexAndFind = (db, collection, field, key) => { |
|||
return new Promise((resolve, reject) => { |
|||
const store = db.transaction([collection], 'readonly').objectStore(collection); |
|||
store.createIndex(field, field); |
|||
const index = store.index(field); |
|||
const request = index.get(key); |
|||
request.onerror = event => reject(event.target.error.message); |
|||
request.onsuccess = event => resolve(event.target.result); |
|||
}); |
|||
}; |
|||
|
|||
const curriedCreate = curry(create); |
|||
export const curriedFindOne = curry(findOne); |
|||
export const curriedFind = curry(find); |
|||
export const curriedRemove = curry(remove); |
|||
export const curriedUpdate = curry(update); |
|||
export const curriedIndex = curry(createIndexAndFind); |
|||
|
|||
const install = Vue => { |
|||
uni.$db = Vue.prototype.$db = {}; |
|||
Vue.prototype.$db.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; |
|||
const request = Vue.prototype.$db.indexedDB.open(name, Date.now()); // IDBRequest 对象
|
|||
request.onerror = error => console.error('打开数据库失败', error); |
|||
request.onsuccess = event => { |
|||
console.log('INDEXED_DB OPEN SUCCESS'); |
|||
Vue.prototype.$db.db = event.target.result; |
|||
}; |
|||
request.onupgradeneeded = event => { |
|||
console.log('INDEXED_DB OPEN onupgradeneeded'); |
|||
Vue.prototype.$db.db = event.target.result; |
|||
// 创建表
|
|||
createCollection(Vue, Vue.prototype.$db.db); |
|||
|
|||
Vue.prototype.$db.create = curriedCreate(Vue.prototype.$db.db); // create 新增数据,颗粒化以后就不用再传db数据了
|
|||
Vue.prototype.$db.findOne = curriedFindOne(Vue.prototype.$db.db); // 查一条
|
|||
Vue.prototype.$db.find = curriedFind(Vue.prototype.$db.db); // 查集合里的所有数据
|
|||
Vue.prototype.$db.update = curriedUpdate(Vue.prototype.$db.db); // 更新某条数据
|
|||
Vue.prototype.$db.remove = curriedRemove(Vue.prototype.$db.db); // 删除某条数据
|
|||
// Vue.prototype.$db.createIndex = curriedIndex(Vue.prototype.$db.db); // 创建索引
|
|||
}; |
|||
}; |
|||
|
|||
export default { install }; |
@ -0,0 +1,53 @@ |
|||
import dayjs from 'dayjs'; |
|||
|
|||
/** |
|||
* 设置时间轴空数据 |
|||
* @param {number} startTime |
|||
* @param {boolean} isUp true 向上加载,false 向下加载 |
|||
* @param {string} timeGranularity 颗粒度 |
|||
* @param {number} pageCount 加载的颗粒度数量 默认值是10 |
|||
*/ |
|||
export const setPlaceholderTasks = (startTime, isUp, timeGranularity, pageCount) => { |
|||
let result = []; |
|||
pageCount = pageCount || uni.$t.task.pageCount; |
|||
for (let i = 0; i < pageCount; i++) { |
|||
const delta = isUp ? `-${i + 1}` - 0 : i + 1; |
|||
let item = { |
|||
id: uni.$u.guid(20, false, 10), |
|||
panel: {}, |
|||
plugins: [], |
|||
process: 4, |
|||
planStart: uni.$moment(startTime).add(delta, timeGranularity).valueOf(), |
|||
}; |
|||
// console.log('isup: ', isUp, 'result:', new Date(item.planStart).toLocaleDateString());
|
|||
|
|||
isUp ? result.unshift(item) : result.push(item); |
|||
} |
|||
return result; |
|||
}; |
|||
|
|||
/** |
|||
* 超出旧数据上、下限 补齐时间刻度到新数据的起始时间颗粒度 |
|||
* @param {object} option |
|||
* @param {array} option.tasks 旧的已有的任务书籍 |
|||
* @param {array} option.data 新拿到的任务数据 空值已经过滤过了 |
|||
* @param {string} option.timeGranularity 颗粒度 |
|||
*/ |
|||
export const computeFillPlaceholderTaskCount = ({ tasks, data, timeGranularity }) => { |
|||
const result = { prev: 0, next: 0 }; |
|||
// 新数据的开始时间 < 旧数据的开始时间
|
|||
// 超出了上限 补上限的时间刻度
|
|||
// 补上 新数据开始时间 到 旧数据开始时间 的刻度
|
|||
if (+data[0].planStart < +tasks[0].planStart) { |
|||
// 找出来需要补几组颗粒度
|
|||
result.prev = dayjs(+tasks[0].planStart).diff(+data[0].planStart, timeGranularity) + 1; |
|||
} |
|||
|
|||
// 新数据的结束时间 > 旧数据的结束时间
|
|||
// 超出了下线 补下限的时间刻度
|
|||
// 补上 旧数据截止时间 到 新数据截止时间 的刻度
|
|||
if (+data[data.length - 1].planStart > +tasks[tasks.length - 1].planStart) { |
|||
result.next = dayjs(+data[data.length - 1].planStart).diff(+tasks[tasks.length - 1].planStart, timeGranularity) + 1; |
|||
} |
|||
return result; |
|||
}; |
Loading…
Reference in new issue