no message

This commit is contained in:
kuaifan 2021-06-13 23:10:49 +08:00
parent f4cd7fe1e2
commit 74fa8ff9a1
15 changed files with 835 additions and 252 deletions

View File

@ -247,43 +247,8 @@ class DialogController extends AbstractController
if (Base::isError($data)) {
return Base::retError($data['msg']);
} else {
$fileData = $data['data'];
$fileData['thumb'] = $fileData['thumb'] ?: 'images/ext/file.png';
switch ($fileData['ext']) {
case "docx":
$fileData['thumb'] = 'images/ext/doc.png';
break;
case "xlsx":
$fileData['thumb'] = 'images/ext/xls.png';
break;
case "pptx":
$fileData['thumb'] = 'images/ext/ppt.png';
break;
case "ai":
case "avi":
case "bmp":
case "cdr":
case "doc":
case "eps":
case "gif":
case "mov":
case "mp3":
case "mp4":
case "pdf":
case "ppt":
case "pr":
case "psd":
case "rar":
case "svg":
case "tif":
case "txt":
case "xls":
case "zip":
$fileData['thumb'] = 'images/ext/' . $fileData['ext'] . '.png';
break;
}
//
$msg = $fileData;
$msg = $data['data'];
$msg['thumb'] = Base::unFillUrl($msg['thumb']);
$msg['size'] *= 1024;
//
return WebSocketDialogMsg::sendMsg($dialog_id, 'file', $msg, $user->userid, $extra_int, $extra_str);

View File

@ -7,6 +7,7 @@ use App\Models\Project;
use App\Models\ProjectColumn;
use App\Models\ProjectLog;
use App\Models\ProjectTask;
use App\Models\ProjectTaskFile;
use App\Models\ProjectUser;
use App\Models\User;
use App\Module\Base;
@ -723,7 +724,7 @@ class ProjectController extends AbstractController
* @apiParam {String} name 任务描述
* @apiParam {String} [content] 任务详情
* @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间2020-01-01 00:00,2020-01-01 23:59
* @apiParam {mixed} [owner] 负责人,留空为自己
* @apiParam {Number} [owner] 负责人,留空为自己
* @apiParam {Array} [subtasks] 子任务(格式:[{name,owner,times}]
* @apiParam {Number} [top] 添加的任务排到列表最前面
*/
@ -750,9 +751,6 @@ class ProjectController extends AbstractController
// 列表
$column = null;
$newColumn = null;
if (is_array($column_id)) {
$column_id = Base::arrayFirst($column_id);
}
if ($column_id) {
if (intval($column_id) > 0) {
$column = $project->projectColumn->find($column_id);
@ -760,6 +758,8 @@ class ProjectController extends AbstractController
if (empty($column)) {
$column = ProjectColumn::whereProjectId($project->id)->whereName($column_id)->first();
}
} else {
$column = ProjectColumn::whereProjectId($project->id)->orderBy('id')->first();
}
if (empty($column)) {
$column = ProjectColumn::createInstance([
@ -798,7 +798,8 @@ class ProjectController extends AbstractController
* @apiParam {String} [color] 任务描述(子任务不支持)
* @apiParam {String} [content] 任务详情(子任务不支持)
* @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间2020-01-01 00:00,2020-01-01 23:59
* @apiParam {mixed} [owner] 修改负责人
* @apiParam {Number} [owner] 修改负责人
* @apiParam {Array} [assist] 修改协助人员
*
* @apiParam {String|false} [complete_at] 完成时间2020-01-01 00:00false表示未完成
*/
@ -850,6 +851,76 @@ class ProjectController extends AbstractController
return $result;
}
/**
* {post} 上传文件
*
* @apiParam {Number} task_id 任务ID
* @apiParam {String} [filename] post-文件名称
* @apiParam {String} [image64] post-base64图片二选一
* @apiParam {File} [files] post-文件对象(二选一)
*/
public function task__upload()
{
$user = User::authE();
if (Base::isError($user)) {
return $user;
} else {
$user = User::IDE($user['data']);
}
//
$task_id = Base::getPostInt('task_id');
// 任务
$task = ProjectTask::whereId($task_id)->first();
if (empty($task)) {
return Base::retError('任务不存在');
}
// 项目
$project = Project::select($this->projectSelect)
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('projects.id', $task->project_id)
->where('project_users.userid', $user->userid)
->first();
if (empty($project)) {
return Base::retError('项目不存在或不在成员列表内');
}
//
$path = "uploads/task/" . $task->id . "/";
$image64 = Base::getPostValue('image64');
$fileName = Base::getPostValue('filename');
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
]);
} else {
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'file',
"path" => $path,
"fileName" => $fileName,
]);
}
//
if (Base::isError($data)) {
return Base::retError($data['msg']);
} else {
$fileData = $data['data'];
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'] * 1024,
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => Base::unFillUrl($fileData['thumb']),
'userid' => $user->userid,
]);
$file->save();
return Base::retSuccess("上传成功", $file->find($file->id));
}
}
/**
* 归档任务
*

View File

@ -27,6 +27,9 @@ class VerifyCsrfToken extends Middleware
// 修改任务
'api/project/task/update/',
// 上传任务问题
'api/project/task/upload/',
// 聊天发文件
'api/dialog/msg/sendfile/',
];

View File

@ -299,10 +299,7 @@ class ProjectTask extends AbstractModel
}
}
// 负责人
if (is_array($owner)) {
$owner = Base::arrayFirst($owner);
}
$owner = $owner ?: User::token2userid();
$owner = intval($owner) ?: User::token2userid();
if (!ProjectUser::whereProjectId($project_id)->whereUserid($owner)->exists()) {
return Base::retError($retPre . '负责人填写错误');
}
@ -360,9 +357,6 @@ class ProjectTask extends AbstractModel
public function updateTask($data)
{
return AbstractModel::transaction(function () use ($data) {
$content = $data['content'];
$times = $data['times'];
$owner = $data['owner'];
// 标题
if (Arr::exists($data, 'name')) {
if (empty($data['name'])) {
@ -373,25 +367,48 @@ class ProjectTask extends AbstractModel
$this->name = $data['name'];
}
// 负责人
if ($owner) {
if (is_array($owner)) {
$owner = Base::arrayFirst($owner);
}
$ownerUser = ProjectTaskUser::whereTaskId($this->id)->whereOwner(1)->first();
if ($ownerUser->userid != $owner) {
$ownerUser->owner = 0;
$ownerUser->save();
if (Arr::exists($data, 'owner')) {
$row = ProjectTaskUser::whereTaskId($this->id)->whereOwner(1)->first();
if ($row->userid != $data['owner']) {
if (!User::find(intval($data['owner']))) {
return Base::retError('请选择正确的负责人');
}
$row->owner = 0;
$row->save();
ProjectTaskUser::updateInsert([
'project_id' => $this->parent_id,
'task_id' => $this->id,
'userid' => $owner,
'userid' => $data['owner'],
], [
'owner' => 1,
]);
}
}
// 协助人员
if (Arr::exists($data, 'assist')) {
$array = [];
$assist = is_array($data['assist']) ? $data['assist'] : [$data['assist']];
foreach ($assist as $uid) {
if (intval($uid) == 0) continue;
if (ProjectTaskUser::whereTaskId($this->id)->whereUserid($uid)->whereOwner(1)->exists()) continue;
//
if (!ProjectTaskUser::whereTaskId($this->id)->whereUserid($uid)->where('owner', '!=', 1)->exists()) {
ProjectTaskUser::createInstance([
'project_id' => $this->parent_id,
'task_id' => $this->id,
'userid' => $uid,
'owner' => 0,
])->save();
}
$array[] = $uid;
}
ProjectTaskUser::whereTaskId($this->id)->where('owner', '!=', 1)->whereNotIn('userid', $array)->delete();
}
// 计划时间
if ($times) {
if (Arr::exists($data, 'times')) {
$this->start_at = null;
$this->end_at = null;
$times = $data['times'];
list($start, $end) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
if (Base::isDate($start) && Base::isDate($end)) {
if ($start != $end) {
@ -407,14 +424,14 @@ class ProjectTask extends AbstractModel
$this->color = $data['color'];
}
// 内容
if ($content && $this->parent_id === 0) {
if (Arr::exists($data, 'content')) {
ProjectTaskContent::updateInsert([
'project_id' => $this->parent_id,
'task_id' => $this->id,
], [
'content' => $content,
'content' => $data['content'],
]);
$this->desc = Base::getHtml($content);
$this->desc = Base::getHtml($data['content']);
}
// 优先级
if (Arr::exists($data, 'p_level')) {
@ -428,6 +445,8 @@ class ProjectTask extends AbstractModel
}
}
$this->save();
if ($this->start_at instanceof \DateTimeInterface) $this->start_at = $this->start_at->format('Y-m-d H:i:s');
if ($this->end_at instanceof \DateTimeInterface) $this->end_at = $this->end_at->format('Y-m-d H:i:s');
return Base::retSuccess('修改成功');
});
}

View File

@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* Class ProjectTaskFile
*
@ -37,5 +39,23 @@ namespace App\Models;
*/
class ProjectTaskFile extends AbstractModel
{
/**
* 地址
* @param $value
* @return string
*/
public function getPathAttribute($value)
{
return Base::fillUrl($value);
}
/**
* 缩略图
* @param $value
* @return string
*/
public function getThumbAttribute($value)
{
return Base::fillUrl($value ?: Base::extIcon($this->ext));
}
}

