Merge branch 'master' of github.com:kuaifan/dootask into develop

# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
#	public/js/build/161.js
#	public/js/build/400.js
#	public/js/build/616.js
#	public/js/build/845.js
This commit is contained in:
kuaifan 2022-01-24 01:45:08 +08:00
commit 8bacc3b6ba
53 changed files with 596 additions and 267 deletions

View File

@ -876,6 +876,7 @@ class ProjectController extends AbstractController
* - yes已完成
* - no未完成
* @apiParam {String} [archived] 归档状态
* - all所有
* - yes已归档
* - no未归档默认
* @apiParam {Object} sorts 排序方式
@ -911,7 +912,7 @@ class ProjectController extends AbstractController
//
$scopeAll = false;
if ($parent_id > 0) {
ProjectTask::userTask($parent_id);
ProjectTask::userTask($parent_id, str_replace(['all', 'yes', 'no'], [null, false, true], $archived));
$scopeAll = true;
$builder->where('project_tasks.parent_id', $parent_id);
} elseif ($parent_id === -1) {
@ -974,6 +975,10 @@ class ProjectController extends AbstractController
* @apiName task__one
*
* @apiParam {Number} task_id 任务ID
* @apiParam {String} [archived] 归档状态
* - all所有
* - yes已归档
* - no未归档默认
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -984,8 +989,9 @@ class ProjectController extends AbstractController
User::auth();
//
$task_id = intval(Request::input('task_id'));
$archived = Request::input('archived', 'no');
//
$task = ProjectTask::userTask($task_id, true, false, ['taskUser', 'taskTag']);
$task = ProjectTask::userTask($task_id, str_replace(['all', 'yes', 'no'], [null, false, true], $archived), false, ['taskUser', 'taskTag']);
//
$data = $task->toArray();
$data['project_name'] = $task->project?->name;
@ -1013,7 +1019,7 @@ class ProjectController extends AbstractController
//
$task_id = intval(Request::input('task_id'));
//
$task = ProjectTask::userTask($task_id);
$task = ProjectTask::userTask($task_id, null);
//
return Base::retSuccess('success', $task->content ?: json_decode('{}'));
}
@ -1038,7 +1044,7 @@ class ProjectController extends AbstractController
//
$task_id = intval(Request::input('task_id'));
//
$task = ProjectTask::userTask($task_id);
$task = ProjectTask::userTask($task_id, null);
//
return Base::retSuccess('success', $task->taskFile);
}
@ -1381,7 +1387,11 @@ class ProjectController extends AbstractController
} elseif ($type == 'add') {
$task->archivedTask(Carbon::now());
}
return Base::retSuccess('操作成功', ['id' => $task->id]);
return Base::retSuccess('操作成功', [
'id' => $task->id,
'archived_at' => $task->archived_at,
'archived_userid' => $task->archived_userid,
]);
}
/**
@ -1435,7 +1445,7 @@ class ProjectController extends AbstractController
return Base::retError('记录不存在');
}
//
$task = ProjectTask::userTask($projectLog->task_id, null, true);
$task = ProjectTask::userTask($projectLog->task_id, true, true);
//
$record = $projectLog->record;
if ($record['flow'] && is_array($record['flow'])) {

View File

@ -18,6 +18,7 @@ use Illuminate\Support\Facades\DB;
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
@ -72,6 +73,15 @@ class AbstractModel extends Model
return $this->setAppends([]);
}
/**
* 取消隐藏值
* @return static
*/
protected function scopeCancelHidden()
{
return $this->setHidden([]);
}
/**
* 为数组 / JSON 序列化准备日期。
* @param DateTimeInterface $date

View File

@ -686,11 +686,11 @@ class ProjectTask extends AbstractModel
$start_at = Carbon::parse($subTask->start_at);
$end_at = Carbon::parse($subTask->end_at);
$isUp = false;
if ($start_at->eq($oldAt[0]) || $start_at->lt(Carbon::parse($this->start_at))) {
if (empty($subTask->start_at) || $start_at->eq($oldAt[0]) || $start_at->lt(Carbon::parse($this->start_at))) {
$subTask->start_at = $this->start_at;
$isUp = true;
}
if ($end_at->eq($oldAt[1]) || $end_at->gt(Carbon::parse($this->end_at))) {
if (empty($subTask->end_at) || $end_at->eq($oldAt[1]) || $end_at->gt(Carbon::parse($this->end_at))) {
$subTask->end_at = $this->end_at;
$isUp = true;
}
@ -918,7 +918,6 @@ class ProjectTask extends AbstractModel
$this->archived_userid = User::userid();
$this->archived_follow = 0;
$this->addLog("任务取消归档");
$this->pushMsg('add', ProjectTask::oneTask($this->id));
} else {
// 归档任务
if ($isAuto === true) {
@ -932,8 +931,12 @@ class ProjectTask extends AbstractModel
$this->archived_userid = $userid;
$this->archived_follow = 0;
$this->addLog($logText, [], $userid);
$this->pushMsg('archived');
}
$this->pushMsg('update', [
'id' => $this->id,
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
]);
self::whereParentId($this->id)->update([
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
@ -1059,7 +1062,7 @@ class ProjectTask extends AbstractModel
/**
* 获取任务(会员有任务权限 会员存在项目内)
* @param int $task_id
* @param bool $archived true:仅限未归档, false:不限制, null:不限制
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param int|bool $mustOwner 0|false:不限制, 1|true:限制任务或项目负责人, 2:已有负责人才限制任务或项目负责人
* @param array $with
* @return self
@ -1072,7 +1075,7 @@ class ProjectTask extends AbstractModel
throw new ApiException('任务不存在', [ 'task_id' => $task_id ], -4002);
}
if ($archived === true && $task->archived_at != null) {
throw new ApiException('任务已归档', [ 'task_id' => $task_id ], -4002);
throw new ApiException('任务已归档', [ 'task_id' => $task_id ]);
}
if ($archived === false && $task->archived_at == null) {
throw new ApiException('任务未归档', [ 'task_id' => $task_id ]);

View File

@ -120,10 +120,10 @@ class WebSocketDialog extends AbstractModel
break;
case "group":
if ($dialog->group_type === 'project') {
$dialog->group_info = Project::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
} elseif ($dialog->group_type === 'task') {
$dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
}
break;

View File

@ -1,6 +1,6 @@
{
"name": "DooTask",
"version": "0.6.83",
"version": "0.6.90",
"description": "DooTask is task management system.",
"main": "main.js",
"license": "MIT",

View File

@ -1,6 +1,6 @@
{
"name": "DooTask",
"version": "0.6.83",
"version": "0.6.90",
"description": "DooTask is task management system.",
"scripts": {
"start": "./cmd dev",

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/113.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
/*!
* html2canvas 1.3.4 <https://html2canvas.hertzen.com>
* Copyright (c) 2021 Niklas von Hertzen <https://hertzen.com>
* html2canvas 1.4.0 <https://html2canvas.hertzen.com>
* Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
* Released under MIT License
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/build/400.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
/*!
* TOAST UI Calendar
* @version 1.15.1 | Wed Dec 22 2021
* @version 1.15.1-5 | Sun Jan 09 2022
* @author NHN FE Development Lab <dl_javascript@nhn.com>
* @license MIT
*/
@ -304,7 +304,7 @@
/*! ./weekdayInMonth */
/*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */
/*! @license DOMPurify 2.3.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.4/LICENSE */
/*! dompurify */