View File

@ -79,8 +79,8 @@ class WebSocketDialogMsg extends AbstractModel
$value = Base::json2array($value);
if ($this->type === 'file') {
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
$value['url'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb']);
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
}
return $value;
}

View File

@ -2486,6 +2486,50 @@ class Base
return true;
}
/**
* 获取后缀名图标相对地址
* @param $ext
* @return string
*/
public static function extIcon($ext)
{
$value = 'images/ext/file.png';
switch ($ext) {
case "docx":
$value = 'images/ext/doc.png';
break;
case "xlsx":
$value = 'images/ext/xls.png';
break;
case "pptx":
$value = 'images/ext/ppt.png';
break;
case "ai":
case "avi":
case "bmp":
case "cdr":
case "doc":
case "eps":
case "gif":
case "mov":
case "mp3":
case "mp4":
case "pdf":
case "ppt":
case "pr":
case "psd":
case "rar":
case "svg":
case "tif":
case "txt":
case "xls":
case "zip":
$value = 'images/ext/' . $ext . '.png';
break;
}
return $value;
}
/**
* 排列组合(无重复)
* @param $arr

View File

@ -110,7 +110,7 @@
maxWidth: projectOpenTask._dialog || projectOpenTask._msgText ? '1200px' : '640px'
}"
footer-hide>
<TaskDetail/>
<TaskDetail :open-task="projectOpenTask"/>
</Modal>
</div>
</template>

View File

@ -7,7 +7,7 @@
<div v-else-if="msgData.type === 'loading'" class="dialog-content loading"><Loading/></div>
<!--文件-->
<div v-else-if="msgData.type === 'file'" :class="['dialog-content', msgData.msg.type]">
<a :href="msgData.msg.url" target="_blank">
<a :href="msgData.msg.path" target="_blank">
<img v-if="msgData.msg.type === 'img'" class="file-img" :style="imageStyle(msgData.msg)" :src="msgData.msg.thumb"/>
<div v-else class="file-box">
<img class="file-thumb" :src="msgData.msg.thumb"/>

View File

@ -107,7 +107,6 @@
:add-top="true"
@on-close="column.addTopShow=false"
@on-priority="addTaskOpen"
@on-success="addTaskSuccess"
auto-active/>
</div>
<Draggable
@ -195,8 +194,7 @@
<TaskAddSimple
:column-id="column.id"
:project-id="projectDetail.id"
@on-priority="addTaskOpen"
@on-success="addTaskSuccess"/>
@on-priority="addTaskOpen"/>
</div>
</Draggable>
</div>
@ -589,13 +587,8 @@ export default {
onAddTask() {
this.taskLoad++;
this.$store.dispatch("call", {
url: 'project/task/add',
data: this.addData,
method: 'post',
}).then(({data, msg}) => {
this.$store.dispatch("taskAdd", this.addData).then(({msg}) => {
this.taskLoad--;
$A.messageSuccess(msg);
this.addShow = false;
this.addData = {
owner: 0,
@ -606,8 +599,7 @@ export default {
p_name: '',
p_color: '',
};
this.$store.dispatch('projectOne', data.project_id);
this.addTaskSuccess(data)
$A.messageSuccess(msg);
}).catch(({msg}) => {
this.taskLoad--;
$A.modalError(msg);
@ -630,21 +622,6 @@ export default {
this.addShow = true;
},
addTaskSuccess(data) {
const {task, in_top, new_column} = data;
if (new_column) {
this.projectDetail.project_column.push(new_column)
}
const column = this.projectDetail.project_column.find(({id}) => id === task.column_id);
if (column) {
if (in_top) {
column.project_task.unshift(task);
} else {
column.project_task.push(task);
}
}
},
addColumnOpen() {
this.addColumnShow = true;
this.$nextTick(() => {

View File

@ -151,13 +151,8 @@ export default {
return;
}
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/task/add',
data: this.getData(),
method: 'post',
}).then(({data, msg}) => {
this.$store.dispatch("taskAdd", this.getData()).then(({msg}) => {
this.loadIng--;
$A.messageSuccess(msg);
this.active = false;
this.addData = {
owner: 0,
@ -168,8 +163,7 @@ export default {
p_name: '',
p_color: '',
}
this.$store.dispatch('projectOne', data.project_id);
this.$emit("on-success", data)
$A.messageSuccess(msg);
}).catch(({msg}) => {
this.loadIng--;
$A.modalError(msg);

View File

@ -1,5 +1,90 @@
<template>
<div :class="{'task-detail':true, 'open-dialog': taskDetail._dialog || taskDetail._msgText, 'completed': taskDetail.complete_at}">
<!--子任务-->
<li v-if="taskDetail.parent_id > 0">
<div class="subtask-icon">
<div v-if="taskDetail.loading === true" class="loading"><Loading /></div>
<EDropdown
v-else
trigger="click"
placement="bottom"
size="small"
@command="dropTask">
<div>
<Icon v-if="taskDetail.complete_at" class="completed" type="md-checkmark-circle" />
<Icon v-else type="md-radio-button-off" />
</div>
<EDropdownMenu slot="dropdown" class="project-list-more-dropdown-menu">
<EDropdownItem v-if="taskDetail.complete_at" command="uncomplete">
<div class="item red">
<Icon type="md-checkmark-circle-outline" />{{$L('标记未完成')}}
</div>
</EDropdownItem>
<EDropdownItem v-else command="complete">
<div class="item">
<Icon type="md-radio-button-off" />{{$L('完成')}}
</div>
</EDropdownItem>
<EDropdownItem command="delete">
<div class="item">
<Icon type="md-trash" />{{$L('删除')}}
</div>
</EDropdownItem>
</EDropdownMenu>
</EDropdown>
</div>
<div class="subtask-name">
<Input
v-model="taskDetail.name"
type="textarea"
:rows="1"
:autosize="{ minRows: 1, maxRows: 8 }"
:maxlength="255"
@on-blur="updateData('name')"
@on-keydown="onNameKeydown"/>
</div>
<DatePicker
v-model="timeValue"
:open="timeOpen"
:options="timeOptions"
format="yyyy-MM-dd HH:mm"
type="datetimerange"
class="subtask-time"
@on-open-change="timeChange"
@on-clear="timeClear"
@on-ok="timeOk"
transfer>
<div
@click="openTime"
:class="['time', taskDetail.today ? 'today' : '', taskDetail.overdue ? 'overdue' : '']">
{{taskDetail.end_at ? expiresFormat(taskDetail.end_at) : '--'}}
</div>
</DatePicker>
<Poptip
ref="owner"
class="subtask-avatar"
:title="$L('修改负责人')"
:width="240"
placement="bottom"
@on-popper-show="openOwner"
@on-popper-hide="ownerShow=false"
@on-ok="onOwner"
transfer>
<div slot="content">
<UserInput
v-if="ownerShow"
v-model="ownerData.owner_userid"
:multiple-max="1"
:placeholder="$L('选择任务负责人')"/>
<div class="task-detail-avatar-buttons">
<Button size="small" type="primary" @click="$refs.owner.ok()">{{$L('确定')}}</Button>
</div>
</div>
<UserAvatar v-if="getOwner" :userid="getOwner.userid" :size="20" hide-icon-menu/>
<div v-else>--</div>
</Poptip>
</li>
<!--主任务-->
<div v-else v-show="taskDetail.id > 0" :class="{'task-detail':true, 'open-dialog': taskDetail._dialog || taskDetail._msgText, 'completed': taskDetail.complete_at}">
<div class="task-info">
<div class="head">
<Icon v-if="taskDetail.complete_at" class="icon completed" type="md-checkmark-circle" @click="updateData('uncomplete')"/>
@ -19,7 +104,8 @@
:rows="1"
:autosize="{ minRows: 1, maxRows: 8 }"
:maxlength="255"
@on-blur="updateData('name')"/>
@on-blur="updateData('name')"
@on-keydown="onNameKeydown"/>
</div>
<div class="desc">
<TEditor
@ -40,7 +126,7 @@
<li>
<EDropdown
trigger="click"
placement="bottom-start"
placement="bottom"
@command="updateData('priority', $event)">
<TaskPriority :backgroundColor="taskDetail.p_color">{{taskDetail.p_name}}</TaskPriority>
<EDropdownMenu slot="dropdown">
@ -56,28 +142,89 @@
</li>
</ul>
</FormItem>
<FormItem v-if="getOwner()">
<FormItem>
<div class="item-label" slot="label">
<i class="iconfont">&#xe6e4;</i>{{$L('负责人')}}
</div>
<ul class="item-content user">
<li @click="openTransfer"><UserAvatar :userid="getOwner().userid" :size="28"/></li>
</ul>
<Poptip
ref="owner"
:title="$L('修改负责人')"
:width="240"
class="item-content user"
placement="bottom"
@on-popper-show="openOwner"
@on-popper-hide="ownerShow=false"
@on-ok="onOwner"
transfer>
<div slot="content">
<UserInput
v-if="ownerShow"
v-model="ownerData.owner_userid"
:multiple-max="1"
:placeholder="$L('选择任务负责人')"/>
<div class="task-detail-avatar-buttons">
<Button size="small" type="primary" @click="$refs.owner.ok()">{{$L('确定')}}</Button>
</div>
</div>
<div v-if="getOwner" class="user-list">
<UserAvatar :userid="getOwner.userid" :size="28" hide-icon-menu/>
</div>
<div v-else>--</div>
</Poptip>
</FormItem>
<FormItem v-if="getAssist.length > 0">
<div class="item-label" slot="label">
<i class="iconfont">&#xe63f;</i>{{$L('协助人员')}}
</div>
<ul class="item-content user">
<li v-for="item in getAssist" @click="openAssist"><UserAvatar :userid="item.userid" :size="28"/></li>
</ul>
<Poptip
ref="assist"
:title="$L('修改协助人员')"
:width="280"
class="item-content user"
placement="bottom"
@on-popper-show="openAssist"
@on-popper-hide="assistShow=false"
@on-ok="onAssist"
transfer>
<div slot="content">
<UserInput
v-if="assistShow"
v-model="assistData.assist_userid"
:multiple-max="10"
:disabled-choice="assistData.disabled"
:placeholder="$L('选择任务协助人员')"/>
<div class="task-detail-avatar-buttons">
<Button size="small" type="primary" @click="$refs.assist.ok()">{{$L('确定')}}</Button>
</div>
</div>
<div class="user-list">
<UserAvatar v-for="item in getAssist" :key="item.userid" :userid="item.userid" :size="28" hide-icon-menu/>
</div>
</Poptip>
</FormItem>
<FormItem v-if="taskDetail.end_at">
<div class="item-label" slot="label">
<i class="iconfont">&#xe6e8;</i>{{$L('截止时间')}}
</div>
<ul class="item-content">
<li>{{taskDetail.end_at}}</li>
<li>
<DatePicker
v-model="timeValue"
:open="timeOpen"
:options="timeOptions"
format="yyyy-MM-dd HH:mm"
type="datetimerange"
@on-open-change="timeChange"
@on-clear="timeClear"
@on-ok="timeOk"
transfer>
<div class="picker-time">
<div @click="openTime" class="time">{{cutTime}}</div>
<Tag v-if="!taskDetail.complete_at && taskDetail.today" color="blue"><Icon type="ios-time-outline"/>{{expiresFormat(taskDetail.end_at)}}</Tag>
<Tag v-if="!taskDetail.complete_at && taskDetail.overdue" color="red">{{$L('超期未完成')}}</Tag>
</div>
</DatePicker>
</li>
</ul>
</FormItem>
<FormItem v-if="hasFile">
@ -86,12 +233,13 @@
</div>
<ul class="item-content file">
<li v-for="file in taskDetail.files">
<img class="file-ext" :src="file.thumb"/>
<img v-if="file.id" class="file-ext" :src="file.thumb"/>
<Loading v-else class="file-load"/>
<div class="file-name">{{file.name}}</div>
<div class="file-size">{{$A.bytesToSize(file.size)}}</div>
</li>
<li>
<div class="add-button">
<div class="add-button" @click="$refs.upload.handleClick()">
<i class="iconfont">&#xe6f2;</i>{{$L('添加附件')}}
</div>
</li>
@ -102,27 +250,9 @@
<i class="iconfont">&#xe6f0;</i>{{$L('子任务')}}
</div>
<ul class="item-content subtask">
<li v-for="task in taskDetail.sub_task">
<Icon class="subtask-icon" type="md-radio-button-off" />
<div class="subtask-name">
<Input
v-model="task.name"
type="textarea"
:rows="1"
:autosize="{ minRows: 1, maxRows: 8 }"
:maxlength="255"/>
</div>
<div
v-if="task.end_at"
:class="['subtask-time-avatar', task.today ? 'today' : '', task.overdue ? 'overdue' : '']">{{expiresFormat(task.end_at)}}</div>
<UserAvatar
v-if="getOwner(task)"
class="subtask-avatar"
:userid="getOwner(task).userid"
:size="20"/>
</li>
<TaskDetail v-for="(task, key) in taskDetail.sub_task" :key="key" :open-task="task"/>
<li>
<div class="add-button">
<div class="add-button" @click="">
<i class="iconfont">&#xe6f2;</i>{{$L('添加子任务')}}
</div>
</li>
@ -132,7 +262,7 @@
<div class="add">
<EDropdown
trigger="click"
placement="bottom-start"
placement="bottom"
@command="">
<div class="add-button">
<i class="iconfont">&#xe6f2;</i>
@ -149,6 +279,7 @@
</EDropdown>
</div>
</div>
<TaskUpload ref="upload" class="upload"/>
</div>
<div class="task-dialog" >
<div class="head">
@ -173,47 +304,6 @@
</div>
</div>
</div>
<!--修改负责人-->
<Modal
v-model="transferShow"
:title="$L('修改负责人')"
:mask-closable="false">
<Form ref="addProject" :model="transferData" label-width="auto" @submit.native.prevent>
<FormItem prop="owner_userid" :label="$L('任务负责人')">
<UserInput
v-if="transferShow"
v-model="transferData.owner_userid"
:multiple-max="1"
:placeholder="$L('选择任务负责人')"/>
</FormItem>
</Form>
<div slot="footer">
<Button type="default" @click="transferShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="transferLoad > 0" @click="onTransfer">{{$L('提交')}}</Button>
</div>
</Modal>
<!--修改协助人员-->
<Modal
v-model="assistShow"
:title="$L('修改协助人员')"
:mask-closable="false">
<Form ref="addProject" :model="assistData" label-width="auto" @submit.native.prevent>
<FormItem prop="owner_userid" :label="$L('协助人员')">
<UserInput
v-if="assistShow"
v-model="assistData.assist_userid"
:multiple-max="1"
:disabled-choice="assistData.disabled"
:placeholder="$L('选择任务协助人员')"/>
</FormItem>
</Form>
<div slot="footer">
<Button type="default" @click="assistShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="assistLoad > 0" @click="onAssist">{{$L('提交')}}</Button>
</div>
</Modal>
</div>
</template>
@ -222,17 +312,26 @@ import {mapState} from "vuex";
import TEditor from "../../../components/TEditor";
import TaskPriority from "./TaskPriority";
import UserInput from "../../../components/UserInput";
import TaskUpload from "./TaskUpload";
export default {
name: "TaskDetail",
components: {UserInput, TaskPriority, TEditor},
components: {TaskUpload, UserInput, TaskPriority, TEditor},
props: {
openTask: {
type: Object,
default: () => {
return {};
}
},
},
data() {
return {
taskDetail: {},
transferShow: false,
transferData: {},
transferLoad: 0,
ownerShow: false,
ownerData: {},
ownerLoad: 0,
assistShow: false,
assistData: {},
@ -268,6 +367,12 @@ export default {
valid_elements : 'a[href|target=_blank],em,strong/b,div[align],span[style],a,br,img[src|alt|witdh|height],pre[class],code',
toolbar: 'uploadImages | uploadFiles | bold italic underline forecolor backcolor | codesample | preview screenload'
},
timeOpen: false,
timeValue: [],
timeOptions: {
shortcuts: []
},
}
},
@ -285,7 +390,7 @@ export default {
},
computed: {
...mapState(['userId', 'projectOpenTask', 'taskPriority']),
...mapState(['userId', 'taskPriority']),
scrollerStyle() {
const {innerHeight, taskDetail} = this;
@ -327,6 +432,18 @@ export default {
}
},
cutTime() {
const {nowTime, taskDetail} = this;
let string = "";
let start_at = Math.round(new Date(taskDetail.start_at).getTime() / 1000);
if (start_at > nowTime) {
string = $A.formatDate('Y/m/d H:i', start_at) + " ~ "
}
let end_at = Math.round(new Date(taskDetail.end_at).getTime() / 1000);
string+= $A.formatDate('Y/m/d H:i', end_at);
return string;
},
hasFile() {
const {taskDetail} = this;
return $A.isArray(taskDetail.files) && taskDetail.files.length > 0;
@ -338,15 +455,11 @@ export default {
},
getOwner() {
return function (task) {
if (task === undefined) {
task = this.taskDetail;
}
if (!$A.isArray(task.task_user)) {
return null;
}
return task.task_user.find(({owner}) => owner === 1);
const {taskDetail} = this;
if (!$A.isArray(taskDetail.task_user)) {
return null;
}
return taskDetail.task_user.find(({owner}) => owner === 1);
},
getAssist() {
@ -407,13 +520,75 @@ export default {
},
watch: {
projectOpenTask(data) {
this.taskDetail = $A.cloneJSON(data);
if (data._show) this.$nextTick(this.$refs.input.focus)
openTask: {
handler(data) {
this.taskDetail = $A.cloneJSON(data);
},
immediate: true,
deep: true
},
'openTask._show' (v) {
if (v) {
this.$nextTick(this.$refs.input.focus)
} else {
this.timeOpen = false;
}
}
},
methods: {
initLanguage() {
const lastSecond = (e) => {
return new Date($A.formatDate("Y-m-d 23:59:29", Math.round(e / 1000)))
};
this.timeOptions = {
shortcuts: [{
text: this.$L('今天'),
value() {
return [new Date(), lastSecond(new Date().getTime())];
}
}, {
text: this.$L('明天'),
value() {
let e = new Date();
e.setDate(e.getDate() + 1);
return [new Date(), lastSecond(e.getTime())];
}
}, {
text: this.$L('本周'),
value() {
return [$A.getData('今天', true), lastSecond($A.getData('本周结束2', true))];
}
}, {
text: this.$L('本月'),
value() {
return [$A.getData('今天', true), lastSecond($A.getData('本月结束', true))];
}
}, {
text: this.$L('3天'),
value() {
let e = new Date();
e.setDate(e.getDate() + 3);
return [new Date(), lastSecond(e.getTime())];
}
}, {
text: this.$L('5天'),
value() {
let e = new Date();
e.setDate(e.getDate() + 5);
return [new Date(), lastSecond(e.getTime())];
}
}, {
text: this.$L('7天'),
value() {
let e = new Date();
e.setDate(e.getDate() + 7);
return [new Date(), lastSecond(e.getTime())];
}
}]
};
},
innerHeightListener() {
this.innerHeight = window.innerHeight;
},
@ -454,6 +629,28 @@ export default {
return duration;
},
onNameKeydown(e) {
if (e.keyCode === 13) {
if (e.shiftKey) {
return;
}
e.preventDefault();
this.updateData('name');
}
},
dropTask(command) {
if (command === 'complete') {
this.updateData('complete')
}
else if (command === 'uncomplete') {
this.updateData('uncomplete')
}
else if (command === 'delete') {
this.archivedOrRemoveTask('delete');
}
},
updateData(action, params) {
switch (action) {
case 'complete':
@ -470,46 +667,77 @@ export default {
this.$set(this.taskDetail, 'p_color', params.color)
action = ['p_level', 'p_name', 'p_color'];
break;
case 'times':
this.$set(this.taskDetail, 'times', [params.start_at, params.end_at])
break;
}
//
let dataJson = {task_id: this.taskDetail.id};
($A.isArray(action) ? action : [action]).forEach(key => {
let newData = this.taskDetail[key];
let originalData = this.projectOpenTask[key];
let originalData = this.openTask[key];
if ($A.jsonStringify(newData) != $A.jsonStringify(originalData)) {
dataJson[key] = newData;
}
})
if (Object.keys(dataJson).length <= 1) return;
//
this.$store.dispatch("taskUpdate", dataJson).then(() => {
this.$store.dispatch("taskUpdate", dataJson).then(({msg}) => {
//
}).catch(() => {
$A.messageSuccess(msg);
}).catch(({msg}) => {
//
$A.modalError(msg);
})
},
openTransfer() {
this.$set(this.taskDetail, 'owner_userid', [this.getOwner().userid])
this.$set(this.transferData, 'owner_userid', [this.getOwner().userid]);
this.transferShow = true;
archivedOrRemoveTask(type) {
let typeTitle = this.taskDetail.parent_id > 0 ? '子任务' : '任务';
$A.modalConfirm({
title: '删除' + typeTitle,
content: '你确定要删除' + typeTitle + '【' + this.taskDetail.name + '】吗?',
loading: true,
onOk: () => {
if (this.taskDetail.loading === true) {
return;
}
this.$set(this.taskDetail, 'loading', true);
this.$store.dispatch("taskArchivedOrRemove", {
task_id: this.taskDetail.id,
type: type,
}).then(({msg}) => {
this.$Modal.remove();
$A.messageSuccess(msg);
}).catch(({msg}) => {
this.$Modal.remove();
$A.modalError(msg, 301);
});
}
});
},
onTransfer() {
if ($A.jsonStringify(this.taskDetail.owner_userid) === $A.jsonStringify(this.transferData.owner_userid)) {
openOwner() {
this.$set(this.taskDetail, 'owner_userid', [this.getOwner.userid])
this.$set(this.ownerData, 'owner_userid', [this.getOwner.userid]);
this.ownerShow = true;
},
onOwner() {
if ($A.jsonStringify(this.taskDetail.owner_userid) === $A.jsonStringify(this.ownerData.owner_userid)) {
return;
}
this.transferLoad++;
this.ownerLoad++;
this.$store.dispatch("taskUpdate", {
task_id: this.taskDetail.id,
owner: this.transferData.owner_userid
}).then(() => {
this.transferLoad--;
this.transferShow = false;
owner: this.ownerData.owner_userid
}).then(({msg}) => {
this.ownerLoad--;
this.ownerShow = false;
this.$store.dispatch("taskOne", this.taskDetail.id);
$A.messageSuccess(msg);
}).catch(({msg}) => {
this.transferLoad--;
this.transferShow = false;
this.ownerLoad--;
this.ownerShow = false;
$A.modalError(msg);
})
},
@ -518,7 +746,7 @@ export default {
const list = this.getAssist.map(({userid}) => userid)
this.$set(this.taskDetail, 'assist_userid', list)
this.$set(this.assistData, 'assist_userid', list);
this.$set(this.assistData, 'disabled', [this.getOwner().userid]);
this.$set(this.assistData, 'disabled', [this.getOwner.userid]);
this.assistShow = true;
},
@ -532,16 +760,58 @@ export default {
this.$store.dispatch("taskUpdate", {
task_id: this.taskDetail.id,
assist,
}).then(() => {
}).then(({msg}) => {
this.assistLoad--;
this.assistShow = false;
this.$store.dispatch("taskOne", this.taskDetail.id);
$A.messageSuccess(msg);
}).catch(({msg}) => {
this.assistLoad--;
this.assistShow = false;
$A.modalError(msg);
})
}
},
openTime() {
this.timeOpen = !this.timeOpen;
if (this.timeOpen) {
this.timeValue = this.taskDetail.end_at ? [this.taskDetail.start_at, this.taskDetail.end_at] : [];
}
},
timeChange(open) {
if (!open) {
this.timeOpen = false;
}
},
timeClear() {
$A.modalConfirm({
content: '你确定要取消任务时间吗?',
cancelText: '不是',
onOk: () => {
this.updateData('times', {
start_at: false,
end_at: false,
});
this.timeOpen = false;
}
});
},
timeOk() {
let times = $A.date2string(this.timeValue, "Y-m-d H:i");
if (times[0] && times[1]) {
if ($A.rightExists(times[0], '00:00') && $A.rightExists(times[1], '00:00')) {
times[1] = times[1].replace("00:00", "23:59");
}
}
this.updateData('times', {
start_at: times[0],
end_at: times[1],
});
this.timeOpen = false;
},
}
}
</script>

View File

@ -0,0 +1,107 @@
<template>
<Upload
name="files"
ref="upload"
:action="actionUrl"
:data="params"
multiple
:format="uploadFormat"
:show-upload-list="false"
:max-size="maxSize"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-format-error="handleFormatError"
:on-exceeded-size="handleMaxSize">
</Upload>
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'TaskUpload',
props: {
maxSize: {
type: Number,
default: 204800
}
},
data() {
return {
uploadFormat: ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'],
actionUrl: this.$store.state.method.apiUrl('project/task/upload'),
}
},
computed: {
...mapState(['userToken', 'projectOpenTask']),
params() {
return {
task_id: this.projectOpenTask.id,
token: this.userToken,
}
}
},
methods: {
handleProgress(event, file) {
//
if (typeof file.tempId === "undefined") {
file.tempId = $A.randomString(8);
this.projectOpenTask.files.push(file);
}
},
handleSuccess(res, file) {
//
let index = this.projectOpenTask.files.findIndex(({tempId}) => tempId == file.tempId);
if (res.ret === 1) {
if (index > -1) {
this.projectOpenTask.files.splice(index, 1, res.data);
this.$store.dispatch("taskData", {
id: this.projectOpenTask.id,
file_num: this.projectOpenTask.files.length,
});
}
} else {
if (index > -1) {
this.projectOpenTask.files.splice(index, 1);
}
this.$refs.upload.fileList.pop();
$A.modalWarning({
title: '发送失败',
content: '文件 ' + file.name + ' 发送失败,' + res.msg
});
}
},
handleFormatError(file) {
//
$A.modalWarning({
title: '文件格式不正确',
content: '文件 ' + file.name + ' 格式不正确,仅支持发送:' + this.uploadFormat.join(',')
});
},
handleMaxSize(file) {
//
$A.modalWarning({
title: '超出文件大小限制',
content: '文件 ' + file.name + ' 太大,不能发送超过' + $A.bytesToSize(this.maxSize * 1024) + '。'
});
},
handleClick() {
//
this.$refs.upload.handleClick()
},
upload(file) {
//file
this.$refs.upload.upload(file);
},
}
}
</script>

View File

@ -389,6 +389,11 @@ export default {
});
if (data.id == state.projectOpenTask.id) {
state.projectOpenTask = Object.assign({}, state.projectOpenTask, data);
} else if (data.parent_id == state.projectOpenTask.id) {
let index = state.projectOpenTask.sub_task.findIndex(({id}) => id === data.id);
if (index > -1) {
state.projectOpenTask.sub_task.splice(index, 1, Object.assign(state.projectOpenTask.sub_task[index], data))
}
}
},
@ -522,28 +527,77 @@ export default {
},
/**
* 更新任务
* 添加任务
* @param state
* @param dispatch
* @param data
* @returns {Promise<unknown>}
*/
taskUpdate({dispatch}, data) {
taskAdd({state, dispatch}, data) {
return new Promise(function (resolve, reject) {
const post = state.method.cloneJSON(data);
if (state.method.isArray(post.column_id)) {
post.column_id = post.column_id.find((val) => val)
}
if (state.method.isArray(post.owner)) {
post.owner = post.owner.find((val) => val)
}
//
dispatch("call", {
url: 'project/task/add',
data: post,
method: 'post',
}).then(result => {
const {task, in_top, new_column} = result.data;
if (state.projectDetail.id == task.project_id) {
if (new_column) {
state.projectDetail.project_column.push(new_column);
}
const column = state.projectDetail.project_column.find(({id}) => id === task.column_id);
if (column) {
if (in_top) {
column.project_task.unshift(task);
} else {
column.project_task.push(task);
}
}
}
dispatch('projectOne', task.project_id);
resolve(result)
}).catch(result => {
reject(result)
});
});
},
/**
* 更新任务
* @param state
* @param dispatch
* @param data
* @returns {Promise<unknown>}
*/
taskUpdate({state, dispatch}, data) {
return new Promise(function (resolve, reject) {
const post = state.method.cloneJSON(data);
if (state.method.isArray(post.owner)) {
post.owner = post.owner.find((id) => id)
}
dispatch("call", {
url: 'project/task/update',
data: data,
data: post,
method: 'post',
}).then(result => {
if (result.data.parent_id) {
dispatch('taskOne', result.data.parent_id);
}
if (typeof data.complete_at !== "undefined") {
if (typeof post.complete_at !== "undefined") {
dispatch('projectOne', result.data.project_id);
}
dispatch("taskData", result.data);
resolve(result)
}).catch(result => {
dispatch('taskOne', data.task_id);
dispatch('taskOne', post.task_id);
reject(result)
});
});
@ -553,7 +607,7 @@ export default {
* 删除或归档任务
* @param state
* @param dispatch
* @param data
* @param data {task_id, type}
* @returns {Promise<unknown>}
*/
taskArchivedOrRemove({state, dispatch}, data) {
@ -565,14 +619,23 @@ export default {
task_id,
},
}).then(result => {
const column = state.projectDetail.project_column.find(({id}) => id === result.data.column_id);
const {data} = result;
const column = state.projectDetail.project_column.find(({id}) => id === data.column_id);
if (column) {
let index = column.project_task.findIndex(({id}) => id === result.data.id);
let index = column.project_task.findIndex(({id}) => id === data.id);
if (index > -1) {
column.project_task.splice(index, 1);
}
}
dispatch('projectDetail', result.data.project_id);
if (data.id == state.projectOpenTask.id) {
state.projectOpenTask = Object.assign({}, state.projectOpenTask, {_show: false});
} else if (data.parent_id == state.projectOpenTask.id) {
let index = state.projectOpenTask.sub_task.findIndex(({id}) => id === data.id);
if (index > -1) {
state.projectOpenTask.sub_task.splice(index, 1)
}
}
dispatch('projectDetail', data.project_id);
resolve(result);
}).catch(result => {
reject(result)

View File

@ -109,18 +109,49 @@
line-height: 26px;
.el-dropdown {
display: flex;
cursor: pointer;
}
.picker-time {
display: flex;
align-items: center;
line-height: 26px;
.time {
cursor: pointer;
}
.ivu-tag {
margin-left: 10px;
padding: 0 4px;
height: 20px;
line-height: 18px;
.ivu-tag-text {
display: flex;
align-items: center;
> i {
padding-right: 2px;
}
}
}
}
}
&.user {
margin-top: 0;
> li {
display: inline-block;
cursor: pointer;
margin-top: 1px;
cursor: pointer;
.user-list {
> div {
display: inline-block;
margin-right: 6px;
}
}
}
&.file {
> li {
margin-bottom: 2px;
.file-load {
margin: 0;
padding: 2px;
width: 16px;
height: 16px;
}
.file-ext {
width: 16px;
}
@ -138,17 +169,25 @@
&.subtask {
> li {
align-items: flex-start;
margin-bottom: 2px;
margin-bottom: 4px;
.subtask-icon {
width: 16px;
height: 26px;
line-height: 26px;
font-size: 16px;
color: #cccccc;
margin-right: 8px;
margin-right: 6px;
cursor: pointer;
&.completed {
color: #87d068;
.loading {
width: 16px;
height: 16px;
margin: 0;
padding: 2px;
}
.ivu-icon {
font-size: 16px;
color: #cccccc;
&.completed {
color: #87d068;
}
}
&.sub-icon {
font-size: 16px;
@ -168,7 +207,7 @@
margin-right: 16px;
display: flex;
.ivu-input {
margin: -1px 0;
margin: -2px 0;
padding: 4px 0;
resize: none;
border-color: transparent;
@ -179,31 +218,28 @@
}
}
.subtask-time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 6px;
font-size: 13px;
height: 26px;
line-height: 26px;
&.overdue {
font-weight: 600;
color: #ed4014;
}
&.today {
font-weight: 500;
color: #ff9900;
margin-right: 8px;
.time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
height: 26px;
line-height: 26px;
cursor: pointer;
&.overdue {
font-weight: 600;
color: #ed4014;
}
&.today {
font-weight: 500;
color: #ff9900;
}
}
}
.subtask-avatar {
height: 26px;
line-height: 1;
.avatar-box {
> em {
right: -2px;
bottom: -1px;
}
}
cursor: pointer;
}
}
}
@ -240,6 +276,9 @@
}
}
}
.upload {
display: none;
}
}
.task-dialog {
flex: 1;
@ -388,4 +427,15 @@
}
}
.task-detail-avatar-buttons {
margin-top: 12px;
margin-bottom: 4px;
text-align: right;
position: absolute;
top: 5px;
right: 14px;
> button {
font-size: 12px;
transform: scale(0.9);
}
}