1
public/js/build/43.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/build/893.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css",
"/.DS_Store": "/.DS_Store",
"/favicon.ico": "/favicon.ico"
}

View File

@ -315,6 +315,46 @@
return [new Date(), lastSecond(e.getTime())];
}
}];
},
/**
* 对话标签
* @param dialog
* @returns {*[]}
*/
dialogTags(dialog) {
let tags = [];
if (dialog.type == 'group') {
if (['project', 'task'].includes(dialog.group_type) && $A.isJson(dialog.group_info)) {
if (dialog.group_type == 'task' && dialog.group_info.complete_at) {
tags.push({
color: 'success',
text: '已完成'
})
}
if (dialog.group_info.deleted_at) {
tags.push({
color: 'red',
text: '已删除'
})
} else if (dialog.group_info.archived_at) {
tags.push({
color: 'default',
text: '已归档'
})
}
}
}
return tags;
},
/**
* 对话完成
* @param dialog
* @returns {*[]}
*/
dialogCompleted(dialog) {
return this.dialogTags(dialog).find(({color}) => color == 'success');
}
});

View File

@ -345,7 +345,7 @@ export default {
msgAllUnread() {
let num = 0;
this.cacheDialogs.map(({unread}) => {
this.cacheDialogs.some(({unread}) => {
if (unread) {
num += unread;
}

View File

@ -95,6 +95,9 @@ export default {
list() {
const {cacheTasks, taskCompleteTemps} = this;
const filterTask = (task, chackCompleted = true) => {
if (task.archived_at) {
return false;
}
if (task.complete_at && chackCompleted === true) {
return false;
}

View File

@ -6,8 +6,11 @@
@dragover.prevent="chatDragOver(true, $event)"
@dragleave.prevent="chatDragOver(false, $event)">
<slot name="head">
<div class="dialog-title">
<div class="dialog-title" :class="{completed:$A.dialogCompleted(dialogData)}">
<div class="main-title">
<template v-for="tag in $A.dialogTags(dialogData)" v-if="tag.color != 'success'">
<Tag :color="tag.color" :fade="false">{{$L(tag.text)}}</Tag>
</template>
<h2>{{dialogData.name}}</h2>
<em v-if="peopleNum > 0">({{peopleNum}})</em>
</div>
@ -86,6 +89,20 @@
<div v-if="dialogDrag" class="drag-over" @click="dialogDrag=false">
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
</div>
<Modal
v-model="pasteShow"
:title="$L(pasteTitle)"
:cancel-text="$L('取消')"
:ok-text="$L('发送')"
@on-ok="pasteSend">
<div class="dialog-wrapper-paste">
<template v-for="item in pasteItem">
<img v-if="item.type == 'image'" :src="item.result"/>
<div v-else>{{$L('文件')}}: {{item.name}} ({{$A.bytesToSize(item.size)}})</div>
</template>
</div>
</Modal>
</div>
</template>
@ -122,6 +139,10 @@ export default {
tempMsgs: [],
dialogMsgSubscribe: null,
pasteShow: false,
pasteFile: [],
pasteItem: [],
}
},
@ -177,6 +198,18 @@ export default {
peopleNum() {
return this.dialogData.type === 'group' ? $A.runNum(this.dialogData.people) : 0;
},
pasteTitle() {
const {pasteItem} = this;
let hasImage = pasteItem.find(({type}) => type == 'image')
let hasFile = pasteItem.find(({type}) => type != 'image')
if (hasImage && hasFile) {
return '发送文件/图片'
} else if (hasImage) {
return '发送图片'
}
return '发送文件'
}
},
@ -264,12 +297,31 @@ export default {
const postFiles = Array.prototype.slice.call(files);
if (postFiles.length > 0) {
e.preventDefault();
postFiles.forEach((file) => {
this.$refs.chatUpload.upload(file);
this.pasteFile = [];
this.pasteItem = [];
postFiles.some(file => {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = ({target}) => {
this.pasteFile.push(file)
this.pasteItem.push({
type: $A.getMiddle(file.type, null, '/'),
name: file.name,
size: file.size,
result: target.result
})
this.pasteShow = true
}
});
}
},
pasteSend() {
this.pasteFile.some(file => {
this.$refs.chatUpload.upload(file)
});
},
chatDragOver(show, e) {
let random = (this.__dialogDrag = $A.randomString(8));
if (!show) {
@ -329,7 +381,6 @@ export default {
sendSuccess(data) {
this.$store.dispatch("saveDialogMsg", data);
this.$store.dispatch("increaseTaskMsgNum", this.dialogId);
this.$store.dispatch("moveDialogTop", this.dialogId);
this.$store.dispatch("updateDialogLastMsg", data);
this.onActive();
},
@ -414,7 +465,7 @@ export default {
}
})
}
}
},
}
}
</script>

View File

@ -75,7 +75,7 @@ Vue.use(Minder)
const MDEditor = () => import('../../../components/MDEditor/index');
const TEditor = () => import('../../../components/TEditor');
const LuckySheet = () => import('../../../components/LuckySheet');
const Flow = () => import('../../../components/flow');
const Flow = () => import('../../../components/Flow');
const OnlyOffice = () => import('../../../components/OnlyOffice');
export default {

View File

@ -51,7 +51,7 @@ Vue.use(Minder)
const MDPreview = () => import('../../../components/MDEditor/preview');
const TEditor = () => import('../../../components/TEditor');
const LuckySheet = () => import('../../../components/LuckySheet');
const Flow = () => import('../../../components/flow');
const Flow = () => import('../../../components/Flow');
const OnlyOffice = () => import('../../../components/OnlyOffice');
export default {

View File

@ -69,14 +69,16 @@
</li>
</ul>
</div>
<div v-if="projectData.desc" class="project-subtitle">{{projectData.desc}}</div>
<div class="project-switch">
<div v-if="completedCount > 0" class="project-checkbox">
<Checkbox :value="projectParameter('completedTask')" @on-change="toggleCompleted">{{$L('显示已完成')}}</Checkbox>
</div>
<div :class="['project-switch-button', !projectParameter('card') ? 'menu' : '']" @click="$store.dispatch('toggleProjectParameter', 'card')">
<div><i class="taskfont">&#xe60c;</i></div>
<div><i class="taskfont">&#xe66a;</i></div>
<div class="project-subbox">
<div class="project-subtitle">{{projectData.desc}}</div>
<div class="project-switch">
<div v-if="completedCount > 0" class="project-checkbox">
<Checkbox :value="projectParameter('completedTask')" @on-change="toggleCompleted">{{$L('显示已完成')}}</Checkbox>
</div>
<div :class="['project-switch-button', !projectParameter('card') ? 'menu' : '']" @click="$store.dispatch('toggleProjectParameter', 'card')">
<div><i class="taskfont">&#xe60c;</i></div>
<div><i class="taskfont">&#xe66a;</i></div>
</div>
</div>
</div>
</div>
@ -595,7 +597,10 @@ export default {
return a.id - b.id;
});
list.forEach((column) => {
column.tasks = this.transforTasks(cacheTasks.filter((task) => {
column.tasks = this.transforTasks(cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.column_id == column.id;
})).sort((a, b) => {
if (a.sort != b.sort) {
@ -655,7 +660,10 @@ export default {
unList() {
const {projectId, cacheTasks, searchText, sortField, sortType} = this;
const array = cacheTasks.filter((task) => {
const array = cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
if (task.project_id != projectId || task.parent_id > 0) {
return false;
}
@ -683,7 +691,10 @@ export default {
completedList() {
const {projectId, cacheTasks, searchText} = this;
const array = cacheTasks.filter((task) => {
const array = cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
if (task.project_id != projectId || task.parent_id > 0) {
return false;
}
@ -703,7 +714,10 @@ export default {
completedCount() {
const {projectId, cacheTasks} = this;
return cacheTasks.filter((task) => {
return cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
if (task.project_id != projectId || task.parent_id > 0) {
return false;
}
@ -766,7 +780,12 @@ export default {
sort = -1;
upTask.push(...item.task.map(id => {
sort++;
upTask.push(...this.cacheTasks.filter(({parent_id}) => parent_id == id).map(({id}) => {
upTask.push(...this.cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.parent_id == id
}).map(({id}) => {
return {
id,
sort,
@ -1161,6 +1180,9 @@ export default {
},
myFilter(task, chackCompleted = true) {
if (task.archived_at) {
return false;
}
if (task.project_id != this.projectId) {
return false;
}
@ -1178,6 +1200,9 @@ export default {
},
helpFilter(task, chackCompleted = true) {
if (task.archived_at) {
return false;
}
if (task.project_id != this.projectId || task.parent_id > 0) {
return false;
}

View File

@ -30,7 +30,7 @@
</Timeline>
</div>
</li>
<li v-if="loadIng > 0" class="logs-loading"><Loading/></li>
<li v-if="loadIng > 0 && showLoad" class="logs-loading"><Loading/></li>
<li v-else-if="hasMorePages" class="logs-more" @click="getMore">{{$L('加载更多')}}</li>
<li v-else-if="totalNum == 0" class="logs-none" @click="getLists(true)">{{$L('没有任何动态')}}</li>
</ul>
@ -52,6 +52,10 @@ export default {
type: Number,
default: 0
},
showLoad: {
type: Boolean,
default: true
},
},
data() {
return {

View File

@ -74,7 +74,7 @@ export default {
},
computed: {
...mapState(['windowMax768'])
...mapState(['cacheTasks', 'windowMax768'])
},
watch: {
projectId: {
@ -98,7 +98,13 @@ export default {
key: 'name',
minWidth: 200,
render: (h, {row}) => {
return h('AutoTip', row.name);
return h('AutoTip', {
on: {
'on-click': () => {
this.$store.dispatch("openTask", row);
}
}
}, row.name);
}
},
{
@ -140,51 +146,70 @@ export default {
align: 'center',
width: 100,
render: (h, params) => {
const recoveryNode = h('Poptip', {
props: {
title: this.$L('你确定要还原归档吗?'),
confirm: true,
transfer: true,
placement: 'left',
},
style: {
fontSize: '13px',
cursor: 'pointer',
color: '#8bcf70',
},
on: {
'on-ok': () => {
this.recovery(params.row);
}
},
}, this.$L('还原'));
const deleteNode = h('Poptip', {
props: {
title: this.$L('你确定要删除任务吗?'),
confirm: true,
transfer: true,
placement: 'left',
},
style: {
marginLeft: '6px',
fontSize: '13px',
cursor: 'pointer',
color: '#f00',
},
on: {
'on-ok': () => {
this.delete(params.row);
}
},
}, this.$L('删除'));
if (this.cacheTasks.find(task => task.id == params.row.id && !task.archived_at)) {
return h('div', {
style: {
color: '#888',
},
}, this.$L('已还原'));
}
const vNodes = [
h('span', {
style: {
fontSize: '13px',
cursor: 'pointer',
color: '#8bcf70',
},
on: {
'click': () => {
this.$store.dispatch("openTask", params.row);
}
},
}, this.$L('查看')),
h('Poptip', {
props: {
title: this.$L('你确定要还原归档吗?'),
confirm: true,
transfer: true,
placement: 'left',
},
style: {
marginLeft: '6px',
fontSize: '13px',
cursor: 'pointer',
color: '#8bcf70',
},
on: {
'on-ok': () => {
this.recovery(params.row);
}
},
}, this.$L('还原')),
h('Poptip', {
props: {
title: this.$L('你确定要删除任务吗?'),
confirm: true,
transfer: true,
placement: 'left',
},
style: {
marginLeft: '6px',
fontSize: '13px',
cursor: 'pointer',
color: '#f00',
},
on: {
'on-ok': () => {
this.delete(params.row);
}
},
}, this.$L('删除'))
];
return h('TableAction', {
props: {
column: params.column
}
}, [
recoveryNode,
deleteNode,
]);
}, vNodes);
}
}
]
@ -239,12 +264,9 @@ export default {
recovery(row) {
this.list = this.list.filter(({id}) => id != row.id);
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/task/archived',
data: {
task_id: row.id,
type: 'recovery'
},
this.$store.dispatch("archivedTask", {
task_id: row.id,
type: 'recovery'
}).then(({msg}) => {
$A.messageSuccess(msg);
this.loadIng--;

View File

@ -79,6 +79,9 @@
<div v-if="taskDetail.flow_item_name" class="flow">
<span :class="taskDetail.flow_item_status" @click.stop="openMenu(taskDetail)">{{taskDetail.flow_item_name}}</span>
</div>
<div v-if="taskDetail.archived_at" class="flow">
<span class="archived" @click.stop="openMenu(taskDetail)">{{$L('已归档')}}</span>
</div>
<div class="nav">
<p v-if="projectName"><span>{{projectName}}</span></p>
<p v-if="columnName"><span>{{columnName}}</span></p>
@ -254,8 +257,10 @@
transfer>
<div class="picker-time">
<div @click="openTime" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
<Tag v-if="!taskDetail.complete_at && taskDetail.today" color="blue"><i class="taskfont">&#xe71d;</i>{{expiresFormat(taskDetail.end_at)}}</Tag>
<Tag v-if="!taskDetail.complete_at && taskDetail.overdue" color="red">{{$L('超期未完成')}}</Tag>
<template v-if="!taskDetail.complete_at">
<Tag v-if="within24Hours(taskDetail.end_at)" color="blue"><i class="taskfont">&#xe71d;</i>{{expiresFormat(taskDetail.end_at)}}</Tag>
<Tag v-if="taskDetail.overdue" color="red">{{$L('超期未完成')}}</Tag>
</template>
</div>
</DatePicker>
</li>
@ -368,7 +373,7 @@
</div>
</div>
</div>
<ProjectLog v-if="navActive=='log' && taskId > 0" ref="log" :task-id="taskDetail.id" @on-load-change="logLoadChange"/>
<ProjectLog v-if="navActive=='log' && taskId > 0" ref="log" :task-id="taskDetail.id" :show-load="false" @on-load-change="logLoadChange"/>
<div v-else class="no-dialog">
<div class="no-tip">{{$L('暂无消息')}}</div>
<div class="no-input">
@ -565,8 +570,11 @@ export default {
if (!this.taskId) {
return [];
}
return this.cacheTasks.filter(({parent_id}) => {
return parent_id == this.taskId
return this.cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.parent_id == this.taskId
}).sort((a, b) => {
return a.id - b.id;
});
@ -708,6 +716,10 @@ export default {
this.innerHeight = Math.min(1100, window.innerHeight);
},
within24Hours(date) {
return Math.round($A.Date(date).getTime() / 1000) - this.nowTime < 86400
},
expiresFormat(date) {
return $A.countDownFormat(date, this.nowTime)
},
@ -762,31 +774,6 @@ export default {
})
},
archivedOrRemoveTask(type) {
let typeDispatch = type == 'remove' ? 'removeTask' : 'archivedTask';
let typeName = type == 'remove' ? '删除' : '归档';
let typeTask = this.taskDetail.parent_id > 0 ? '子任务' : '任务';
$A.modalConfirm({
title: typeName + typeTask,
content: '你确定要' + typeName + typeTask + '【' + this.taskDetail.name + '】吗?',
loading: true,
onOk: () => {
if (this.taskDetail.loading === true) {
this.$Modal.remove();
return;
}
this.$set(this.taskDetail, 'loading', true);
this.$store.dispatch(typeDispatch, this.taskDetail.id).then(({msg}) => {
$A.messageSuccess(msg);
this.$Modal.remove();
}).catch(({msg}) => {
$A.modalError(msg, 301);
this.$Modal.remove();
});
}
});
},
openOwner() {
const list = this.getOwner.map(({userid}) => userid)
this.$set(this.taskDetail, 'owner_userid', list)

View File

@ -48,7 +48,7 @@
<template v-if="task.parent_id === 0">
<EDropdownItem :divided="turns.length > 0" command="archived">
<div class="item">
<Icon type="ios-filing" />{{$L('归档')}}
<Icon type="ios-filing" />{{$L(task.archived_at ? '还原归档' : '归档')}}
</div>
</EDropdownItem>
<EDropdownItem command="remove">
@ -246,9 +246,21 @@ export default {
},
archivedOrRemoveTask(type) {
let typeDispatch = type == 'remove' ? 'removeTask' : 'archivedTask';
let typeName = type == 'remove' ? '删除' : '归档';
let typeDispatch = 'removeTask';
let typeName = '删除';
let typeData = this.task.id;
let typeTask = this.task.parent_id > 0 ? '子任务' : '任务';
if (type == 'archived') {
typeDispatch = 'archivedTask'
typeName = '归档'
if (this.task.archived_at) {
typeName = '还原归档'
typeData = {
task_id: this.task.id,
type: 'recovery'
}
}
}
$A.modalConfirm({
title: typeName + typeTask,
content: '你确定要' + typeName + typeTask + '【' + this.task.name + '】吗?',
@ -258,7 +270,7 @@ export default {
this.$Modal.remove();
return;
}
this.$store.dispatch(typeDispatch, this.task.id).then(({msg}) => {
this.$store.dispatch(typeDispatch, typeData).then(({msg}) => {
$A.messageSuccess(msg);
this.$Modal.remove();
}).catch(({msg}) => {

View File

@ -163,8 +163,11 @@ export default {
subTask() {
return function(task_id) {
return this.cacheTasks.filter(({parent_id}) => {
return parent_id == task_id
return this.cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.parent_id == task_id
}).sort((a, b) => {
return a.id - b.id;
});

View File

@ -14,7 +14,7 @@
v-for="(item, key) in dialogType"
:key="key"
:class="{active:dialogActive==item.type}"
@click="dialogActive=item.type">
@click="onActive(item.type)">
<Badge class="nav-num" :count="msgUnread(item.type)"/>
{{$L(item.name)}}
</p>
@ -27,18 +27,22 @@
<ul v-if="tabActive==='dialog'" class="dialog">
<li
v-for="(dialog, key) in dialogList"
:ref="`dialog_${dialog.id}`"
:key="key"
:class="{active: dialog.id == dialogId}"
@click="openDialog(dialog, true)">
<template v-if="dialog.type=='group'">
<i v-if="dialog.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="dialog.group_type=='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<i v-else-if="dialog.group_type=='task'" class="taskfont icon-avatar task" :class="{completed:$A.dialogCompleted(dialog)}">&#xe6f4;</i>
<Icon v-else class="icon-avatar" type="ios-people" />
</template>
<div v-else-if="dialog.dialog_user" class="user-avatar"><UserAvatar :userid="dialog.dialog_user.userid" :size="42"/></div>
<Icon v-else class="icon-avatar" type="md-person" />
<div class="dialog-box">
<div class="dialog-title">
<template v-for="tag in $A.dialogTags(dialog)" v-if="tag.color != 'success'">
<Tag :color="tag.color" :fade="false">{{$L(tag.text)}}</Tag>
</template>
<span>{{dialog.name}}</span>
<Icon v-if="dialog.type == 'user' && lastMsgReadDone(dialog.last_msg)" :type="lastMsgReadDone(dialog.last_msg)"/>
<em v-if="dialog.last_at">{{$A.formatTime(dialog.last_at)}}</em>
@ -123,22 +127,24 @@ export default {
dialogList() {
const {dialogActive, dialogKey} = this;
if (dialogActive == '' && dialogKey == '') {
return this.cacheDialogs.filter(({name}) => name !== undefined);
return this.cacheDialogs.filter(dialog => this.filterDialog(dialog)).sort((a, b) => {
return $A.Date(b.last_at) - $A.Date(a.last_at);
});
}
return this.cacheDialogs.filter(({name, type, group_type, last_msg}) => {
if (name === undefined) {
return this.cacheDialogs.filter(dialog => {
if (!this.filterDialog(dialog)) {
return false;
}
if (dialogActive) {
switch (dialogActive) {
case 'project':
case 'task':
if (group_type != dialogActive) {
if (dialog.group_type != dialogActive) {
return false;
}
break;
case 'user':
if (type != 'user') {
if (dialog.type != 'user') {
return false;
}
break;
@ -147,20 +153,22 @@ export default {
}
}
if (dialogKey) {
let existName = $A.strExists(name, dialogKey);
let existMsg = last_msg && last_msg.type === 'text' && $A.strExists(last_msg.msg.text, dialogKey);
let existName = $A.strExists(dialog.name, dialogKey);
let existMsg = dialog.last_msg && dialog.last_msg.type === 'text' && $A.strExists(dialog.last_msg.msg.text, dialogKey);
if (!existName && !existMsg) {
return false;
}
}
return true;
}).sort((a, b) => {
return $A.Date(b.last_at) - $A.Date(a.last_at);
})
},
msgUnread() {
return function (type) {
let num = 0;
this.cacheDialogs.map((dialog) => {
this.cacheDialogs.some((dialog) => {
if (dialog.unread) {
switch (type) {
case 'project':
@ -219,6 +227,24 @@ export default {
}
},
onActive(type) {
if (this.dialogActive == type) {
//
const dialog = this.dialogList.find(({unread}) => unread > 0)
if (dialog) {
try {
this.$refs[`dialog_${dialog.id}`][0].scrollIntoView();
} catch (e) {
scrollIntoView(this.$refs[`dialog_${dialog.id}`][0], {
behavior: 'instant',
inline: 'end',
})
}
}
}
this.dialogActive = type
},
closeDialog() {
this.dialogId = 0;
$A.setStorage("messenger::dialogId", 0)
@ -245,6 +271,44 @@ export default {
});
},
filterDialog(dialog) {
if (dialog.unread > 0 || dialog.id == this.dialogId) {
return true
}
if (dialog.name === undefined) {
return false;
}
if (!dialog.last_at) {
return false;
}
if (dialog.type == 'group') {
if (['project', 'task'].includes(dialog.group_type) && $A.isJson(dialog.group_info)) {
if (dialog.group_type == 'task' && dialog.group_info.complete_at) {
// 5
let time = Math.max($A.Date(dialog.last_at, true), $A.Date(dialog.group_info.complete_at, true))
if (5 * 86400 + time < $A.Time()) {
return false
}
}
if (dialog.group_info.deleted_at) {
// 2
let time = Math.max($A.Date(dialog.last_at, true), $A.Date(dialog.group_info.deleted_at, true))
if (2 * 86400 + time < $A.Time()) {
return false
}
}
if (dialog.group_info.archived_at) {
// 3
let time = Math.max($A.Date(dialog.last_at, true), $A.Date(dialog.group_info.archived_at, true))
if (3 * 86400 + time < $A.Time()) {
return false
}
}
}
}
return true;
},
getContactsList(page) {
if (this.contactsData === null) {
this.contactsData = {};

View File

@ -65,7 +65,10 @@ export default {
return;
}
this.loadIng++;
this.$store.dispatch("getTaskOne", task_id).then(({data}) => {
this.$store.dispatch("getTaskOne", {
task_id,
archived: 'all'
}).then(({data}) => {
this.loadIng--;
this.taskInfo = data;
this.$store.dispatch("getTaskContent", task_id);

View File

@ -538,6 +538,19 @@ export default {
}
state.cacheProjects.push(data);
}
//
state.cacheDialogs.some(dialog => {
if (dialog.type == 'group' && dialog.group_type == 'project' && dialog.group_info.id == data.id) {
if (data.name !== undefined) {
dialog.name = data.name
}
for (let key in dialog.group_info) {
if (!dialog.group_info.hasOwnProperty(key) || data[key] === undefined) continue;
dialog.group_info[key] = data[key];
}
}
})
//
setTimeout(() => {
$A.setStorage("cacheProjects", state.cacheProjects);
})
@ -896,6 +909,18 @@ export default {
dispatch("getTaskForParent", data.id).catch(() => {})
}
//
state.cacheDialogs.some(dialog => {
if (dialog.type == 'group' && dialog.group_type == 'task' && dialog.group_info.id == data.id) {
if (data.name !== undefined) {
dialog.name = data.name
}
for (let key in dialog.group_info) {
if (!dialog.group_info.hasOwnProperty(key) || data[key] === undefined) continue;
dialog.group_info[key] = data[key];
}
}
})
//
setTimeout(() => {
$A.setStorage("cacheTasks", state.cacheTasks);
})
@ -1010,20 +1035,21 @@ export default {
* 获取单个任务
* @param state
* @param dispatch
* @param task_id
* @param data Number|JSONObject{task_id, ?archived_at}
* @returns {Promise<unknown>}
*/
getTaskOne({state, dispatch}, task_id) {
getTaskOne({state, dispatch}, data) {
return new Promise(function (resolve, reject) {
if ($A.runNum(task_id) === 0) {
if (/^\d+$/.test(data)) {
data = {task_id: data}
}
if ($A.runNum(data.task_id) === 0) {
reject({msg: 'Parameter error'});
return;
}
dispatch("call", {
url: 'project/task/one',
data: {
task_id,
},
data,
}).then(result => {
dispatch("saveTask", result.data);
resolve(result)
@ -1125,7 +1151,10 @@ export default {
const newIds = state.cacheTasks.filter(task => task.parent_id == parent_id && task._time >= time).map(({id}) => id)
dispatch("forgetTask", currentIds.filter(v => newIds.indexOf(v) == -1))
}
dispatch("getTasks", {parent_id}).then(() => {
dispatch("getTasks", {
parent_id,
archived: 'all'
}).then(() => {
call()
resolve()
}).catch(() => {
@ -1168,32 +1197,33 @@ export default {
},
/**
* 归档任务
* 归档还原任务
* @param state
* @param dispatch
* @param task_id
* @param data Number|JSONObject{task_id, ?archived_at}
* @returns {Promise<unknown>}
*/
archivedTask({state, dispatch}, task_id) {
archivedTask({state, dispatch}, data) {
return new Promise(function (resolve, reject) {
if ($A.runNum(task_id) === 0) {
if (/^\d+$/.test(data)) {
data = {task_id: data}
}
if ($A.runNum(data.task_id) === 0) {
reject({msg: 'Parameter error'});
return;
}
dispatch("taskLoadStart", task_id)
dispatch("taskLoadStart", data.task_id)
dispatch("call", {
url: 'project/task/archived',
data: {
task_id: task_id,
},
data,
}).then(result => {
dispatch("forgetTask", task_id)
dispatch("taskLoadEnd", task_id)
dispatch("saveTask", result.data)
dispatch("taskLoadEnd", data.task_id)
resolve(result)
}).catch(e => {
console.warn(e);
dispatch("getTaskOne", task_id).catch(() => {})
dispatch("taskLoadEnd", task_id)
dispatch("getTaskOne", data.task_id).catch(() => {})
dispatch("taskLoadEnd", data.task_id)
reject(e)
});
});
@ -1292,7 +1322,10 @@ export default {
}
state.taskId = task_id;
if (task_id > 0) {
dispatch("getTaskOne", task_id).then(() => {
dispatch("getTaskOne", {
task_id,
archived: 'all'
}).then(() => {
dispatch("getTaskContent", task_id);
dispatch("getTaskFiles", task_id);
dispatch("getTaskForParent", task_id).catch(() => {})
@ -1749,22 +1782,6 @@ export default {
});
},
/**
* 将会话移动到首位
* @param state
* @param dialog_id
*/
moveDialogTop({state}, dialog_id) {
$A.execMainDispatch("moveDialogTop", dialog_id)
//
const index = state.cacheDialogs.findIndex(({id}) => id == dialog_id);
if (index > -1) {
const tmp = $A.cloneJSON(state.cacheDialogs[index]);
state.cacheDialogs.splice(index, 1);
state.cacheDialogs.unshift(tmp);
}
},
/**
* 忘记对话数据
* @param state
@ -2026,8 +2043,6 @@ export default {
if (dialog) {
// 新增未读数
dialog.unread++;
// 移动到首位
dispatch("moveDialogTop", dialog_id);
}
Store.set('dialogMsgPush', data);
}
@ -2105,7 +2120,6 @@ export default {
case 'filedelete':
dispatch("forgetTaskFile", data.id)
break;
case 'archived':
case 'delete':
dispatch("forgetTask", data.id)
break;

View File

@ -119,6 +119,9 @@ export default {
todayEnd = $A.Date($A.formatDate("Y-m-d 23:59:59")),
todayNow = $A.Date($A.formatDate("Y-m-d H:i:s"));
const filterTask = (task, chackCompleted = true) => {
if (task.archived_at) {
return false;
}
if (task.complete_at && chackCompleted === true) {
return false;
}

View File

@ -15,7 +15,7 @@
padding: 0 30px;
height: 68px;
position: relative;
&:after {
&:before {
content: "";
position: absolute;
left: 0;
@ -24,11 +24,41 @@
height: 1px;
background-color: #f4f5f5;
}
&.completed {
&:after {
content: "\f373";
font-family: Ionicons, serif;
pointer-events: none;
position: absolute;
top: 50%;
right: 24px;
transform: translateY(-50%);
font-size: 52px;
color: #19be6b;
opacity: .2;
z-index: 1;
}
}
.main-title {
display: flex;
align-items: center;
line-height: 22px;
max-width: 100%;
.ivu-tag {
margin: 0 6px 0 0;
padding: 0 5px;
&.ivu-tag-success {
padding: 0 6px;
}
}
.ivu-icon {
font-size: 18px;
margin-right: 6px;
&.completed {
color: $primary-color;
}
}
> h2 {
font-size: 17px;
font-weight: 600;
@ -411,6 +441,13 @@
}
}
.dialog-wrapper-paste {
img {
max-width: 100%;
max-height: 1000px;
}
}
@media (max-width: 768px) {
.dialog-wrapper {
.dialog-footer {

View File

@ -112,77 +112,82 @@
}
}
}
.project-subtitle {
width: 100%;
color: #999999;
line-height: 24px;
margin-top: -6px;
margin-bottom: -18px;
padding-right: 260px;
}
.project-switch {
.project-subbox {
width: 100%;
display: flex;
justify-content: flex-end;
.project-checkbox {
display: flex;
align-items: center;
margin-right: 14px;
opacity: 0.9;
.ivu-checkbox-focus {
box-shadow: none;
}
justify-content: space-between;
.project-subtitle {
flex: 1;
color: #999999;
line-height: 24px;
}
.project-switch-button {
.project-switch {
margin-left: 80px;
flex-shrink: 0;
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 6px;
position: relative;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 0 10px #e6ecfa;
}
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
z-index: 0;
color: $primary-color;
border-radius: 6px;
border: 1px solid $primary-color;
background-color: rgba($primary-color, 0.1);
transition: left 0.2s;
}
> div {
z-index: 1;
width: 32px;
height: 30px;
align-items: flex-end;
.project-checkbox {
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: $primary-text-color;
> i {
font-size: 17px;
}
&:first-child {
color: $primary-color;
margin-right: 14px;
opacity: 0.9;
height: 30px;
.ivu-checkbox-focus {
box-shadow: none;
}
}
&.menu {
.project-switch-button {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 6px;
height: 30px;
position: relative;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 0 10px #e6ecfa;
}
&:before {
left: 50%;
}
> div:first-child {
color: $primary-text-color;
}
> div:last-child {
content: "";
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
z-index: 0;
color: $primary-color;
border-radius: 6px;
border: 1px solid $primary-color;
background-color: rgba($primary-color, 0.1);
transition: left 0.2s;
}
> div {
z-index: 1;
width: 32px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: $primary-text-color;
> i {
font-size: 17px;
}
&:first-child {
color: $primary-color;
}
}
&.menu {
&:before {
left: 50%;
}
> div:first-child {
color: $primary-text-color;
}
> div:last-child {
color: $primary-color;
}
}
}
}
@ -934,9 +939,15 @@
justify-content: flex-end;
}
}
.project-subtitle {
padding-right: 12px;
margin-bottom: 6px;
.project-subbox {
display: block;
.project-subtitle {
margin-bottom: 6px;
}
.project-switch {
margin-left: 0;
justify-content: flex-end;
}
}
}
.project-column {

View File

@ -118,7 +118,7 @@
.log-text {
display: inline-block;
color: rgba(0, 0, 0, .8);
color: rgba(0, 0, 0, .72);
.detail-user {
display: inline-block;

View File

@ -27,6 +27,7 @@
background-color: #f4f5f5;
}
.icon {
margin-right: 18px;
.task-menu-icon {
display: flex;
align-items: center;
@ -47,8 +48,7 @@
}
}
.flow {
margin-left: 18px;
margin-right: -3px;
margin-right: 10px;
> span {
font-size: 14px;
height: 26px;
@ -60,7 +60,8 @@
display: inline-block;
text-align: center;
cursor: pointer;
&.start {
&.start,
&.archived {
background-color: rgba(38, 38, 38, 0.05);
border-color: rgba(38, 38, 38, 0.05);
color: #595959;
@ -81,7 +82,6 @@
flex: 1;
display: flex;
align-items: center;
margin-left: 18px;
font-size: 12px;
width: 0;
height: 40px;

View File

@ -111,6 +111,20 @@
background-color: #9B96DF;
font-size: 24px;
}
&.completed {
&:after {
content: "\f373";
font-family: Ionicons, serif;
pointer-events: none;
position: absolute;
bottom: 0;
right: 12px;
font-size: 32px;
color: #19be6b;
opacity: .2;
z-index: 1;
}
}
}
.dialog-box {
flex: 1;
@ -124,6 +138,13 @@
align-items: center;
justify-content: space-between;
line-height: 24px;
.ivu-tag {
margin: 0 4px 0 0;
padding: 0 5px;
&.ivu-tag-success {
padding: 0 6px;
}
}
> span {
flex: 1;
color: #333333;
@ -138,6 +159,11 @@
transform: scale(0.9);
font-size: 12px;
color: $primary-color;
&.completed {
font-size: 18px;
margin: 0 4px 0 0;
transform: scale(1);
}
}
> em {
flex-shrink: 0;